< Summary - Combined Code Coverage

Information
Class: NLightning.Infrastructure.Bitcoin.Signers.LocalLightningSigner
Assembly: NLightning.Infrastructure.Bitcoin
File(s): /home/runner/work/nlightning/nlightning/src/NLightning.Infrastructure.Bitcoin/Signers/LocalLightningSigner.cs
Tag: 36_15743069263
Line coverage
37%
Covered lines: 56
Uncovered lines: 93
Coverable lines: 149
Total lines: 316
Line coverage: 37.5%
Branch coverage
38%
Covered branches: 7
Total branches: 18
Branch coverage: 38.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/nlightning/nlightning/src/NLightning.Infrastructure.Bitcoin/Signers/LocalLightningSigner.cs

#LineLine coverage
 1using System.Collections.Concurrent;
 2using Microsoft.Extensions.Logging;
 3using NBitcoin;
 4using NBitcoin.Crypto;
 5using NLightning.Domain.Bitcoin.Transactions.Outputs;
 6
 7namespace NLightning.Infrastructure.Bitcoin.Signers;
 8
 9using Builders;
 10using Domain.Bitcoin.Interfaces;
 11using Domain.Bitcoin.ValueObjects;
 12using Domain.Channels.ValueObjects;
 13using Domain.Crypto.Constants;
 14using Domain.Crypto.ValueObjects;
 15using Domain.Exceptions;
 16using Domain.Node.Options;
 17using Domain.Protocol.Interfaces;
 18
 19public class LocalLightningSigner : ILightningSigner
 20{
 21    private const int FundingDerivationIndex = 0; // m/0' is the funding key
 22    private const int RevocationDerivationIndex = 1; // m/1' is the revocation key
 23    private const int PaymentDerivationIndex = 2; // m/2' is the payment key
 24    private const int DelayedPaymentDerivationIndex = 3; // m/3' is the delayed payment key
 25    private const int HtlcDerivationIndex = 4; // m/4' is the HTLC key
 26    private const int PerCommitmentSeedDerivationIndex = 5; // m/5' is the per-commitment seed
 27
 28    private readonly ISecureKeyManager _secureKeyManager;
 29    private readonly IFundingOutputBuilder _fundingOutputBuilder;
 30    private readonly IKeyDerivationService _keyDerivationService;
 7231    private readonly ConcurrentDictionary<ChannelId, ChannelSigningInfo> _channelSigningInfo = new();
 32    private readonly ILogger<LocalLightningSigner> _logger;
 33    private readonly Network _network;
 34
 7235    public LocalLightningSigner(IFundingOutputBuilder fundingOutputBuilder, IKeyDerivationService keyDerivationService,
 7236                                ILogger<LocalLightningSigner> logger, NodeOptions nodeOptions,
 7237                                ISecureKeyManager secureKeyManager)
 38    {
 7239        _fundingOutputBuilder = fundingOutputBuilder;
 7240        _keyDerivationService = keyDerivationService;
 7241        _logger = logger;
 7242        _secureKeyManager = secureKeyManager;
 43
 7244        _network = Network.GetNetwork(nodeOptions.BitcoinNetwork) ??
 7245                   throw new ArgumentException("Invalid Bitcoin network specified", nameof(nodeOptions));
 46
 47        // TODO: Load channel key data from database
 7248    }
 49
 50    /// <inheritdoc />
 51    public uint CreateNewChannel(out ChannelBasepoints basepoints, out CompactPubKey firstPerCommitmentPoint)
 52    {
 53        // Generate a new key for this channel
 054        var channelPrivExtKey = _secureKeyManager.GetNextKey(out var index);
 055        var channelKey = ExtKey.CreateFromBytes(channelPrivExtKey);
 56
 57        // Generate Lightning basepoints using proper BIP32 derivation paths
 058        using var localFundingSecret = GenerateFundingPrivateKey(channelKey);
 059        using var localRevocationSecret = channelKey.Derive(RevocationDerivationIndex, true).PrivateKey;
 060        using var localPaymentSecret = channelKey.Derive(PaymentDerivationIndex, true).PrivateKey;
 061        using var localDelayedPaymentSecret = channelKey.Derive(DelayedPaymentDerivationIndex, true).PrivateKey;
 062        using var localHtlcSecret = channelKey.Derive(HtlcDerivationIndex, true).PrivateKey;
 063        using var perCommitmentSeed = channelKey.Derive(PerCommitmentSeedDerivationIndex, true).PrivateKey;
 64
 65        // Generate static basepoints (these don't change per commitment)
 066        basepoints = new ChannelBasepoints(
 067            localFundingSecret.PubKey.ToBytes(),
 068            localRevocationSecret.PubKey.ToBytes(),
 069            localPaymentSecret.PubKey.ToBytes(),
 070            localDelayedPaymentSecret.PubKey.ToBytes(),
 071            localHtlcSecret.PubKey.ToBytes()
 072        );
 73
 74        // Generate the first per-commitment point
 075        var firstPerCommitmentSecretBytes = _keyDerivationService
 076           .GeneratePerCommitmentSecret(perCommitmentSeed.ToBytes(), CryptoConstants.FirstPerCommitmentIndex);
 077        using var firstPerCommitmentSecret = new Key(firstPerCommitmentSecretBytes);
 078        firstPerCommitmentPoint = firstPerCommitmentSecret.PubKey.ToBytes();
 79
 080        return index;
 081    }
 82
 83    /// <inheritdoc />
 84    public ChannelBasepoints GetChannelBasepoints(uint channelKeyIndex)
 85    {
 086        _logger.LogTrace("Generating channel basepoints for key index {ChannelKeyIndex}", channelKeyIndex);
 87
 88        // Recreate the basepoints from the channel key index
 089        var channelExtKey = _secureKeyManager.GetKeyAtIndex(channelKeyIndex);
 090        var channelKey = ExtKey.CreateFromBytes(channelExtKey);
 91
 092        using var localFundingSecret = channelKey.Derive(FundingDerivationIndex, true).PrivateKey;
 093        using var localRevocationSecret = channelKey.Derive(RevocationDerivationIndex, true).PrivateKey;
 094        using var localPaymentSecret = channelKey.Derive(PaymentDerivationIndex, true).PrivateKey;
 095        using var localDelayedPaymentSecret = channelKey.Derive(DelayedPaymentDerivationIndex, true).PrivateKey;
 096        using var localHtlcSecret = channelKey.Derive(HtlcDerivationIndex, true).PrivateKey;
 97
 098        return new ChannelBasepoints(
 099            localFundingSecret.PubKey.ToBytes(),
 0100            localRevocationSecret.PubKey.ToBytes(),
 0101            localPaymentSecret.PubKey.ToBytes(),
 0102            localDelayedPaymentSecret.PubKey.ToBytes(),
 0103            localHtlcSecret.PubKey.ToBytes()
 0104        );
 0105    }
 106
 107    /// <inheritdoc />
 108    public ChannelBasepoints GetChannelBasepoints(ChannelId channelId)
 109    {
 0110        _logger.LogTrace("Retrieving channel basepoints for channel {ChannelId}", channelId);
 111
 0112        if (!_channelSigningInfo.TryGetValue(channelId, out var signingInfo))
 0113            throw new InvalidOperationException($"Channel {channelId} not registered");
 114
 0115        return GetChannelBasepoints(signingInfo.ChannelKeyIndex);
 116    }
 117
 118    /// <inheritdoc />
 0119    public CompactPubKey GetNodePublicKey() => _secureKeyManager.GetNodeKeyPair().CompactPubKey;
 120
 121    /// <inheritdoc />
 122    public CompactPubKey GetPerCommitmentPoint(uint channelKeyIndex, ulong commitmentNumber)
 123    {
 0124        _logger.LogTrace(
 0125            "Generating per-commitment point for channel key index {ChannelKeyIndex} and commitment number {CommitmentNu
 0126            channelKeyIndex, commitmentNumber);
 127
 128        // Derive the per-commitment seed from the channel key
 0129        var channelExtKey = _secureKeyManager.GetKeyAtIndex(channelKeyIndex);
 0130        var channelKey = ExtKey.CreateFromBytes(channelExtKey);
 0131        using var perCommitmentSeed = channelKey.Derive(5).PrivateKey;
 132
 0133        var perCommitmentSecret =
 0134            _keyDerivationService.GeneratePerCommitmentSecret(perCommitmentSeed.ToBytes(), commitmentNumber);
 135
 0136        var perCommitmentPoint = new Key(perCommitmentSecret).PubKey;
 0137        return perCommitmentPoint.ToBytes();
 0138    }
 139
 140    /// <inheritdoc />
 141    public CompactPubKey GetPerCommitmentPoint(ChannelId channelId, ulong commitmentNumber)
 142    {
 0143        if (!_channelSigningInfo.TryGetValue(channelId, out var signingInfo))
 0144            throw new InvalidOperationException($"Channel {channelId} not registered");
 145
 0146        return GetPerCommitmentPoint(signingInfo.ChannelKeyIndex, commitmentNumber);
 147    }
 148
 149    /// <inheritdoc />
 150    public void RegisterChannel(ChannelId channelId, ChannelSigningInfo signingInfo)
 151    {
 68152        _logger.LogTrace("Registering channel {ChannelId} with signing info", channelId);
 153
 68154        _channelSigningInfo.TryAdd(channelId, signingInfo);
 68155    }
 156
 157    /// <inheritdoc />
 158    public Secret ReleasePerCommitmentSecret(uint channelKeyIndex, ulong commitmentNumber)
 159    {
 0160        _logger.LogTrace(
 0161            "Releasing per-commitment secret for channel key index {ChannelKeyIndex} and commitment number {CommitmentNu
 0162            channelKeyIndex, commitmentNumber);
 163
 164        // Derive the per-commitment seed from the channel key
 0165        var channelExtKey = _secureKeyManager.GetKeyAtIndex(channelKeyIndex);
 0166        var channelKey = ExtKey.CreateFromBytes(channelExtKey);
 0167        using var perCommitmentSeed = channelKey.Derive(5).PrivateKey;
 168
 0169        return _keyDerivationService.GeneratePerCommitmentSecret(
 0170            perCommitmentSeed.ToBytes(), commitmentNumber);
 0171    }
 172
 173    /// <inheritdoc />
 174    public Secret ReleasePerCommitmentSecret(ChannelId channelId, ulong commitmentNumber)
 175    {
 0176        if (!_channelSigningInfo.TryGetValue(channelId, out var signingInfo))
 0177            throw new InvalidOperationException($"Channel {channelId} not registered");
 178
 0179        return ReleasePerCommitmentSecret(signingInfo.ChannelKeyIndex, commitmentNumber);
 180    }
 181
 182    /// <inheritdoc />
 183    public CompactSignature SignTransaction(ChannelId channelId, SignedTransaction unsignedTransaction)
 184    {
 64185        _logger.LogTrace("Signing transaction for channel {ChannelId} with TxId {TxId}", channelId,
 64186                         unsignedTransaction.TxId);
 187
 64188        if (!_channelSigningInfo.TryGetValue(channelId, out var signingInfo))
 0189            throw new InvalidOperationException($"Channel {channelId} not registered with signer");
 190
 191        Transaction nBitcoinTx;
 192        try
 193        {
 64194            nBitcoinTx = Transaction.Load(unsignedTransaction.RawTxBytes, _network);
 64195        }
 0196        catch (Exception ex)
 197        {
 0198            throw new ArgumentException(
 0199                $"Failed to load transaction from RawTxBytes. TxId hint: {unsignedTransaction.TxId}", ex);
 200        }
 201
 202        try
 203        {
 204            // Build the funding output using the channel's signing info
 64205            var fundingOutputInfo = new FundingOutputInfo(signingInfo.FundingSatoshis, signingInfo.LocalFundingPubKey,
 64206                                                          signingInfo.RemoteFundingPubKey, signingInfo.FundingTxId,
 64207                                                          signingInfo.FundingOutputIndex);
 208
 64209            var fundingOutput = _fundingOutputBuilder.Build(fundingOutputInfo);
 64210            var spentOutput = fundingOutput.ToTxOut();
 211
 212            // Get the signature hash for SegWit
 64213            var signatureHash = nBitcoinTx.GetSignatureHash(fundingOutput.RedeemScript,
 64214                                                            (int)signingInfo.FundingOutputIndex, SigHash.All,
 64215                                                            spentOutput, HashVersion.WitnessV0);
 216
 217            // Get the funding private key
 64218            using var fundingPrivateKey = GenerateFundingPrivateKey(signingInfo.ChannelKeyIndex);
 219
 64220            var signature = fundingPrivateKey.Sign(signatureHash, new SigningOptions(SigHash.All, false));
 221
 64222            return signature.Signature.MakeCanonical().ToCompact();
 223        }
 0224        catch (Exception ex)
 225        {
 0226            throw new InvalidOperationException(
 0227                $"Exception during signature verification for TxId {nBitcoinTx.GetHash()}", ex);
 228        }
 64229    }
 230
 231    /// <inheritdoc />
 232    public void ValidateSignature(ChannelId channelId, CompactSignature signature,
 233                                  SignedTransaction unsignedTransaction)
 234    {
 72235        _logger.LogTrace("Validating signature for channel {ChannelId} with TxId {TxId}", channelId,
 72236                         unsignedTransaction.TxId);
 237
 72238        if (!_channelSigningInfo.TryGetValue(channelId, out var signingInfo))
 4239            throw new SignerException("Channel not registered with signer", channelId, "Internal error");
 240
 241        Transaction nBitcoinTx;
 242        try
 243        {
 68244            nBitcoinTx = Transaction.Load(unsignedTransaction.RawTxBytes, _network);
 68245        }
 0246        catch (Exception e)
 247        {
 0248            throw new SignerException("Failed to load transaction from RawTxBytes", channelId, e, "Internal error");
 249        }
 250
 251        PubKey pubKey;
 252        try
 253        {
 68254            pubKey = new PubKey(signingInfo.RemoteFundingPubKey);
 68255        }
 0256        catch (Exception e)
 257        {
 0258            throw new SignerException("Failed to parse public key from CompactPubKey", channelId, e, "Internal error");
 259        }
 260
 261        ECDSASignature txSignature;
 262        try
 263        {
 68264            if (!ECDSASignature.TryParseFromCompact(signature, out txSignature))
 0265                throw new SignerException("Failed to parse compact signature", channelId, "Signature format error");
 266
 68267            if (!txSignature.IsLowS)
 0268                throw new SignerException("Signature is not low S", channelId,
 0269                                          "Signature is malleable");
 68270        }
 0271        catch (Exception e)
 272        {
 0273            throw new SignerException("Failed to parse DER signature", channelId, e,
 0274                                      "Signature format error");
 275        }
 276
 277        try
 278        {
 279            // Build the funding output using the channel's signing info
 68280            var fundingOutputInfo = new FundingOutputInfo(signingInfo.FundingSatoshis, signingInfo.LocalFundingPubKey,
 68281                                                          signingInfo.RemoteFundingPubKey)
 68282            {
 68283                TransactionId = signingInfo.FundingTxId,
 68284                Index = signingInfo.FundingOutputIndex
 68285            };
 286
 68287            var fundingOutput = _fundingOutputBuilder.Build(fundingOutputInfo);
 68288            var spentOutput = fundingOutput.ToTxOut();
 289
 68290            var signatureHash = nBitcoinTx.GetSignatureHash(fundingOutput.RedeemScript,
 68291                                                            (int)signingInfo.FundingOutputIndex, SigHash.All,
 68292                                                            spentOutput, HashVersion.WitnessV0);
 293
 68294            if (!pubKey.Verify(signatureHash, txSignature))
 0295                throw new SignerException("Peer signature is invalid", channelId, "Invalid signature provided");
 68296        }
 0297        catch (Exception e)
 298        {
 0299            throw new SignerException("Exception during signature verification", channelId, e,
 0300                                      "Signature verification error");
 301        }
 68302    }
 303
 304    protected virtual Key GenerateFundingPrivateKey(uint channelKeyIndex)
 305    {
 0306        var channelExtKey = _secureKeyManager.GetKeyAtIndex(channelKeyIndex);
 0307        var channelKey = ExtKey.CreateFromBytes(channelExtKey);
 308
 0309        return GenerateFundingPrivateKey(channelKey);
 310    }
 311
 312    private Key GenerateFundingPrivateKey(ExtKey extKey)
 313    {
 0314        return extKey.Derive(FundingDerivationIndex, true).PrivateKey;
 315    }
 316}