From b6ebf3f27d8a926c103b64227671652196d19f6a Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Thu, 10 Sep 2020 15:48:39 +0200 Subject: [PATCH] lntest: use web fee estimator in itests --- lntest/fee_service.go | 106 +++++++++++++++++++++++++++++++++++++ lntest/fee_service_test.go | 39 ++++++++++++++ lntest/harness.go | 15 ++++++ lntest/node.go | 6 +++ 4 files changed, 166 insertions(+) create mode 100644 lntest/fee_service.go create mode 100644 lntest/fee_service_test.go diff --git a/lntest/fee_service.go b/lntest/fee_service.go new file mode 100644 index 00000000..68e7d435 --- /dev/null +++ b/lntest/fee_service.go @@ -0,0 +1,106 @@ +package lntest + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "sync" + + "github.com/lightningnetwork/lnd/lnwallet/chainfee" +) + +const ( + // feeServiceTarget is the confirmation target for which a fee estimate + // is returned. Requests for higher confirmation targets will fall back + // to this. + feeServiceTarget = 2 + + // feeServicePort is the tcp port on which the service runs. + feeServicePort = 16534 +) + +// feeService runs a web service that provides fee estimation information. +type feeService struct { + feeEstimates + + srv *http.Server + wg sync.WaitGroup + + url string + + lock sync.Mutex +} + +// feeEstimates contains the current fee estimates. +type feeEstimates struct { + Fees map[uint32]uint32 `json:"fee_by_block_target"` +} + +// startFeeService spins up a go-routine to serve fee estimates. +func startFeeService() *feeService { + f := feeService{ + url: fmt.Sprintf( + "http://localhost:%v/fee-estimates.json", feeServicePort, + ), + } + + // Initialize default fee estimate. + f.Fees = map[uint32]uint32{feeServiceTarget: 50000} + + listenAddr := fmt.Sprintf(":%v", feeServicePort) + f.srv = &http.Server{ + Addr: listenAddr, + } + + http.HandleFunc("/fee-estimates.json", f.handleRequest) + + f.wg.Add(1) + go func() { + defer f.wg.Done() + + if err := f.srv.ListenAndServe(); err != http.ErrServerClosed { + fmt.Printf("error: cannot start fee api: %v", err) + } + }() + + return &f +} + +// handleRequest handles a client request for fee estimates. +func (f *feeService) handleRequest(w http.ResponseWriter, r *http.Request) { + f.lock.Lock() + defer f.lock.Unlock() + + bytes, err := json.Marshal(f.feeEstimates) + if err != nil { + fmt.Printf("error: cannot serialize "+ + "estimates: %v", err) + + return + } + + _, err = io.WriteString(w, string(bytes)) + if err != nil { + fmt.Printf("error: cannot send estimates: %v", + err) + } +} + +// stop stops the web server. +func (f *feeService) stop() { + if err := f.srv.Shutdown(context.Background()); err != nil { + fmt.Printf("error: cannot stop fee api: %v", err) + } + + f.wg.Wait() +} + +// setFee changes the current fee estimate for the fixed confirmation target. +func (f *feeService) setFee(fee chainfee.SatPerKWeight) { + f.lock.Lock() + defer f.lock.Unlock() + + f.Fees[feeServiceTarget] = uint32(fee.FeePerKVByte()) +} diff --git a/lntest/fee_service_test.go b/lntest/fee_service_test.go new file mode 100644 index 00000000..c7ad38c4 --- /dev/null +++ b/lntest/fee_service_test.go @@ -0,0 +1,39 @@ +package lntest + +import ( + "io/ioutil" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +// TestFeeService tests the itest fee estimating web service. +func TestFeeService(t *testing.T) { + service := startFeeService() + defer service.stop() + + service.setFee(5000) + + // Wait for service to start accepting connections. + var resp *http.Response + require.Eventually( + t, + func() bool { + var err error + resp, err = http.Get(service.url) // nolint:bodyclose + return err == nil + }, + 10*time.Second, time.Second, + ) + + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + + require.Equal( + t, "{\"fee_by_block_target\":{\"2\":20000}}", string(body), + ) +} diff --git a/lntest/harness.go b/lntest/harness.go index d680f9be..f8c0b7fc 100644 --- a/lntest/harness.go +++ b/lntest/harness.go @@ -22,6 +22,7 @@ import ( "github.com/lightningnetwork/lnd" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lntest/wait" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" "google.golang.org/grpc/grpclog" ) @@ -63,6 +64,10 @@ type NetworkHarness struct { // to main process. lndErrorChan chan error + // feeService is a web service that provides external fee estimates to + // lnd. + feeService *feeService + quit chan struct{} mtx sync.Mutex @@ -75,6 +80,8 @@ type NetworkHarness struct { func NewNetworkHarness(r *rpctest.Harness, b BackendConfig, lndBinary string) ( *NetworkHarness, error) { + feeService := startFeeService() + n := NetworkHarness{ activeNodes: make(map[int]*HarnessNode), nodesByPub: make(map[string]*HarnessNode), @@ -84,6 +91,7 @@ func NewNetworkHarness(r *rpctest.Harness, b BackendConfig, lndBinary string) ( netParams: r.ActiveNet, Miner: r, BackendCfg: b, + feeService: feeService, quit: make(chan struct{}), lndBinary: lndBinary, } @@ -251,6 +259,8 @@ func (n *NetworkHarness) TearDownAll() error { close(n.lndErrorChan) close(n.quit) + n.feeService.stop() + return nil } @@ -358,6 +368,7 @@ func (n *NetworkHarness) newNode(name string, extraArgs []string, BackendCfg: n.BackendCfg, NetParams: n.netParams, ExtraArgs: extraArgs, + FeeURL: n.feeService.url, }) if err != nil { return nil, err @@ -1404,6 +1415,10 @@ func (n *NetworkHarness) sendCoins(ctx context.Context, amt btcutil.Amount, return target.WaitForBalance(expectedBalance, true) } +func (n *NetworkHarness) SetFeeEstimate(fee chainfee.SatPerKWeight) { + n.feeService.setFee(fee) +} + // CopyFile copies the file src to dest. func CopyFile(dest, src string) error { s, err := os.Open(src) diff --git a/lntest/node.go b/lntest/node.go index 83d23b4c..222c4fe0 100644 --- a/lntest/node.go +++ b/lntest/node.go @@ -154,6 +154,8 @@ type NodeConfig struct { ProfilePort int AcceptKeySend bool + + FeeURL string } func (cfg NodeConfig) P2PAddr() string { @@ -232,6 +234,10 @@ func (cfg NodeConfig) genArgs() []string { args = append(args, "--accept-keysend") } + if cfg.FeeURL != "" { + args = append(args, "--feeurl="+cfg.FeeURL) + } + return args }