Skip to content

Commit 81129d6

Browse files
accounts: add randomized accounts migration tests
1 parent c732979 commit 81129d6

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

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)