Skip to content

Commit b4cffd8

Browse files
accounts: add randomized accounts migration tests
1 parent 85cdb4b commit b4cffd8

File tree

3 files changed

+242
-1
lines changed

3 files changed

+242
-1
lines changed

.gitignore

+3
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/kv_sql_migration_test.go

+238
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"database/sql"
66
"errors"
7+
"fmt"
78
"testing"
89
"time"
910

@@ -12,8 +13,11 @@ import (
1213
"github.com/lightningnetwork/lnd/fn"
1314
"github.com/lightningnetwork/lnd/lnrpc"
1415
"github.com/lightningnetwork/lnd/lntypes"
16+
"github.com/lightningnetwork/lnd/lnwire"
1517
"github.com/lightningnetwork/lnd/sqldb"
1618
"github.com/stretchr/testify/require"
19+
"golang.org/x/exp/rand"
20+
"pgregory.net/rapid"
1721
)
1822

1923
// TestAccountStoreMigration tests the migration of account store from a bolt
@@ -320,6 +324,16 @@ func TestAccountStoreMigration(t *testing.T) {
320324
require.False(t, known)
321325
},
322326
},
327+
{
328+
"randomized accounts",
329+
true,
330+
randomizeAccounts,
331+
},
332+
{
333+
"rapid randomized accounts",
334+
true,
335+
rapidRandomizeAccounts,
336+
},
323337
}
324338

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

go.mod

+1-1
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 (
@@ -223,7 +224,6 @@ require (
223224
modernc.org/mathutil v1.6.0 // indirect
224225
modernc.org/memory v1.8.0 // indirect
225226
nhooyr.io/websocket v1.8.7 // indirect
226-
pgregory.net/rapid v1.2.0 // indirect
227227
sigs.k8s.io/yaml v1.2.0 // indirect
228228
)
229229

0 commit comments

Comments
 (0)