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

invoices: migrate KV invoices to native SQL for users of KV SQL backends #8831

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
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
93 changes: 61 additions & 32 deletions config_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"github.com/lightningnetwork/lnd/rpcperms"
"github.com/lightningnetwork/lnd/signal"
"github.com/lightningnetwork/lnd/sqldb"
"github.com/lightningnetwork/lnd/sqldb/sqlc"
"github.com/lightningnetwork/lnd/sweep"
"github.com/lightningnetwork/lnd/walletunlocker"
"github.com/lightningnetwork/lnd/watchtower"
Expand All @@ -60,6 +61,16 @@
"gopkg.in/macaroon-bakery.v2/bakery"
)

const (
// invoiceMigrationBatchSize is the number of invoices that will be
// migrated in a single batch.
invoiceMigrationBatchSize = 1000

// invoiceMigration is the version of the migration that will be used to
// migrate invoices from the kvdb to the sql database.
invoiceMigration = 6
)

// GrpcRegistrar is an interface that must be satisfied by an external subserver
// that wants to be able to register its own gRPC server onto lnd's main
// grpc.Server instance.
Expand Down Expand Up @@ -932,10 +943,10 @@
// the btcwallet's loader.
WalletDB btcwallet.LoaderOption

// NativeSQLStore is a pointer to a native SQL store that can be used
// for native SQL queries for tables that already support it. This may
// be nil if the use-native-sql flag was not set.
NativeSQLStore *sqldb.BaseDB
// NativeSQLStore holds a reference to the native SQL store that can
// be used for native SQL queries for tables that already support it.
// This may be nil if the use-native-sql flag was not set.
NativeSQLStore sqldb.DB
}

// DefaultDatabaseBuilder is a type that builds the default database backends
Expand Down Expand Up @@ -1038,7 +1049,7 @@
if err != nil {
cleanUp()

err := fmt.Errorf("unable to open graph DB: %w", err)
err = fmt.Errorf("unable to open graph DB: %w", err)
d.logger.Error(err)

return nil, nil, err
Expand Down Expand Up @@ -1072,51 +1083,69 @@
case err != nil:
cleanUp()

err := fmt.Errorf("unable to open graph DB: %w", err)
err = fmt.Errorf("unable to open graph DB: %w", err)
d.logger.Error(err)
return nil, nil, err
}

// Instantiate a native SQL invoice store if the flag is set.
// Instantiate a native SQL store if the flag is set.
if d.cfg.DB.UseNativeSQL {
// KV invoice db resides in the same database as the channel
// state DB. Let's query the database to see if we have any
// invoices there. If we do, we won't allow the user to start
// lnd with native SQL enabled, as we don't currently migrate
// the invoices to the new database schema.
Copy link
Collaborator

Choose a reason for hiding this comment

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

dont we still want this check for anyone who has set d.cfg.DB.SkipSQLInvoiceMigration=true?

Copy link
Collaborator

Choose a reason for hiding this comment

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

and to protect against users who had a bbolt invoice store before?

invoiceSlice, err := dbs.ChanStateDB.QueryInvoices(
ctx, invoices.InvoiceQuery{
NumMaxInvoices: 1,
},
)
if err != nil {
cleanUp()
d.logger.Errorf("Unable to query KV invoice DB: %v",
err)
migrations := sqldb.GetMigrations()

// If the user has not explicitly disabled the SQL invoice
// migration, attach the custom migration function to invoice
// migration (version 6). Even if this custom migration is
// disabled, the regular native SQL store migrations will still
// run. If the database version is already above this custom
// migration's version (6), it will be skipped permanently,
// regardless of the flag.
if !d.cfg.DB.SkipSQLInvoiceMigration {
migrationFn := func(tx *sqlc.Queries) error {
return invoices.MigrateInvoicesToSQL(
ctx, dbs.ChanStateDB.Backend,
dbs.ChanStateDB, tx,
invoiceMigrationBatchSize,
)
}

return nil, nil, err
// Make sure we attach the custom migration function to
// the correct migration version.
for i := 0; i < len(migrations); i++ {
if migrations[i].Version != invoiceMigration {
continue
}

migrations[i].MigrationFn = migrationFn
}
}

if len(invoiceSlice.Invoices) > 0 {
// We need to apply all migrations to the native SQL store
// before we can use it.
err = dbs.NativeSQLStore.ApplyAllMigrations(migrations)
if err != nil {
cleanUp()
err := fmt.Errorf("found invoices in the KV invoice " +
"DB, migration to native SQL is not yet " +
"supported")
err = fmt.Errorf("faild to run migrations for the "+
"native SQL store: %w", err)
d.logger.Error(err)

return nil, nil, err
}

Check failure on line 1132 in config_builder.go

View workflow job for this annotation

GitHub Actions / check commits

cannot use executor (variable of type *sqldb.TransactionExecutor[invoices.SQLInvoiceQueries]) as invoices.BatchedSQLInvoiceQueries value in argument to invoices.NewSQLStore: *sqldb.TransactionExecutor[invoices.SQLInvoiceQueries] does not implement invoices.BatchedSQLInvoiceQueries (missing method InsertAMPSubInvoice)

// With the DB ready and migrations applied, we can now create
// the base DB and transaction executor for the native SQL
// invoice store.
baseDB := dbs.NativeSQLStore.GetBaseDB()
executor := sqldb.NewTransactionExecutor(
dbs.NativeSQLStore,
func(tx *sql.Tx) invoices.SQLInvoiceQueries {
return dbs.NativeSQLStore.WithTx(tx)
baseDB, func(tx *sql.Tx) invoices.SQLInvoiceQueries {
return baseDB.WithTx(tx)
},
)

dbs.InvoiceDB = invoices.NewSQLStore(
sqlInvoiceDB := invoices.NewSQLStore(
executor, clock.NewDefaultClock(),
)

dbs.InvoiceDB = sqlInvoiceDB
} else {
dbs.InvoiceDB = dbs.ChanStateDB
}
Expand All @@ -1129,7 +1158,7 @@
if err != nil {
cleanUp()

err := fmt.Errorf("unable to open %s database: %w",
err = fmt.Errorf("unable to open %s database: %w",
lncfg.NSTowerClientDB, err)
d.logger.Error(err)
return nil, nil, err
Expand All @@ -1144,7 +1173,7 @@
if err != nil {
cleanUp()

err := fmt.Errorf("unable to open %s database: %w",
err = fmt.Errorf("unable to open %s database: %w",
lncfg.NSTowerServerDB, err)
d.logger.Error(err)
return nil, nil, err
Expand Down
5 changes: 5 additions & 0 deletions docs/release-notes/release-notes-0.19.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,10 @@ The underlying functionality between those two options remain the same.
store](https://github.com/lightningnetwork/lnd/pull/9001) so that results are
namespaced. All existing results are written to the "default" namespace.

* [Migrate KV invoices to
SQL](https://github.com/lightningnetwork/lnd/pull/8831) as part of a larger
effort to support SQL databases natively in LND.

## Code Health

* A code refactor that [moves all the graph related DB code out of the
Expand All @@ -231,6 +235,7 @@ The underlying functionality between those two options remain the same.
# Contributors (Alphabetical Order)

* Abdullahi Yunus
* Andras Banki-Horvath
* Animesh Bilthare
* Boris Nagaev
* Carla Kirk-Cohen
Expand Down
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ require (
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/opencontainers/runc v1.1.12 // indirect
github.com/ory/dockertest/v3 v3.10.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.26.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
Expand Down Expand Up @@ -207,6 +207,10 @@ replace github.com/gogo/protobuf => github.com/gogo/protobuf v1.3.2
// allows us to specify that as an option.
replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display

// Temporary replace until https://github.com/lightningnetwork/lnd/pull/8831 is
// merged.
replace github.com/lightningnetwork/lnd/sqldb => ./sqldb
Comment on lines +210 to +212
Copy link
Collaborator

Choose a reason for hiding this comment

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

Seems like this commit should be dropped.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Note that this is still needed as we modify the sqldb package in this PR.

Copy link
Collaborator

Choose a reason for hiding this comment

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

since this is close to merge - should we not split this up into 2 PRs? 1) that updates sqldb and 2) that points to that one? so that we dont merge with this replacement. or is the plan just to do an immediate follow up removing the replace?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Both works imo, I'm happy to split it up once we get all approves :)


// If you change this please also update docs/INSTALL.md and GO_VERSION in
// Makefile (then run `make lint` to see where else it needs to be updated as
// well).
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -464,8 +464,6 @@ github.com/lightningnetwork/lnd/kvdb v1.4.11 h1:fk1HMVFrsVK3xqU7q+JWHRgBltw/a2qI
github.com/lightningnetwork/lnd/kvdb v1.4.11/go.mod h1:a5cMDKMjbJA8dD06ZqqnYkmSh5DhEbbG8C1YHM3NN+k=
github.com/lightningnetwork/lnd/queue v1.1.1 h1:99ovBlpM9B0FRCGYJo6RSFDlt8/vOkQQZznVb18iNMI=
github.com/lightningnetwork/lnd/queue v1.1.1/go.mod h1:7A6nC1Qrm32FHuhx/mi1cieAiBZo5O6l8IBIoQxvkz4=
github.com/lightningnetwork/lnd/sqldb v1.0.5 h1:ax5vBPf44tN/uD6C5+hBPBjOJ7cRMrUL+sVOdzmLVt4=
github.com/lightningnetwork/lnd/sqldb v1.0.5/go.mod h1:OG09zL/PHPaBJefp4HsPz2YLUJ+zIQHbpgCtLnOx8I4=
github.com/lightningnetwork/lnd/ticker v1.1.1 h1:J/b6N2hibFtC7JLV77ULQp++QLtCwT6ijJlbdiZFbSM=
github.com/lightningnetwork/lnd/ticker v1.1.1/go.mod h1:waPTRAAcwtu7Ji3+3k+u/xH5GHovTsCoSVpho0KDvdA=
github.com/lightningnetwork/lnd/tlv v1.2.6 h1:icvQG2yDr6k3ZuZzfRdG3EJp6pHurcuh3R6dg0gv/Mw=
Expand Down
5 changes: 5 additions & 0 deletions invoices/invoices.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ func (r InvoiceRef) Modifier() RefModifier {
return r.refModifier
}

// IsHashOnly returns true if the invoice ref only contains a payment hash.
func (r InvoiceRef) IsHashOnly() bool {
return r.payHash != nil && r.payAddr == nil && r.setID == nil
}

// String returns a human-readable representation of an InvoiceRef.
func (r InvoiceRef) String() string {
var ids []string
Expand Down
146 changes: 146 additions & 0 deletions invoices/kv_sql_migration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package invoices_test

import (
"context"
"database/sql"
"testing"
"time"

"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/clock"
invpkg "github.com/lightningnetwork/lnd/invoices"
"github.com/lightningnetwork/lnd/sqldb"
"github.com/lightningnetwork/lnd/sqldb/sqlc"
"github.com/stretchr/testify/require"
)

// TestMigrationWithChannelDB tests the migration of invoices from a bolt backed
// channel.db to a SQL database. Note that this test does not attempt to be a
// complete migration test for all invoice types but rather is added as a tool
// for developers and users to debug invoice migration issues with an actual
// channel.db file.
func TestMigrationWithChannelDB(t *testing.T) {
// First create a shared Postgres instance so we don't spawn a new
// docker container for each test.
pgFixture := sqldb.NewTestPgFixture(
t, sqldb.DefaultPostgresFixtureLifetime,
)
t.Cleanup(func() {
pgFixture.TearDown(t)
})

makeSQLDB := func(t *testing.T, sqlite bool) (*invpkg.SQLStore,
*sqldb.TransactionExecutor[*sqlc.Queries]) {

var db *sqldb.BaseDB
if sqlite {
db = sqldb.NewTestSqliteDB(t).BaseDB
} else {
db = sqldb.NewTestPostgresDB(t, pgFixture).BaseDB
}

invoiceExecutor := sqldb.NewTransactionExecutor(
db, func(tx *sql.Tx) invpkg.SQLInvoiceQueries {
return db.WithTx(tx)
},
ellemouton marked this conversation as resolved.
Show resolved Hide resolved
)

genericExecutor := sqldb.NewTransactionExecutor(
db, func(tx *sql.Tx) *sqlc.Queries {
return db.WithTx(tx)
},
)

testClock := clock.NewTestClock(time.Unix(1, 0))

return invpkg.NewSQLStore(invoiceExecutor, testClock),
genericExecutor
}

migrationTest := func(t *testing.T, kvStore *channeldb.DB,
sqlite bool) {

sqlInvoiceStore, sqlStore := makeSQLDB(t, sqlite)
ctxb := context.Background()

const batchSize = 11
ellemouton marked this conversation as resolved.
Show resolved Hide resolved
var opts sqldb.MigrationTxOptions
err := sqlStore.ExecTx(
ctxb, &opts, func(tx *sqlc.Queries) error {
return invpkg.MigrateInvoicesToSQL(
ctxb, kvStore.Backend, kvStore, tx,
batchSize,
)
}, func() {},
)
require.NoError(t, err)

// MigrateInvoices will check if the inserted invoice equals to
// the migrated one, but as a sanity check, we'll also fetch the
// invoices from the store and compare them to the original
// invoices.
query := invpkg.InvoiceQuery{
IndexOffset: 0,
// As a sanity check, fetch more invoices than we have
// to ensure that we did not add any extra invoices.
// Note that we don't really have a way to know the
// exact number of invoices in the bolt db without first
// iterating over all of them, but for test purposes
// constant should be enough.
NumMaxInvoices: 9999,
ziggie1984 marked this conversation as resolved.
Show resolved Hide resolved
}
result1, err := kvStore.QueryInvoices(ctxb, query)
require.NoError(t, err)
numInvoices := len(result1.Invoices)

result2, err := sqlInvoiceStore.QueryInvoices(ctxb, query)
require.NoError(t, err)
require.Equal(t, numInvoices, len(result2.Invoices))

// Simply zero out the add index so we don't fail on that when
// comparing.
for i := 0; i < numInvoices; i++ {
result1.Invoices[i].AddIndex = 0
result2.Invoices[i].AddIndex = 0

// We need to override the timezone of the invoices as
// the provided DB vs the test runners local time zone
// might be different.
invpkg.OverrideInvoiceTimeZone(&result1.Invoices[i])
invpkg.OverrideInvoiceTimeZone(&result2.Invoices[i])

require.Equal(
t, result1.Invoices[i], result2.Invoices[i],
)
}
}

tests := []struct {
name string
dbPath string
}{
{
"empty",
t.TempDir(),
},
{
"testdata",
"testdata",
},
}

for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
store := channeldb.OpenForTesting(t, test.dbPath)

t.Run("Postgres", func(t *testing.T) {
migrationTest(t, store, false)
})

t.Run("SQLite", func(t *testing.T) {
migrationTest(t, store, true)
})
})
}
}
Loading
Loading