Skip to content

Commit 1e0e88b

Browse files
accounts: add randomized accounts migration tests
1 parent dbb5d73 commit 1e0e88b

File tree

3 files changed

+243
-1
lines changed

3 files changed

+243
-1
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ itest/itest.test
1515
itest/.logs
1616
itest/*.log
1717

18+
# Failed rapid test runs
19+
accounts/testdata/rapid/*
20+
1821
vendor
1922
*.idea
2023
*.run

accounts/sql_migration_test.go

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package accounts
33
import (
44
"context"
55
"database/sql"
6+
"fmt"
67
"testing"
78
"time"
89

@@ -11,8 +12,11 @@ import (
1112
"github.com/lightningnetwork/lnd/fn"
1213
"github.com/lightningnetwork/lnd/lnrpc"
1314
"github.com/lightningnetwork/lnd/lntypes"
15+
"github.com/lightningnetwork/lnd/lnwire"
1416
"github.com/lightningnetwork/lnd/sqldb"
1517
"github.com/stretchr/testify/require"
18+
"golang.org/x/exp/rand"
19+
"pgregory.net/rapid"
1620
)
1721

1822
// TestAccountStoreMigration tests the migration of account store from a bolt
@@ -283,6 +287,16 @@ func TestAccountStoreMigration(t *testing.T) {
283287
require.False(t, known)
284288
},
285289
},
290+
{
291+
name: "randomized accounts",
292+
expectLastIndex: true,
293+
populateDB: randomizeAccounts,
294+
},
295+
{
296+
name: "rapid randomized accounts",
297+
expectLastIndex: true,
298+
populateDB: rapidRandomizeAccounts,
299+
},
286300
}
287301

288302
for _, test := range tests {
@@ -344,3 +358,228 @@ func TestAccountStoreMigration(t *testing.T) {
344358
})
345359
}
346360
}
361+
362+
// randomizeAccounts adds 10 randomized accounts to the kvStore, each with
363+
// 50-1000 invoices and payments. The accounts are randomized in terms of
364+
// balance, expiry, number of invoices and payments, and payment status.
365+
func randomizeAccounts(t *testing.T, kvStore *BoltStore) {
366+
ctx := context.Background()
367+
368+
var (
369+
// numberOfAccounts is set to 10 to add enough accounts to get
370+
// enough variation between number of invoices and payments, but
371+
// kept low enough for the test not take too long to run, as the
372+
// test time increases drastically by the number of accounts we
373+
// migrate.
374+
numberOfAccounts = 10
375+
invoiceCounter uint64 = 0
376+
)
377+
378+
for i := 0; i < numberOfAccounts; i++ {
379+
label := fmt.Sprintf("account%d", i)
380+
381+
// Generate a random balance between 1,000 and 100,000,000.
382+
balance := lnwire.MilliSatoshi(
383+
rand.Int63n(100000000-1000) + 1000,
384+
)
385+
386+
// Generate a random expiry between 10 and 10,000 minutes.
387+
expiry := time.Now().Add(
388+
time.Minute * time.Duration(rand.Intn(10000-10)+10),
389+
)
390+
391+
acct, err := kvStore.NewAccount(ctx, balance, expiry, label)
392+
require.NoError(t, err)
393+
394+
// Add between 50 and 1000 invoices for the account.
395+
numberOfInvoices := rand.Intn(1000-50) + 50
396+
for j := 0; j < numberOfInvoices; j++ {
397+
invoiceCounter++
398+
399+
var rHash lntypes.Hash
400+
_, err := rand.Read(rHash[:])
401+
require.NoError(t, err)
402+
403+
err = kvStore.AddAccountInvoice(ctx, acct.ID, rHash)
404+
require.NoError(t, err)
405+
406+
err = kvStore.StoreLastIndexes(ctx, invoiceCounter, 0)
407+
require.NoError(t, err)
408+
}
409+
410+
// Add between 50 and 1000 payments for the account.
411+
numberOfPayments := rand.Intn(1000-50) + 50
412+
for j := 0; j < numberOfPayments; j++ {
413+
var rHash lntypes.Hash
414+
_, err := rand.Read(rHash[:])
415+
require.NoError(t, err)
416+
417+
// Generate a random payment amount from 1,000 to
418+
// 100,000,000.
419+
amt := lnwire.MilliSatoshi(
420+
rand.Int63n(100000000-1000) + 1000,
421+
)
422+
423+
// Ensure that we get an almost equal amount of
424+
// different payment statuses for the payments.
425+
status := paymentStatus(j)
426+
427+
known, err := kvStore.UpsertAccountPayment(
428+
ctx, acct.ID, rHash, amt, status,
429+
)
430+
require.NoError(t, err)
431+
require.False(t, known)
432+
}
433+
}
434+
}
435+
436+
// rapidRandomizeAccounts is a rapid test that generates randomized
437+
// accounts using rapid, invoices and payments, and inserts them into the
438+
// kvStore. Each account is generated with a random balance, expiry, label,
439+
// and a random number of 20-100 invoices and payments. The invoices and
440+
// payments are also generated with random hashes and amounts.
441+
func rapidRandomizeAccounts(t *testing.T, kvStore *BoltStore) {
442+
invoiceCounter := uint64(0)
443+
444+
ctx := context.Background()
445+
446+
rapid.Check(t, func(t *rapid.T) {
447+
// Generate the randomized account for this check run.
448+
acct := makeAccountGen().Draw(t, "account")
449+
450+
// Then proceed to insert the account with its invoices and
451+
// payments into the db
452+
newAcct, err := kvStore.NewAccount(
453+
ctx, acct.balance, acct.expiry, acct.label,
454+
)
455+
require.NoError(t, err)
456+
457+
for _, invoiceHash := range acct.invoices {
458+
invoiceCounter++
459+
460+
err := kvStore.AddAccountInvoice(
461+
ctx, newAcct.ID, invoiceHash,
462+
)
463+
require.NoError(t, err)
464+
465+
err = kvStore.StoreLastIndexes(ctx, invoiceCounter, 0)
466+
require.NoError(t, err)
467+
}
468+
469+
for _, pmt := range acct.payments {
470+
// Note that as rapid can generate multiple payments
471+
// of the same values, we cannot be sure that the
472+
// payment is unknown.
473+
_, err := kvStore.UpsertAccountPayment(
474+
ctx, newAcct.ID, pmt.hash, pmt.amt, pmt.status,
475+
)
476+
require.NoError(t, err)
477+
}
478+
})
479+
}
480+
481+
// makeAccountGen returns a rapid generator that generates accounts, with
482+
// random labels, balances, expiry times, and between 20-100 randomly generated
483+
// invoices and payments. The invoices and payments are also generated with
484+
// random hashes and amounts.
485+
func makeAccountGen() *rapid.Generator[account] {
486+
return rapid.Custom(func(t *rapid.T) account {
487+
// As the store has a unique constraint for inserting labels,
488+
// we don't use rapid to generate it, and instead use
489+
// sufficiently large random number as the account suffix to
490+
// avoid collisions.
491+
label := fmt.Sprintf("account:%d", rand.Int63())
492+
493+
balance := lnwire.MilliSatoshi(
494+
rapid.Int64Range(1000, 100000000).Draw(
495+
t, fmt.Sprintf("balance_%s", label),
496+
),
497+
)
498+
499+
expiry := time.Now().Add(
500+
time.Duration(
501+
rapid.IntRange(10, 10000).Draw(
502+
t, fmt.Sprintf("expiry_%s", label),
503+
),
504+
) * time.Minute,
505+
)
506+
507+
// Generate the random invoices
508+
numInvoices := rapid.IntRange(20, 100).Draw(
509+
t, fmt.Sprintf("numInvoices_%s", label),
510+
)
511+
invoices := make([]lntypes.Hash, numInvoices)
512+
for i := range invoices {
513+
invoices[i] = randomHash(
514+
t, fmt.Sprintf("invoiceHash_%s_%d", label, i),
515+
)
516+
}
517+
518+
// Generate the random payments
519+
numPayments := rapid.IntRange(20, 100).Draw(
520+
t, fmt.Sprintf("numPayments_%s", label),
521+
)
522+
payments := make([]payment, numPayments)
523+
for i := range payments {
524+
hashName := fmt.Sprintf("paymentHash_%s_%d", label, i)
525+
amtName := fmt.Sprintf("amt_%s_%d", label, i)
526+
527+
payments[i] = payment{
528+
hash: randomHash(t, hashName),
529+
amt: lnwire.MilliSatoshi(
530+
rapid.Int64Range(1000, 100000000).Draw(
531+
t, amtName,
532+
),
533+
),
534+
status: paymentStatus(i),
535+
}
536+
}
537+
538+
return account{
539+
label: label,
540+
balance: balance,
541+
expiry: expiry,
542+
invoices: invoices,
543+
payments: payments,
544+
}
545+
})
546+
}
547+
548+
// randomHash generates a random hash of 32 bytes. It uses rapid to generate
549+
// the random bytes, and then copies them into a lntypes.Hash struct.
550+
func randomHash(t *rapid.T, name string) lntypes.Hash {
551+
hashBytes := rapid.SliceOfN(rapid.Byte(), 32, 32).Draw(t, name)
552+
var hash lntypes.Hash
553+
copy(hash[:], hashBytes)
554+
return hash
555+
}
556+
557+
// paymentStatus returns a payment status based on the given index by taking
558+
// the index modulo 4. This ensures an approximately equal distribution of
559+
// different payment statuses across payments.
560+
func paymentStatus(i int) lnrpc.Payment_PaymentStatus {
561+
switch i % 4 {
562+
case 0:
563+
return lnrpc.Payment_SUCCEEDED
564+
case 1:
565+
return lnrpc.Payment_IN_FLIGHT
566+
case 2:
567+
return lnrpc.Payment_UNKNOWN
568+
default:
569+
return lnrpc.Payment_FAILED
570+
}
571+
}
572+
573+
type account struct {
574+
label string
575+
balance lnwire.MilliSatoshi
576+
expiry time.Time
577+
invoices []lntypes.Hash
578+
payments []payment
579+
}
580+
581+
type payment struct {
582+
hash lntypes.Hash
583+
amt lnwire.MilliSatoshi
584+
status lnrpc.Payment_PaymentStatus
585+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ require (
5454
gopkg.in/macaroon-bakery.v2 v2.1.0
5555
gopkg.in/macaroon.v2 v2.1.0
5656
modernc.org/sqlite v1.34.5
57+
pgregory.net/rapid v1.2.0
5758
)
5859

5960
require (
@@ -222,7 +223,6 @@ require (
222223
modernc.org/mathutil v1.6.0 // indirect
223224
modernc.org/memory v1.8.0 // indirect
224225
nhooyr.io/websocket v1.8.7 // indirect
225-
pgregory.net/rapid v1.2.0 // indirect
226226
sigs.k8s.io/yaml v1.2.0 // indirect
227227
)
228228

0 commit comments

Comments
 (0)