diff --git a/go.mod b/go.mod index 6566285..173a796 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,14 @@ go 1.19 require ( github.com/aws/aws-sdk-go v1.30.20 + github.com/breez/lntest v0.0.3 github.com/btcsuite/btcd v0.23.1 github.com/btcsuite/btcd/btcec/v2 v2.2.1 github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 github.com/caddyserver/certmagic v0.11.2 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 + github.com/docker/docker v20.10.21+incompatible + github.com/docker/go-connections v0.4.0 github.com/golang/protobuf v1.5.2 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/jackc/pgtype v1.8.1 @@ -17,7 +20,23 @@ require ( github.com/lightningnetwork/lnd v0.15.1-beta github.com/niftynei/glightning v0.8.2 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - google.golang.org/grpc v1.38.0 + google.golang.org/grpc v1.50.1 + gotest.tools v2.2.0+incompatible +) + +require ( + github.com/Microsoft/go-winio v0.6.0 // indirect + github.com/docker/distribution v2.8.1+incompatible // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/google/go-cmp v0.5.8 // indirect + github.com/moby/term v0.0.0-20221120202655-abb19827d345 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + golang.org/x/mod v0.6.0 // indirect + golang.org/x/tools v0.2.0 // indirect + gotest.tools/v3 v3.4.0 // indirect ) require ( @@ -88,7 +107,7 @@ require ( github.com/lightningnetwork/lnd/kvdb v1.3.1 // indirect github.com/lightningnetwork/lnd/queue v1.1.0 // indirect github.com/lightningnetwork/lnd/ticker v1.1.0 // indirect - github.com/lightningnetwork/lnd/tlv v1.0.3 // indirect + github.com/lightningnetwork/lnd/tlv v1.0.3 github.com/lightningnetwork/lnd/tor v1.0.1 // indirect github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect @@ -108,8 +127,8 @@ require ( github.com/sirupsen/logrus v1.7.0 // indirect github.com/soheilhy/cmux v0.1.5 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/objx v0.2.0 // indirect - github.com/stretchr/testify v1.7.1 // indirect + github.com/stretchr/objx v0.5.0 // indirect + github.com/stretchr/testify v1.8.1 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect github.com/ulikunitz/xz v0.5.10 // indirect @@ -134,27 +153,27 @@ require ( go.opentelemetry.io/otel/trace v0.20.0 // indirect go.opentelemetry.io/proto/otlp v0.7.0 // indirect go.uber.org/atomic v1.7.0 // indirect - go.uber.org/multierr v1.6.0 // indirect + go.uber.org/multierr v1.8.0 // indirect go.uber.org/zap v1.17.0 // indirect - golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect + golang.org/x/crypto v0.1.0 // indirect golang.org/x/exp v0.0.0-20221114191408-850992195362 - golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect + golang.org/x/net v0.1.0 // indirect golang.org/x/sys v0.1.0 // indirect - golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect - golang.org/x/text v0.3.7 // indirect + golang.org/x/term v0.1.0 // indirect + golang.org/x/text v0.4.0 // indirect golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced // indirect - google.golang.org/protobuf v1.26.0 // indirect + google.golang.org/protobuf v1.27.1 // indirect gopkg.in/errgo.v1 v1.0.1 // indirect gopkg.in/macaroon-bakery.v2 v2.0.1 // indirect gopkg.in/macaroon.v2 v2.0.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/square/go-jose.v2 v2.3.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.2.0 // indirect ) replace github.com/lightningnetwork/lnd v0.15.1-beta => github.com/breez/lnd v0.15.0-beta.rc6.0.20220831104847-00b86a81e57a -replace github.com/niftynei/glightning v0.8.2 => github.com/breez/glightning v0.0.0-20220822151439-7bb360481467 +replace github.com/niftynei/glightning v0.8.2 => github.com/breez/glightning v0.0.0-20221124075140-383be4672b47 diff --git a/itest/intercept_zero_conf_test.go b/itest/intercept_zero_conf_test.go new file mode 100644 index 0000000..2318e56 --- /dev/null +++ b/itest/intercept_zero_conf_test.go @@ -0,0 +1,141 @@ +package itest + +import ( + "log" + "testing" + "time" + + "github.com/breez/lntest" + lspd "github.com/breez/lspd/rpc" + "gotest.tools/assert" +) + +func TestOpenZeroConfChannelOnReceive(t *testing.T) { + harness := lntest.NewTestHarness(t) + defer harness.TearDown() + + timeout := time.Now().Add(time.Minute) + + miner := lntest.NewMiner(harness) + alice := lntest.NewCoreLightningNode(harness, miner, "Alice", timeout) + bob := NewZeroConfNode(harness, miner, "Bob", timeout) + lsp := NewLspdNode(harness, miner, "Lsp", timeout) + + alice.Fund(10000000, timeout) + lsp.lightningNode.Fund(10000000, timeout) + + log.Print("Opening channel between Alice and the lsp") + alice.OpenChannelAndWait(lsp.lightningNode, &lntest.OpenChannelOptions{ + AmountSat: 1000000, + }, timeout) + + log.Printf("Adding bob's invoices") + outerAmountMsat := uint64(2100000) + innerAmountMsat := uint64(2100000) + description := "Please pay me" + innerInvoice, outerInvoice := bob.GenerateInvoices(generateInvoicesRequest{ + innerAmountMsat: innerAmountMsat, + outerAmountMsat: outerAmountMsat, + description: description, + lsp: lsp, + }) + + log.Print("Connecting bob to lspd") + bob.lightningNode.ConnectPeer(lsp.lightningNode) + + // NOTE: We pretend to be paying fees to the lsp, but actually we won't. + log.Printf("Registering payment with lsp") + pretendAmount := outerAmountMsat - 2000000 + lsp.RegisterPayment(&lspd.PaymentInformation{ + PaymentHash: innerInvoice.paymentHash, + PaymentSecret: innerInvoice.paymentSecret, + Destination: bob.lightningNode.NodeId(), + IncomingAmountMsat: int64(outerAmountMsat), + OutgoingAmountMsat: int64(pretendAmount), + }) + + log.Printf("Alice paying") + payResp := alice.Pay(outerInvoice.bolt11, timeout) + bobInvoice := bob.lightningNode.GetInvoice(payResp.PaymentHash) + + assert.DeepEqual(t, payResp.PaymentPreimage, bobInvoice.PaymentPreimage) + assert.Equal(t, outerAmountMsat, bobInvoice.AmountReceivedMsat) +} + +func TestOpenZeroConfSingleHtlc(t *testing.T) { + harness := lntest.NewTestHarness(t) + defer harness.TearDown() + + timeout := time.Now().Add(time.Minute) + + miner := lntest.NewMiner(harness) + alice := lntest.NewCoreLightningNode(harness, miner, "Alice", timeout) + bob := NewZeroConfNode(harness, miner, "Bob", timeout) + lsp := NewLspdNode(harness, miner, "Lsp", timeout) + + alice.Fund(10000000, timeout) + lsp.lightningNode.Fund(10000000, timeout) + + log.Print("Opening channel between Alice and the lsp") + channel := alice.OpenChannelAndWait(lsp.lightningNode, &lntest.OpenChannelOptions{ + AmountSat: 1000000, + }, timeout) + + log.Printf("Adding bob's invoices") + outerAmountMsat := uint64(2100000) + innerAmountMsat := uint64(2100000) + description := "Please pay me" + innerInvoice, outerInvoice := bob.GenerateInvoices(generateInvoicesRequest{ + innerAmountMsat: innerAmountMsat, + outerAmountMsat: outerAmountMsat, + description: description, + lsp: lsp, + }) + + log.Print("Connecting bob to lspd") + bob.lightningNode.ConnectPeer(lsp.lightningNode) + + // NOTE: We pretend to be paying fees to the lsp, but actually we won't. + log.Printf("Registering payment with lsp") + pretendAmount := outerAmountMsat - 2000000 + lsp.RegisterPayment(&lspd.PaymentInformation{ + PaymentHash: innerInvoice.paymentHash, + PaymentSecret: innerInvoice.paymentSecret, + Destination: bob.lightningNode.NodeId(), + IncomingAmountMsat: int64(outerAmountMsat), + OutgoingAmountMsat: int64(pretendAmount), + }) + + log.Printf("Alice paying") + route := constructRoute(lsp.lightningNode, bob.lightningNode, channel.ChannelId, "1x0x0", outerAmountMsat) + alice.StartPayPartViaRoute(outerAmountMsat, outerInvoice.paymentHash, outerInvoice.paymentSecret, 0, route) + payResp := alice.WaitForPaymentPart(outerInvoice.paymentHash, timeout, 0) + bobInvoice := bob.lightningNode.GetInvoice(payResp.PaymentHash) + + assert.DeepEqual(t, payResp.PaymentPreimage, bobInvoice.PaymentPreimage) + assert.Equal(t, outerAmountMsat, bobInvoice.AmountReceivedMsat) +} + +func constructRoute( + lsp *lntest.CoreLightningNode, + bob *lntest.CoreLightningNode, + aliceLspChannel string, + lspBobChannel string, + amountMsat uint64) *lntest.Route { + return &lntest.Route{ + Route: []*lntest.Hop{ + { + Id: lsp.NodeId(), + Channel: aliceLspChannel, + AmountMsat: amountMsat + uint64(lspBaseFeeMsat) + (amountMsat * uint64(lspFeeRatePpm) / 1000000), + Delay: 144 + lspCltvDelta, + }, + { + Id: bob.NodeId(), + Channel: lspBobChannel, + AmountMsat: amountMsat, + Delay: 144, + }, + }, + } +} diff --git a/itest/lspd_node.go b/itest/lspd_node.go new file mode 100644 index 0000000..dc76539 --- /dev/null +++ b/itest/lspd_node.go @@ -0,0 +1,200 @@ +package itest + +import ( + "bufio" + context "context" + "flag" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "time" + + "github.com/breez/lntest" + "github.com/breez/lspd/btceclegacy" + lspd "github.com/breez/lspd/rpc" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/golang/protobuf/proto" + grpc "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +var ( + lspdExecutable = flag.String( + "lspdexec", "", "full path to lpsd plugin binary", + ) + lspdMigrationsDir = flag.String( + "lspdmigrationsdir", "", "full path to lspd sql migrations directory", + ) +) + +var ( + lspBaseFeeMsat uint32 = 1000 + lspFeeRatePpm uint32 = 1 + lspCltvDelta uint16 = 40 +) + +type LspNode struct { + harness *lntest.TestHarness + lightningNode *lntest.CoreLightningNode + rpc lspd.ChannelOpenerClient + rpcPort uint32 + rpcHost string + privateKey btcec.PrivateKey + publicKey btcec.PublicKey + postgresBackend *PostgresContainer + scriptDir string +} + +func NewLspdNode(h *lntest.TestHarness, m *lntest.Miner, name string, timeout time.Time) *LspNode { + scriptDir := h.GetDirectory(fmt.Sprintf("lspd-%s", name)) + migrationsDir, err := GetMigrationsDir() + lntest.CheckError(h.T, err) + + pgLogfile := filepath.Join(scriptDir, "postgres.log") + postgresBackend := StartPostgresContainer(h.T, h.Ctx, pgLogfile) + postgresBackend.RunMigrations(h.T, h.Ctx, migrationsDir) + + lspdBinary, err := GetLspdBinary() + lntest.CheckError(h.T, err) + + lspdPort, err := lntest.GetPort() + lntest.CheckError(h.T, err) + + lspdPrivateKeyBytes, err := GenerateRandomBytes(32) + lntest.CheckError(h.T, err) + + priv, publ := btcec.PrivKeyFromBytes(lspdPrivateKeyBytes) + + host := "localhost" + grpcAddress := fmt.Sprintf("%s:%d", host, lspdPort) + env := []string{ + "NODE_NAME=lsp", + "NODE_PUBKEY=dunno", + "NODE_HOST=host:port", + "RUN_CLN=true", + "TOKEN=hello", + fmt.Sprintf("DATABASE_URL=%s", postgresBackend.ConnectionString()), + fmt.Sprintf("LISTEN_ADDRESS=%s", grpcAddress), + fmt.Sprintf("LSPD_PRIVATE_KEY=%x", lspdPrivateKeyBytes), + } + + scriptFilePath := filepath.Join(scriptDir, "start-lspd.sh") + log.Printf("Creating lspd startup script at %s", scriptFilePath) + scriptFile, err := os.OpenFile(scriptFilePath, os.O_CREATE|os.O_WRONLY, 0755) + lntest.CheckError(h.T, err) + + writer := bufio.NewWriter(scriptFile) + _, err = writer.WriteString("#!/bin/bash\n") + lntest.CheckError(h.T, err) + + for _, str := range env { + _, err = writer.WriteString("export " + str + "\n") + lntest.CheckError(h.T, err) + } + + _, err = writer.WriteString(lspdBinary + "\n") + lntest.CheckError(h.T, err) + + err = writer.Flush() + lntest.CheckError(h.T, err) + scriptFile.Close() + + args := []string{ + fmt.Sprintf("--plugin=%s", scriptFilePath), + fmt.Sprintf("--fee-base=%d", lspBaseFeeMsat), + fmt.Sprintf("--fee-per-satoshi=%d", lspFeeRatePpm), + fmt.Sprintf("--cltv-delta=%d", lspCltvDelta), + } + + lightningNode := lntest.NewCoreLightningNode(h, m, name, timeout, args...) + + conn, err := grpc.Dial( + grpcAddress, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithPerRPCCredentials(&token{token: "hello"}), + ) + lntest.CheckError(h.T, err) + + client := lspd.NewChannelOpenerClient(conn) + + lspNode := &LspNode{ + harness: h, + lightningNode: lightningNode, + rpc: client, + rpcPort: lspdPort, + rpcHost: host, + privateKey: *priv, + publicKey: *publ, + postgresBackend: postgresBackend, + scriptDir: scriptDir, + } + + h.AddStoppable(lspNode) + h.AddCleanable(lspNode) + h.RegisterLogfile(pgLogfile, fmt.Sprintf("%s-postgres", name)) + return lspNode +} + +func (l *LspNode) RegisterPayment(paymentInfo *lspd.PaymentInformation) { + serialized, err := proto.Marshal(paymentInfo) + lntest.CheckError(l.harness.T, err) + + encrypted, err := btceclegacy.Encrypt(&l.publicKey, serialized) + lntest.CheckError(l.harness.T, err) + + log.Printf("Registering payment") + _, err = l.rpc.RegisterPayment( + l.harness.Ctx, + &lspd.RegisterPaymentRequest{ + Blob: encrypted, + }, + ) + lntest.CheckError(l.harness.T, err) +} + +func (l *LspNode) TearDown() error { + // NOTE: The lightningnode will be torn down on its own. + return l.postgresBackend.Shutdown(l.harness.Ctx) +} + +func (l *LspNode) Cleanup() error { + return l.postgresBackend.Cleanup(l.harness.Ctx) +} + +func (l *LspNode) NodeId() []byte { + return l.lightningNode.NodeId() +} + +func GetLspdBinary() (string, error) { + if lspdExecutable != nil { + return *lspdExecutable, nil + } + + return exec.LookPath("lspd") +} + +func GetMigrationsDir() (string, error) { + if lspdMigrationsDir != nil { + return *lspdMigrationsDir, nil + } + + return exec.LookPath("lspdmigrationsdir") +} + +type token struct { + token string +} + +func (t *token) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { + m := make(map[string]string) + m["authorization"] = "Bearer " + t.token + return m, nil +} + +// RequireTransportSecurity indicates whether the credentials requires +// transport security. +func (t *token) RequireTransportSecurity() bool { + return false +} diff --git a/itest/postgres.go b/itest/postgres.go new file mode 100644 index 0000000..91539e0 --- /dev/null +++ b/itest/postgres.go @@ -0,0 +1,192 @@ +package itest + +import ( + "context" + "encoding/binary" + "errors" + "fmt" + "io" + "log" + "os" + "path/filepath" + "sort" + "strconv" + "testing" + "time" + + "github.com/breez/lntest" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/client" + "github.com/docker/go-connections/nat" + "github.com/jackc/pgx/v4/pgxpool" +) + +type PostgresContainer struct { + id string + password string + port uint32 + cli *client.Client +} + +func StartPostgresContainer(t *testing.T, ctx context.Context, logfile string) *PostgresContainer { + cli, err := client.NewClientWithOpts(client.FromEnv) + lntest.CheckError(t, err) + + image := "postgres:15" + _, _, err = cli.ImageInspectWithRaw(ctx, image) + if err != nil { + if !client.IsErrNotFound(err) { + lntest.CheckError(t, err) + } + + pullReader, err := cli.ImagePull(ctx, image, types.ImagePullOptions{}) + lntest.CheckError(t, err) + _, err = io.Copy(io.Discard, pullReader) + pullReader.Close() + lntest.CheckError(t, err) + } + + port, err := lntest.GetPort() + lntest.CheckError(t, err) + createResp, err := cli.ContainerCreate(ctx, &container.Config{ + Image: image, + Cmd: []string{ + "postgres", + "-c", + "log_statement=all", + }, + Env: []string{ + "POSTGRES_DB=postgres", + "POSTGRES_PASSWORD=pgpassword", + "POSTGRES_USER=postgres", + }, + Healthcheck: &container.HealthConfig{ + Test: []string{"CMD-SHELL", "pg_isready -U postgres"}, + Interval: time.Second, + Timeout: time.Second, + Retries: 10, + }, + }, &container.HostConfig{ + PortBindings: nat.PortMap{ + "5432/tcp": []nat.PortBinding{ + {HostPort: strconv.FormatUint(uint64(port), 10)}, + }, + }, + }, + nil, + nil, + "", + ) + lntest.CheckError(t, err) + + err = cli.ContainerStart(ctx, createResp.ID, types.ContainerStartOptions{}) + lntest.CheckError(t, err) + +HealthCheck: + for { + inspect, err := cli.ContainerInspect(ctx, createResp.ID) + lntest.CheckError(t, err) + + status := inspect.State.Health.Status + switch status { + case "unhealthy": + lntest.CheckError(t, errors.New("container unhealthy")) + case "healthy": + break HealthCheck + default: + time.Sleep(500 * time.Millisecond) + } + } + + ct := &PostgresContainer{ + id: createResp.ID, + password: "pgpassword", + port: port, + cli: cli, + } + + go ct.monitorLogs(logfile) + return ct +} + +func (c *PostgresContainer) monitorLogs(logfile string) { + i, err := c.cli.ContainerLogs(context.Background(), c.id, types.ContainerLogsOptions{ + ShowStderr: true, + ShowStdout: true, + Timestamps: false, + Follow: true, + Tail: "40", + }) + if err != nil { + log.Printf("Could not get container logs: %v", err) + return + } + defer i.Close() + + file, err := os.OpenFile(logfile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600) + if err != nil { + log.Printf("Could not create container log file: %v", err) + return + } + defer file.Close() + + hdr := make([]byte, 8) + for { + _, err := i.Read(hdr) + if err != nil { + return + } + count := binary.BigEndian.Uint32(hdr[4:]) + dat := make([]byte, count) + _, err = i.Read(dat) + if err != nil { + return + } + _, err = file.Write(dat) + if err != nil { + return + } + } +} + +func (c *PostgresContainer) ConnectionString() string { + return fmt.Sprintf("postgres://postgres:%s@127.0.0.1:%d/postgres", c.password, c.port) +} + +func (c *PostgresContainer) Shutdown(ctx context.Context) error { + defer c.cli.Close() + timeout := time.Second + err := c.cli.ContainerStop(ctx, c.id, &timeout) + return err +} + +func (c *PostgresContainer) Cleanup(ctx context.Context) error { + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return err + } + defer cli.Close() + return cli.ContainerRemove(ctx, c.id, types.ContainerRemoveOptions{ + Force: true, + }) +} + +func (c *PostgresContainer) RunMigrations(t *testing.T, ctx context.Context, migrationDir string) { + filenames, err := filepath.Glob(filepath.Join(migrationDir, "*.up.sql")) + lntest.CheckError(t, err) + + sort.Strings(filenames) + + pgxPool, err := pgxpool.Connect(context.Background(), c.ConnectionString()) + lntest.CheckError(t, err) + defer pgxPool.Close() + + for _, filename := range filenames { + data, err := os.ReadFile(filename) + lntest.CheckError(t, err) + + _, err = pgxPool.Exec(ctx, string(data)) + lntest.CheckError(t, err) + } +} diff --git a/itest/test_common.go b/itest/test_common.go new file mode 100644 index 0000000..50600c7 --- /dev/null +++ b/itest/test_common.go @@ -0,0 +1,14 @@ +package itest + +import "crypto/rand" + +func GenerateRandomBytes(n int) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + // Note that err == nil only if we read len(b) bytes. + if err != nil { + return nil, err + } + + return b, nil +} diff --git a/itest/zero_conf_node.go b/itest/zero_conf_node.go new file mode 100644 index 0000000..ef7c1db --- /dev/null +++ b/itest/zero_conf_node.go @@ -0,0 +1,175 @@ +package itest + +import ( + "bufio" + "crypto/sha256" + "fmt" + "log" + "os" + "path/filepath" + "time" + + "github.com/breez/lntest" + btcec "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/btcsuite/btcd/chaincfg" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/zpay32" +) + +type ZeroConfNode struct { + name string + harness *lntest.TestHarness + lightningNode *lntest.CoreLightningNode + privKey *secp256k1.PrivateKey + scriptDir string +} + +var pluginContent string = `#!/usr/bin/env python3 +"""Use the openchannel hook to selectively opt-into zeroconf +""" + +from pyln.client import Plugin + +plugin = Plugin() + + +@plugin.hook('openchannel') +def on_openchannel(openchannel, plugin, **kwargs): + plugin.log(repr(openchannel)) + mindepth = int(0) + + plugin.log(f"This peer is in the zeroconf allowlist, setting mindepth={mindepth}") + return {'result': 'continue', 'mindepth': mindepth} + +plugin.run() +` + +var pluginStartupContent string = `python3 -m venv %s > /dev/null 2>&1 +source %s > /dev/null 2>&1 +pip install pyln-client > /dev/null 2>&1 +python %s +` + +func NewZeroConfNode(h *lntest.TestHarness, m *lntest.Miner, name string, timeout time.Time) *ZeroConfNode { + privKey, err := btcec.NewPrivateKey() + lntest.CheckError(h.T, err) + + s := privKey.Serialize() + + scriptDir, err := os.MkdirTemp(h.Dir, name) + lntest.CheckError(h.T, err) + pythonFilePath := filepath.Join(scriptDir, "zero_conf_plugin.py") + pythonFile, err := os.OpenFile(pythonFilePath, os.O_CREATE|os.O_WRONLY, 0755) + lntest.CheckError(h.T, err) + + pythonWriter := bufio.NewWriter(pythonFile) + _, err = pythonWriter.WriteString(pluginContent) + lntest.CheckError(h.T, err) + + err = pythonWriter.Flush() + lntest.CheckError(h.T, err) + pythonFile.Close() + + pluginFilePath := filepath.Join(scriptDir, "start_zero_conf_plugin.sh") + pluginFile, err := os.OpenFile(pluginFilePath, os.O_CREATE|os.O_WRONLY, 0755) + lntest.CheckError(h.T, err) + + pluginWriter := bufio.NewWriter(pluginFile) + venvDir := filepath.Join(scriptDir, "venv") + activatePath := filepath.Join(venvDir, "bin", "activate") + _, err = pluginWriter.WriteString(fmt.Sprintf(pluginStartupContent, venvDir, activatePath, pythonFilePath)) + lntest.CheckError(h.T, err) + + err = pluginWriter.Flush() + lntest.CheckError(h.T, err) + pluginFile.Close() + + node := lntest.NewCoreLightningNode( + h, + m, + name, + timeout, + fmt.Sprintf("--dev-force-privkey=%x", s), + fmt.Sprintf("--plugin=%s", pluginFilePath), + ) + + return &ZeroConfNode{ + name: name, + harness: h, + lightningNode: node, + scriptDir: scriptDir, + privKey: privKey, + } +} + +type generateInvoicesRequest struct { + innerAmountMsat uint64 + outerAmountMsat uint64 + description string + lsp *LspNode +} + +type invoice struct { + bolt11 string + paymentHash []byte + paymentSecret []byte + paymentPreimage []byte + expiresAt uint64 +} + +func (n *ZeroConfNode) GenerateInvoices(req generateInvoicesRequest) (invoice, invoice) { + preimage, err := GenerateRandomBytes(32) + lntest.CheckError(n.harness.T, err) + + lspNodeId, err := btcec.ParsePubKey(req.lsp.lightningNode.NodeId()) + lntest.CheckError(n.harness.T, err) + + log.Printf("Adding bob's invoices") + innerInvoice := n.lightningNode.CreateBolt11Invoice(&lntest.CreateInvoiceOptions{ + AmountMsat: req.innerAmountMsat, + Description: &req.description, + Preimage: &preimage, + }) + outerInvoiceRaw, err := zpay32.Decode(innerInvoice.Bolt11, &chaincfg.RegressionNetParams) + lntest.CheckError(n.harness.T, err) + + milliSat := lnwire.MilliSatoshi(req.outerAmountMsat) + outerInvoiceRaw.MilliSat = &milliSat + fakeChanId := &lnwire.ShortChannelID{BlockHeight: 1, TxIndex: 0, TxPosition: 0} + outerInvoiceRaw.RouteHints = append(outerInvoiceRaw.RouteHints, []zpay32.HopHint{ + { + NodeID: lspNodeId, + ChannelID: fakeChanId.ToUint64(), + FeeBaseMSat: lspBaseFeeMsat, + FeeProportionalMillionths: lspFeeRatePpm, + CLTVExpiryDelta: lspCltvDelta, + }, + }) + + outerInvoice, err := outerInvoiceRaw.Encode(zpay32.MessageSigner{ + SignCompact: func(msg []byte) ([]byte, error) { + hash := sha256.Sum256(msg) + return ecdsa.SignCompact(n.privKey, hash[:], true) + }, + }) + lntest.CheckError(n.harness.T, err) + + inner := invoice{ + bolt11: innerInvoice.Bolt11, + paymentHash: innerInvoice.PaymentHash, + paymentSecret: innerInvoice.PaymentSecret, + paymentPreimage: preimage, + expiresAt: innerInvoice.ExpiresAt, + } + outer := invoice{ + bolt11: outerInvoice, + paymentHash: outerInvoiceRaw.PaymentHash[:], + paymentSecret: innerInvoice.PaymentSecret, + paymentPreimage: preimage, + expiresAt: innerInvoice.ExpiresAt, + } + + return inner, outer +}