package lightning import ( "context" "crypto/rand" "encoding/hex" "fmt" "sync" "time" ) // FakeNode is a Lightning node that can be told to misbehave. // // Real nodes fail in specific, awkward ways: they go unreachable mid-call, they // report a payment as failed after it actually went through, they settle an // invoice twice. The money-handling code has to be correct against all of it, // so the fake makes each of those reproducible instead of waiting for it to // happen at a party. type FakeNode struct { mu sync.Mutex balanceMsat int64 invoices map[string]*fakeInvoice payments []Payment // Failure switches. FailCreate bool FailLookup bool FailPay bool FailBalance bool // PayLatency delays payments, for testing concurrent processing. PayLatency time.Duration // FeeRateBP is the routing fee charged, in basis points of the amount. // Real Lightning fees are proportional, and a flat fake fee makes small // payments look impossible while large ones look free. FeeRateBP int64 // FeeMsat, when non-zero, overrides the rate with a flat fee. Used to // test the fee cap. FeeMsat int64 } // maxRateBP mirrors Limits.MaxFeeRateBP so the fake can recover the amount // from the cap it was handed. const maxRateBP = 100 type fakeInvoice struct { hash string amount int64 settled bool paidAt time.Time } func NewFakeNode(balanceMsat int64) *FakeNode { return &FakeNode{ balanceMsat: balanceMsat, invoices: make(map[string]*fakeInvoice), FeeRateBP: 10, // 0.1%, a realistic routing fee } } func (f *FakeNode) CreateInvoice(ctx context.Context, amountMsat int64, memo string) (Invoice, error) { f.mu.Lock() defer f.mu.Unlock() if f.FailCreate { return Invoice{}, fmt.Errorf("fake: node refused to create an invoice") } var raw [16]byte if _, err := rand.Read(raw[:]); err != nil { return Invoice{}, err } hash := hex.EncodeToString(raw[:]) f.invoices[hash] = &fakeInvoice{hash: hash, amount: amountMsat} return Invoice{ PaymentHash: hash, Bolt11: "lnbcrt" + hash, AmountMsat: amountMsat, ExpiresAt: time.Now().Add(time.Hour), }, nil } func (f *FakeNode) LookupInvoice(ctx context.Context, paymentHash string) (bool, int64, error) { f.mu.Lock() defer f.mu.Unlock() if f.FailLookup { return false, 0, fmt.Errorf("fake: node unreachable") } inv, ok := f.invoices[paymentHash] if !ok { return false, 0, fmt.Errorf("fake: unknown invoice") } return inv.settled, inv.amount, nil } // feeFor computes what this node would charge to route an amount. func (f *FakeNode) feeFor(amountMsat int64) int64 { if f.FeeMsat > 0 { return f.FeeMsat } return amountMsat * f.FeeRateBP / 10000 } func (f *FakeNode) PayInvoice(ctx context.Context, bolt11 string, maxFeeMsat int64) (Payment, error) { if f.PayLatency > 0 { select { case <-time.After(f.PayLatency): case <-ctx.Done(): return Payment{}, ctx.Err() } } f.mu.Lock() defer f.mu.Unlock() if f.FailPay { return Payment{}, fmt.Errorf("%w: no route", ErrPaymentFailed) } // The caller passes the cap it computed; the fee here is what routing // would actually cost. A real node refuses when the cap is too tight. fee := f.feeFor(maxFeeMsat * 10000 / maxRateBP) if f.FeeMsat > 0 { fee = f.FeeMsat } if fee > maxFeeMsat { return Payment{}, fmt.Errorf("%w: fee %d exceeds cap %d", ErrPaymentFailed, fee, maxFeeMsat) } var raw [16]byte if _, err := rand.Read(raw[:]); err != nil { return Payment{}, err } p := Payment{ PaymentHash: hex.EncodeToString(raw[:]), Preimage: hex.EncodeToString(raw[:]), FeeMsat: fee, } f.payments = append(f.payments, p) return p, nil } func (f *FakeNode) Balance(ctx context.Context) (int64, error) { f.mu.Lock() defer f.mu.Unlock() if f.FailBalance { return 0, fmt.Errorf("fake: node unreachable") } return f.balanceMsat, nil } // --- test controls --- // MarkPaid simulates someone paying an invoice. func (f *FakeNode) MarkPaid(paymentHash string) { f.mu.Lock() defer f.mu.Unlock() if inv, ok := f.invoices[paymentHash]; ok { inv.settled = true inv.paidAt = time.Now() f.balanceMsat += inv.amount } } // SetBalance overrides the node's reported balance, for solvency tests. func (f *FakeNode) SetBalance(msat int64) { f.mu.Lock() defer f.mu.Unlock() f.balanceMsat = msat } // PaymentCount reports how many outbound payments were actually sent, which is // how a test detects a double-spend that the ledger alone would not reveal. func (f *FakeNode) PaymentCount() int { f.mu.Lock() defer f.mu.Unlock() return len(f.payments) }