Skip to content
Open
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
56 changes: 40 additions & 16 deletions cmd/commands/invoicesrpc_active.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,18 @@ var addHoldInvoiceCommand = cli.Command{
Category: "Invoices",
Usage: "Add a new hold invoice.",
Description: `
Add a new invoice, expressing intent for a future payment.
Add a new hold invoice, expressing intent for a future payment.

Invoices without an amount can be created by not supplying any
parameters or providing an amount of 0. These invoices allow the payer
to specify the amount of satoshis they wish to send.`,
ArgsUsage: "hash [amt]",
to specify the amount of satoshis they wish to send.

The hash can be provided as the first positional argument (legacy) or
via the --hash flag. If no hash is provided, the server will
auto-generate a random preimage and derive the hash, returning the
generated preimage in the response. The caller must save that
preimage to settle the invoice later.`,
ArgsUsage: "[hash] [amt]",
Flags: []cli.Flag{
cli.StringFlag{
Name: "memo",
Expand Down Expand Up @@ -198,13 +204,24 @@ var addHoldInvoiceCommand = cli.Command{
"private channels in order to assist the " +
"payer in reaching you",
},
cli.StringFlag{
Name: "hash",
Usage: "the hash of the preimage (32 bytes, hex " +
"encoded). If not set, the server will " +
"auto-generate a random preimage and " +
"hash, returning the preimage in the " +
"response.",
},
},
Action: actionDecorator(addHoldInvoice),
}

func addHoldInvoice(ctx *cli.Context) error {
var (
descHash []byte
hash []byte
amt int64
amtMsat int64
err error
)

Expand All @@ -213,26 +230,33 @@ func addHoldInvoice(ctx *cli.Context) error {
defer cleanUp()

args := ctx.Args()
if ctx.NArg() == 0 {
cli.ShowCommandHelp(ctx, "addholdinvoice")
return nil
}

hash, err := hex.DecodeString(args.First())
if err != nil {
return fmt.Errorf("unable to parse hash: %w", err)
}
switch {
// --hash flag takes priority.
case ctx.IsSet("hash"):
hash, err = hex.DecodeString(ctx.String("hash"))
if err != nil {
return fmt.Errorf("unable to parse hash: %w", err)
}

args = args.Tail()
// If the first positional arg looks like a hex-encoded hash
// (64 hex chars = 32 bytes), use it for backward compatibility.
case args.Present() && len(args.First()) == 64:
hash, err = hex.DecodeString(args.First())
if err != nil {
return fmt.Errorf("unable to parse hash: %w", err)
}
args = args.Tail()
}

amt := ctx.Int64("amt")
amtMsat := ctx.Int64("amt_msat")
amt = ctx.Int64("amt")
amtMsat = ctx.Int64("amt_msat")

if !ctx.IsSet("amt") && !ctx.IsSet("amt_msat") && args.Present() {
amt, err = strconv.ParseInt(args.First(), 10, 64)
if err != nil {
return fmt.Errorf("unable to decode amt argument: %w",
err)
return fmt.Errorf("unable to decode amt argument: "+
"%w", err)
}
}

Expand Down
75 changes: 75 additions & 0 deletions docs/release-notes/release-notes-0.22.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Release Notes
- [Bug Fixes](#bug-fixes)
- [New Features](#new-features)
- [Functional Enhancements](#functional-enhancements)
- [RPC Additions](#rpc-additions)
- [lncli Additions](#lncli-additions)
- [Improvements](#improvements)
- [Functional Updates](#functional-updates)
- [RPC Updates](#rpc-updates)
- [lncli Updates](#lncli-updates)
- [Breaking Changes](#breaking-changes)
- [Performance Improvements](#performance-improvements)
- [Deprecations](#deprecations)
- [Technical and Architectural Updates](#technical-and-architectural-updates)
- [BOLT Spec Updates](#bolt-spec-updates)
- [Testing](#testing)
- [Database](#database)
- [Code Health](#code-health)
- [Tooling and Documentation](#tooling-and-documentation)

# Bug Fixes

# New Features

## Functional Enhancements

## RPC Additions

## lncli Additions

# Improvements

## Functional Updates

## RPC Updates

* [`AddHoldInvoice` hash field is now
optional](https://github.com/lightningnetwork/lnd/pull/10685). When omitted,
the server generates a random preimage, derives the payment hash, and
returns the preimage in the response. The preimage is never persisted — the
caller must save the returned value to settle the invoice later, preserving
the hold-invoice invariant that lnd only learns the preimage at
`SettleInvoice` time. The response adds `payment_preimage` (populated only
for the auto-generated case) and `payment_hash` (always populated).

## lncli Updates

* The [`addholdinvoice` command now accepts `--hash` and `--preimage`
flags](https://github.com/lightningnetwork/lnd/pull/10685). When neither is
provided, the server generates both automatically. The legacy positional hash
argument is still supported for backward compatibility.

## Code Health

## Breaking Changes

## Performance Improvements

## Deprecations

# Technical and Architectural Updates

## BOLT Spec Updates

## Testing

## Database

## Code Health

## Tooling and Documentation

# Contributors (Alphabetical Order)

* Suheb
4 changes: 4 additions & 0 deletions itest/list_on_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@ var allTestCases = []*lntest.TestCase{
Name: "hold invoice sender persistence",
TestFunc: testHoldInvoicePersistence,
},
{
Name: "hold invoice auto generate",
TestFunc: testHoldInvoiceAutoGenerate,
},
{
Name: "maximum channel size",
TestFunc: testMaxChannelSize,
Expand Down
104 changes: 104 additions & 0 deletions itest/lnd_hold_invoice_auto_generate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package itest

import (
"github.com/btcsuite/btcd/btcutil"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/stretchr/testify/require"
)

// testHoldInvoiceAutoGenerate tests that hold invoices can be created without
// providing a hash. The server auto-generates a preimage, derives the hash,
// and returns the preimage in the response without persisting it. It also
// verifies the hash-only flow still works unchanged.
func testHoldInvoiceAutoGenerate(ht *lntest.HarnessTest) {
// Open a channel between Alice and Bob.
_, nodes := ht.CreateSimpleNetwork(
[][]string{nil, nil}, lntest.OpenChannelParams{
Amt: btcutil.Amount(1_000_000),
},
)
alice, bob := nodes[0], nodes[1]

// Test 1: Auto-generate preimage and hash (no hash provided).
autoReq := &invoicesrpc.AddHoldInvoiceRequest{
Memo: "auto-generated",
Value: 10_000,
}
autoResp := bob.RPC.AddHoldInvoice(autoReq)

// The response must contain both a preimage and hash.
require.Len(ht, autoResp.PaymentPreimage, 32,
"expected 32-byte preimage")
require.Len(ht, autoResp.PaymentHash, 32,
"expected 32-byte payment hash")

// Verify the hash matches SHA256 of the preimage.
var autoPreimage lntypes.Preimage
copy(autoPreimage[:], autoResp.PaymentPreimage)
autoHash := autoPreimage.Hash()
require.Equal(ht, autoHash[:], autoResp.PaymentHash,
"hash should be SHA256 of preimage")

// Verify the generated preimage is not persisted: a LookupInvoice
// before settlement must report an empty r_preimage. The server
// only learns the preimage again when the caller passes it to
// SettleInvoice.
lookup := bob.RPC.LookupInvoice(autoResp.PaymentHash)
require.Empty(ht, lookup.RPreimage,
"auto-generated preimage must not be stored in the DB")

// Subscribe, pay, and settle.
autoStream := bob.RPC.SubscribeSingleInvoice(autoResp.PaymentHash)

ht.SendPaymentAndAssertStatus(alice, &routerrpc.SendPaymentRequest{
PaymentRequest: autoResp.PaymentRequest,
FeeLimitSat: 1_000_000,
}, lnrpc.Payment_IN_FLIGHT)

ht.AssertInvoiceState(autoStream, lnrpc.Invoice_ACCEPTED)

bob.RPC.SettleInvoice(autoResp.PaymentPreimage)
ht.AssertInvoiceState(autoStream, lnrpc.Invoice_SETTLED)
ht.AssertPaymentStatus(
alice, autoHash, lnrpc.Payment_SUCCEEDED,
)

// Test 2: Traditional hash-only flow still works. The response
// preimage must be empty because the server does not know it.
var hashPreimage lntypes.Preimage
copy(hashPreimage[:], ht.Random32Bytes())
payHash := hashPreimage.Hash()

hashResp := bob.RPC.AddHoldInvoice(
&invoicesrpc.AddHoldInvoiceRequest{
Memo: "hash-only",
Value: 10_000,
Hash: payHash[:],
},
)

require.Empty(ht, hashResp.PaymentPreimage,
"preimage should be empty for hash-only")
require.Equal(ht, payHash[:], hashResp.PaymentHash,
"returned hash should match provided hash")

// Subscribe, pay, and settle.
hashStream := bob.RPC.SubscribeSingleInvoice(payHash[:])

ht.SendPaymentAndAssertStatus(alice, &routerrpc.SendPaymentRequest{
PaymentRequest: hashResp.PaymentRequest,
FeeLimitSat: 1_000_000,
}, lnrpc.Payment_IN_FLIGHT)

ht.AssertInvoiceState(hashStream, lnrpc.Invoice_ACCEPTED)

bob.RPC.SettleInvoice(hashPreimage[:])
ht.AssertInvoiceState(hashStream, lnrpc.Invoice_SETTLED)
ht.AssertPaymentStatus(
alice, payHash, lnrpc.Payment_SUCCEEDED,
)
}
37 changes: 33 additions & 4 deletions lnrpc/invoicesrpc/invoices.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 22 additions & 1 deletion lnrpc/invoicesrpc/invoices.proto
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,13 @@ message AddHoldInvoiceRequest {
*/
string memo = 1;

// The hash of the preimage
/*
The hash of the preimage. Optional: when omitted, the server generates
a random preimage locally, derives the hash, and returns the preimage
in the response (see payment_preimage). The generated preimage is not
persisted by the server, so the caller must save the returned value to
settle the invoice later.
*/
bytes hash = 2;

/*
Expand Down Expand Up @@ -153,6 +159,21 @@ message AddHoldInvoiceResp {
security.
*/
bytes payment_addr = 3;

/*
The preimage for this invoice. Populated only when the server
auto-generates the preimage (i.e. the request was made without a
hash). The server does not persist this preimage — callers must
save it and supply it to SettleInvoice later. Empty when the
caller provided a hash, since the server does not know the
preimage in that case.
*/
bytes payment_preimage = 4;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we return this? We don't do so for normal (non-hold) invoices. If we do want to return this, I think it should be based on a macaroon scope.


/*
The payment hash for this invoice. Always populated.
*/
bytes payment_hash = 5;
}

message SettleInvoiceMsg {
Expand Down
Loading
Loading