Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Bolt3 #19

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/NLightning.Bolts/BOLT3/Comparers/TransactionOutputComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
namespace NLightning.Bolts.BOLT3.Transactions;

public class TransactionOutputComparer : IComparer<TransactionOutput>
{
public int Compare(TransactionOutput? x, TransactionOutput? y)
{
// Deal with nulls
if (x == null && y == null) return 0;
if (x == null) return -1;
if (y == null) return 1;

// Compare by value (satoshis)
var valueComparison = x.Value.CompareTo(y.Value);
if (valueComparison != 0)
{
return valueComparison;
}

// Compare by scriptPubKey lexicographically
var scriptComparison = CompareScriptPubKey(x.ScriptPubKey, y.ScriptPubKey);
if (scriptComparison != 0)
{
return scriptComparison;
}

// Compare by length if scripts are identical up to the length of the shorter one
if (x.ScriptPubKey.Length != y.ScriptPubKey.Length)
{
return x.ScriptPubKey.Length.CompareTo(y.ScriptPubKey.Length);
}

// For HTLC outputs, compare by CLTV expiry
if (x.CltvExpiry != y.CltvExpiry)
{
return x.CltvExpiry.CompareTo(y.CltvExpiry);
}

return 0;
}

private static int CompareScriptPubKey(byte[] script1, byte[] script2)
{
var length = Math.Min(script1.Length, script2.Length);
for (var i = 0; i < length; i++)
{
if (script1[i] != script2[i])
{
return script1[i].CompareTo(script2[i]);
}
}

return script1.Length.CompareTo(script2.Length);
}
}
176 changes: 176 additions & 0 deletions src/NLightning.Bolts/BOLT3/Factories/CommitmentTransactionFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
using NBitcoin;

namespace NLightning.Bolts.BOLT3.Factories;

using Transactions;

public class CommitmentTransactionFactory
{
public CommitmentTransaction CreateCommitmentTransaction(
uint256 fundingTxId,
uint fundingOutputIndex,
Money fundingAmount,
PubKey localPubKey,
PubKey remotePubKey,
PubKey localDelayedPubKey,
PubKey revocationPubKey,
Money toLocalAmount,
Money toRemoteAmount,
uint toSelfDelay,
ulong obscuredCommitmentNumber,
List<Htlc> htlcs,

Check failure on line 21 in src/NLightning.Bolts/BOLT3/Factories/CommitmentTransactionFactory.cs

View workflow job for this annotation

GitHub Actions / build-pr

The type or namespace name 'Htlc' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 21 in src/NLightning.Bolts/BOLT3/Factories/CommitmentTransactionFactory.cs

View workflow job for this annotation

GitHub Actions / build-pr

The type or namespace name 'Htlc' could not be found (are you missing a using directive or an assembly reference?)
Money dustLimitSatoshis,
FeeRate feeRatePerKw,
bool optionAnchors)
{
var tx = Transaction.Create(Network.Main);

// Initialize the commitment transaction input and locktime
InitializeTransaction(tx, fundingTxId, fundingOutputIndex, obscuredCommitmentNumber);

// Calculate which committed HTLCs need to be trimmed
var trimmedHtlcs = TrimmedOutputs(htlcs, dustLimitSatoshis, feeRatePerKw);

// Calculate the base commitment transaction fee
var baseFee = CalculateBaseFee(feeRatePerKw, trimmedHtlcs.Count);

// Adjust amounts for fees and anchors
AdjustAmountsForFees(ref toLocalAmount, ref toRemoteAmount, baseFee, optionAnchors);

// Add HTLC outputs
AddHtlcOutputs(tx, trimmedHtlcs, localPubKey, remotePubKey);

// Add to_local output
if (toLocalAmount >= dustLimitSatoshis)
{
AddToLocalOutput(tx, toLocalAmount, localDelayedPubKey, revocationPubKey, toSelfDelay);
}

// Add to_remote output
if (toRemoteAmount >= dustLimitSatoshis)
{
AddToRemoteOutput(tx, toRemoteAmount, remotePubKey, optionAnchors);
}

// Add anchor outputs if option_anchors applies
if (optionAnchors)
{
AddAnchorOutputs(tx, toLocalAmount, toRemoteAmount, trimmedHtlcs.Count, localPubKey, remotePubKey);
}

// Sort the outputs into BIP 69+CLTV order
SortOutputs(tx);

return new CommitmentTransaction(tx);
}

private void InitializeTransaction(Transaction tx, uint256 fundingTxId, uint fundingOutputIndex, ulong obscuredCommitmentNumber)
{
tx.Version = 2;
tx.LockTime = new LockTime((0x20 << 24) | (uint)(obscuredCommitmentNumber & 0xFFFFFF));
tx.Inputs.Add(new TxIn(new OutPoint(fundingTxId, fundingOutputIndex))
{
Sequence = (0x80 << 24) | (uint)((obscuredCommitmentNumber >> 24) & 0xFFFFFF)
});
}

private List<Htlc> TrimmedOutputs(List<Htlc> htlcs, Money dustLimitSatoshis, FeeRate feeRatePerKw)

Check failure on line 77 in src/NLightning.Bolts/BOLT3/Factories/CommitmentTransactionFactory.cs

View workflow job for this annotation

GitHub Actions / build-pr

The type or namespace name 'Htlc' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 77 in src/NLightning.Bolts/BOLT3/Factories/CommitmentTransactionFactory.cs

View workflow job for this annotation

GitHub Actions / build-pr

The type or namespace name 'Htlc' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 77 in src/NLightning.Bolts/BOLT3/Factories/CommitmentTransactionFactory.cs

View workflow job for this annotation

GitHub Actions / build-pr

The type or namespace name 'Htlc' could not be found (are you missing a using directive or an assembly reference?)
{
// Implement logic to calculate which HTLCs need to be trimmed
// based on dust limit and fee rate.
return htlcs; // Placeholder for actual implementation
}

private Money CalculateBaseFee(FeeRate feeRatePerKw, int numHtlcs)
{
// Implement logic to calculate the base fee for the transaction.
return Money.Satoshis(1000); // Placeholder for actual implementation
}

private void AdjustAmountsForFees(ref Money toLocalAmount, ref Money toRemoteAmount, Money baseFee, bool optionAnchors)
{
var totalFee = baseFee;
if (optionAnchors)
{
totalFee += Money.Satoshis(660); // Two anchors of 330 sat each
}

if (toLocalAmount >= toRemoteAmount)
{
toLocalAmount -= totalFee;
}
else
{
toRemoteAmount -= totalFee;
}
}

private void AddHtlcOutputs(Transaction tx, List<Htlc> htlcs, PubKey localPubKey, PubKey remotePubKey)

Check failure on line 108 in src/NLightning.Bolts/BOLT3/Factories/CommitmentTransactionFactory.cs

View workflow job for this annotation

GitHub Actions / build-pr

The type or namespace name 'Htlc' could not be found (are you missing a using directive or an assembly reference?)
{
// Implement logic to add HTLC outputs to the transaction
// based on whether they are offered or received.
}

private void AddToLocalOutput(Transaction tx, Money toLocalAmount, PubKey localDelayedPubKey, PubKey revocationPubKey, uint toSelfDelay)
{
var toLocalScript = CreateToLocalScript(localDelayedPubKey, revocationPubKey, toSelfDelay);
tx.Outputs.Add(new TxOut(toLocalAmount, toLocalScript.WitHash));
}

private void AddToRemoteOutput(Transaction tx, Money toRemoteAmount, PubKey remotePubKey, bool optionAnchors)
{
Script toRemoteScript;
if (optionAnchors)
{
toRemoteScript = new Script(
Op.GetPushOp(remotePubKey.ToBytes()),
OpcodeType.OP_CHECKSIG,
OpcodeType.OP_1,
OpcodeType.OP_CHECKSEQUENCEVERIFY
);
}
else
{
toRemoteScript = remotePubKey.Hash.ScriptPubKey;
}
tx.Outputs.Add(new TxOut(toRemoteAmount, toRemoteScript.WitHash));
}

private void AddAnchorOutputs(Transaction tx, Money toLocalAmount, Money toRemoteAmount, int numHtlcs, PubKey localPubKey, PubKey remotePubKey)
{
var anchorAmount = Money.Satoshis(330);

if (toLocalAmount >= Money.Zero || numHtlcs > 0)
{
tx.Outputs.Add(new TxOut(anchorAmount, localPubKey.ScriptPubKey));
}

if (toRemoteAmount >= Money.Zero || numHtlcs > 0)
{
tx.Outputs.Add(new TxOut(anchorAmount, remotePubKey.ScriptPubKey));
}
}

private void SortOutputs(Transaction tx)
{
tx.Outputs = new TxOutList(tx.Outputs.OrderBy(o => o.Value)
.ThenBy(o => o.ScriptPubKey.ToHex())
.ThenBy(o => o.ScriptPubKey.IsUnspendable ? 0 : 1)
.ToList());
}

private Script CreateToLocalScript(PubKey localDelayedPubKey, PubKey revocationPubKey, uint toSelfDelay)
{
return new Script(
OpcodeType.OP_IF,
Op.GetPushOp(revocationPubKey.ToBytes()),
OpcodeType.OP_ELSE,
Op.GetPushOp(toSelfDelay),
OpcodeType.OP_CHECKSEQUENCEVERIFY,
OpcodeType.OP_DROP,
Op.GetPushOp(localDelayedPubKey.ToBytes()),
OpcodeType.OP_ENDIF,
OpcodeType.OP_CHECKSIG
);
}
}
75 changes: 75 additions & 0 deletions src/NLightning.Bolts/BOLT3/Hashes/RIPEMD160.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace NLightning.Bolts.BOLT3.Hashes;

using BOLT8;
using Bolts.Constants;

/// <summary>
/// RIPEMD-160 hash function implementation based on the algorithm described in RFC 2925 (https://tools.ietf.org/html/rfc2925).
/// </summary>
internal sealed class RIPEMD160 : IDisposable
{
private readonly IntPtr _state = Marshal.AllocHGlobal(80); // RIPEMD-160 uses 80 bytes for its internal state
private bool _disposed;

internal RIPEMD160() => Reset();

/// <summary>
/// Appends the specified data to the data already processed in the hash.
/// </summary>
internal void AppendData(ReadOnlySpan<byte> data)
{
if (!data.IsEmpty)
{
// Assuming Libsodium has a function similar to crypto_hash_sha256_update for RIPEMD-160, adjust the method call accordingly
_ = Libsodium.crypto_hash_ripemd160_update(
_state,
ref MemoryMarshal.GetReference(data),
(ulong)data.Length
);
}
}

/// <summary>
/// Retrieves the hash for the accumulated data into the hash parameter,
/// and resets the object to its initial state.
/// </summary>
internal void GetHashAndReset(Span<byte> hash)
{
Debug.Assert(hash.Length == HashConstants.RIPEMD160_HASH_LEN);

// Assuming Libsodium has a function similar to crypto_hash_sha256_final for RIPEMD-160, adjust the method call accordingly
_ = Libsodium.crypto_hash_ripemd160_final(
_state,
ref MemoryMarshal.GetReference(hash)
);

Reset();
}

private void Reset()
{
// Assuming Libsodium has an initialization function for RIPEMD-160
_ = Libsodium.crypto_hash_ripemd160_init(_state);
}

#region Dispose Pattern
public void Dispose()
{
if (!_disposed)
{
Marshal.FreeHGlobal(_state);
_disposed = true;
}

GC.SuppressFinalize(this);
}

~RIPEMD160()
{
Dispose();
}
#endregion
}
84 changes: 84 additions & 0 deletions src/NLightning.Bolts/BOLT3/Htlcs/OfferedHtlcOutput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using NBitcoin;

namespace NLightning.Bolts.BOLT3.Htlcs;

using BOLT3.Hashes;
using BOLT8.Hashes;

Check failure on line 6 in src/NLightning.Bolts/BOLT3/Htlcs/OfferedHtlcOutput.cs

View workflow job for this annotation

GitHub Actions / build-pr

The type or namespace name 'Hashes' does not exist in the namespace 'NLightning.Bolts.BOLT8' (are you missing an assembly reference?)

Check failure on line 6 in src/NLightning.Bolts/BOLT3/Htlcs/OfferedHtlcOutput.cs

View workflow job for this annotation

GitHub Actions / build-pr

The type or namespace name 'Hashes' does not exist in the namespace 'NLightning.Bolts.BOLT8' (are you missing an assembly reference?)
using Bolts.Constants;
using Exceptions;

public class OfferedHtlcOutputs
{
public Script ScriptPubKey { get; }
public Money Amount { get; }

public OfferedHtlcOutputs(PubKey revocationPubKey, PubKey remoteHtlcPubKey, PubKey localHtlcPubKey, uint256 paymentHash, Money amount, bool optionAnchors)
{
using var sha256 = new SHA256();
using var ripemd160 = new RIPEMD160();

// Hash the revocationPubKey
sha256.AppendData(revocationPubKey.ToBytes());
var revocationPubKeyHash = new byte[HashConstants.SHA256_HASH_LEN];
sha256.GetHashAndReset(revocationPubKeyHash);
ripemd160.AppendData(revocationPubKeyHash);
ripemd160.GetHashAndReset(revocationPubKeyHash);

// Hash the paymentHash
var paymentHashRipemd160 = new byte[HashConstants.RIPEMD160_HASH_LEN];
ripemd160.AppendData(paymentHash.ToBytes());
ripemd160.GetHashAndReset(paymentHashRipemd160);

List<Op> ops = [
OpcodeType.OP_DUP,
OpcodeType.OP_HASH160,
Op.GetPushOp(revocationPubKeyHash[..HashConstants.RIPEMD160_HASH_LEN]), // Get only the first 20 bytes
OpcodeType.OP_EQUAL,
OpcodeType.OP_IF,
OpcodeType.OP_CHECKSIG,
OpcodeType.OP_ELSE,
Op.GetPushOp(remoteHtlcPubKey.ToBytes()),
OpcodeType.OP_SWAP,
OpcodeType.OP_SIZE,
Op.GetPushOp(32),
OpcodeType.OP_EQUAL,
OpcodeType.OP_NOTIF,
OpcodeType.OP_DROP,
OpcodeType.OP_2,
OpcodeType.OP_SWAP,
Op.GetPushOp(localHtlcPubKey.ToBytes()),
OpcodeType.OP_2,
OpcodeType.OP_CHECKMULTISIG,
OpcodeType.OP_ELSE,
OpcodeType.OP_HASH160,
Op.GetPushOp(paymentHashRipemd160),
OpcodeType.OP_EQUALVERIFY,
OpcodeType.OP_CHECKSIG,
OpcodeType.OP_ENDIF
];

Script script;
if (optionAnchors)
{
ops.AddRange([
OpcodeType.OP_1,
OpcodeType.OP_CHECKSEQUENCEVERIFY,
OpcodeType.OP_DROP
]);
}

// Close last IF
ops.Add(OpcodeType.OP_ENDIF);

script = new Script(ops);

// Check if script is correct
if (script.IsUnspendable || !script.IsValid)
{
throw new InvalidScriptException("Script is either 'invalid' or 'unspendable'.");
}

ScriptPubKey = PayToWitScriptHashTemplate.Instance.GenerateScriptPubKey(script.WitHash);
Amount = amount;
}
}
Loading
Loading