Repository: lightningnetwork/lnd
Pull request #7197 (2022-11-22 19:20:26)

lncli+chainrpc: add chainkit RPC service with endpoints GetBlock, GetBestBlock, GetBlockHash
## Change Description
This PR adds chain RPC calls so as to facilitate taro merkle proof verification, see: https://github.com/lightninglabs/taro/issues/143 .

## Steps to Test
Steps for reviewers to follow to test the change.

## Pull Request Checklist
### Testing
- [ ] Your PR passes all CI checks.
- [ ] Tests covering the positive and negative (error paths) are included.
- [ ] Bug fixes contain tests triggering the bug to prevent regressions.

### Code Style and Documentation
- [ ] The change obeys the [Code Documentation and Commenting](https://github.com/lightningnetwork/lnd/blob/master/docs/code_contribution_guidelines.md#CodeDocumentation) guidelines, and lines wrap at 80.
- [ ] Commits follow the [Ideal Git Commit Structure](https://github.com/lightningnetwork/lnd/blob/master/docs/code_contribution_guidelines.md#IdealGitCommitStructure).
- [ ] Any new logging statements use an appropriate subsystem and logging level.
- [ ]  [There is a change description in the release notes](https://github.com/lightningnetwork/lnd/tree/master/docs/release-notes), or `[skip ci]` in the commit message for small changes.

📝 Please see our  [Contribution Guidelines](https://github.com/lightningnetwork/lnd/blob/master/docs/code_contribution_guidelines.md) for further guidance.

Roasbeef: So while we're at it, I think it makes sense to consolidate the RPC calls a bit from the existing `neutrinorpc` server: https://github.com/lightningnetwork/lnd/blob/master/lnrpc/neutrinorpc/neutrino.proto#L7-L52. To do so, we'd extract the block/header related calls from the gRPC service, and move then to this _new_ `chainrpc` service. The remaining gRPC service for neutrino would be more neutrino specific as the remaining calls would be oriented around adding/removing peers, forcing a rescan (eventually), etc, etc. 

Arguably, given the fact that the compact filters _may_ also be available within the target backend (over RPC), we might also want to move those over as well. 
ffranr: > So while we're at it, I think it makes sense to consolidate the RPC calls a bit from the existing `neutrinorpc` server...

That makes sense to me! Would you mind if I start a new PR for that bit?
ffranr: @guggero I've moved the command line changes into their own commit.

The RPC additions and itests are included in a single commit. Let me know if you'd prefer to see a different commit structure.
guggero: I pushed a new tag `chaincfg/chainhash/v1.0.2` in `btcd` that we can now use in this PR.
guggero: needs a rebase because of the release notes.

Patch:
diff --git a/channeldb/payment_control.go b/channeldb/payment_control.go
index 3328f086d..7bd00e4d4 100644
--- a/channeldb/payment_control.go
+++ b/channeldb/payment_control.go
@@ -43,8 +43,7 @@ var (
 
 	// ErrPaymentTerminal is returned if we attempt to alter a payment that
 	// already has reached a terminal condition.
-	ErrPaymentTerminal = errors.New("payment has reached terminal " +
-		"condition")
+	ErrPaymentTerminal = errors.New("payment has reached terminal condition")
 
 	// ErrAttemptAlreadySettled is returned if we try to alter an already
 	// settled HTLC attempt.
@@ -78,8 +77,7 @@ var (
 
 	// ErrMPPTotalAmountMismatch is returned if we try to register an MPP
 	// shard where the total amount doesn't match existing shards.
-	ErrMPPTotalAmountMismatch = errors.New("mp payment total amount " +
-		"mismatch")
+	ErrMPPTotalAmountMismatch = errors.New("mp payment total amount mismatch")
 
 	// errNoAttemptInfo is returned when no attempt info is stored yet.
 	errNoAttemptInfo = errors.New("unable to find attempt info for " +
diff --git a/channeldb/payment_status.go b/channeldb/payment_status.go
deleted file mode 100644
index fedf99d2d..000000000
--- a/channeldb/payment_status.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package channeldb
-
-// PaymentStatus represent current status of payment.
-type PaymentStatus byte
-
-const (
-	// StatusUnknown is the status where a payment has never been initiated
-	// and hence is unknown.
-	StatusUnknown PaymentStatus = 0
-
-	// StatusInFlight is the status where a payment has been initiated, but
-	// a response has not been received.
-	StatusInFlight PaymentStatus = 1
-
-	// StatusSucceeded is the status where a payment has been initiated and
-	// the payment was completed successfully.
-	StatusSucceeded PaymentStatus = 2
-
-	// StatusFailed is the status where a payment has been initiated and a
-	// failure result has come back.
-	StatusFailed PaymentStatus = 3
-)
-
-// String returns readable representation of payment status.
-func (ps PaymentStatus) String() string {
-	switch ps {
-	case StatusUnknown:
-		return "Unknown"
-
-	case StatusInFlight:
-		return "In Flight"
-
-	case StatusSucceeded:
-		return "Succeeded"
-
-	case StatusFailed:
-		return "Failed"
-
-	default:
-		return "Unknown"
-	}
-}
diff --git a/channeldb/payments.go b/channeldb/payments.go
index 93abe4fb1..aef5ddfa2 100644
--- a/channeldb/payments.go
+++ b/channeldb/payments.go
@@ -175,6 +175,43 @@ func (r FailureReason) String() string {
 	return "unknown"
 }
 
+// PaymentStatus represent current status of payment
+type PaymentStatus byte
+
+const (
+	// StatusUnknown is the status where a payment has never been initiated
+	// and hence is unknown.
+	StatusUnknown PaymentStatus = 0
+
+	// StatusInFlight is the status where a payment has been initiated, but
+	// a response has not been received.
+	StatusInFlight PaymentStatus = 1
+
+	// StatusSucceeded is the status where a payment has been initiated and
+	// the payment was completed successfully.
+	StatusSucceeded PaymentStatus = 2
+
+	// StatusFailed is the status where a payment has been initiated and a
+	// failure result has come back.
+	StatusFailed PaymentStatus = 3
+)
+
+// String returns readable representation of payment status.
+func (ps PaymentStatus) String() string {
+	switch ps {
+	case StatusUnknown:
+		return "Unknown"
+	case StatusInFlight:
+		return "In Flight"
+	case StatusSucceeded:
+		return "Succeeded"
+	case StatusFailed:
+		return "Failed"
+	default:
+		return "Unknown"
+	}
+}
+
 // PaymentCreationInfo is the information necessary to have ready when
 // initiating a payment, moving it into state InFlight.
 type PaymentCreationInfo struct {
diff --git a/cmd/lncli/chainrpc_active.go b/cmd/lncli/chainrpc_active.go
new file mode 100644
index 000000000..0d8b891ac
--- /dev/null
+++ b/cmd/lncli/chainrpc_active.go
@@ -0,0 +1,219 @@
+//go:build chainrpc
+// +build chainrpc
+
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"strconv"
+
+	"github.com/btcsuite/btcd/chaincfg/chainhash"
+	"github.com/btcsuite/btcd/wire"
+	"github.com/lightningnetwork/lnd/lnrpc/chainrpc"
+	"github.com/urfave/cli"
+)
+
+// chainCommands will return the set of commands to enable for chainrpc builds.
+func chainCommands() []cli.Command {
+	return []cli.Command{
+		{
+			Name:     "chain",
+			Category: "On-chain",
+			Usage:    "Interact with the bitcoin blockchain.",
+			Subcommands: []cli.Command{
+				getBlockCommand,
+				getBestBlockCommand,
+				getBlockHashCommand,
+			},
+		},
+	}
+}
+
+func getChainClient(ctx *cli.Context) (chainrpc.ChainKitClient, func()) {
+	conn := getClientConn(ctx, false)
+
+	cleanUp := func() {
+		conn.Close()
+	}
+
+	return chainrpc.NewChainKitClient(conn), cleanUp
+}
+
+var getBlockCommand = cli.Command{
+	Name:        "getblock",
+	Category:    "On-chain",
+	Usage:       "Get block by block hash.",
+	Description: "Returns a block given the corresponding block hash.",
+	Flags: []cli.Flag{
+		cli.StringFlag{
+			Name:  "hash",
+			Usage: "the target block hash",
+		},
+		cli.BoolFlag{
+			Name:  "verbose",
+			Usage: "print entire block as JSON",
+		},
+	},
+	Action: actionDecorator(getBlock),
+}
+
+func getBlock(ctx *cli.Context) error {
+	ctxc := getContext()
+
+	var (
+		args            = ctx.Args()
+		blockHashString string
+	)
+
+	verbose := false
+	if ctx.IsSet("verbose") {
+		verbose = true
+	}
+
+	switch {
+	case ctx.IsSet("hash"):
+		blockHashString = ctx.String("hash")
+
+	case args.Present():
+		blockHashString = args.First()
+
+	default:
+		return fmt.Errorf("hash argument missing")
+	}
+
+	blockHash, err := chainhash.NewHashFromStr(blockHashString)
+	if err != nil {
+		return err
+	}
+
+	client, cleanUp := getChainClient(ctx)
+	defer cleanUp()
+
+	req := &chainrpc.GetBlockRequest{BlockHash: blockHash.CloneBytes()}
+	resp, err := client.GetBlock(ctxc, req)
+	if err != nil {
+		return err
+	}
+
+	// Convert raw block bytes into wire.MsgBlock.
+	msgBlock := &wire.MsgBlock{}
+	blockReader := bytes.NewReader(resp.RawBlock)
+	err = msgBlock.Deserialize(blockReader)
+	if err != nil {
+		return err
+	}
+
+	if verbose {
+		printJSON(msgBlock)
+	} else {
+		printJSON(msgBlock.Header)
+	}
+
+	return nil
+}
+
+var getBestBlockCommand = cli.Command{
+	Name:     "getbestblock",
+	Category: "On-chain",
+	Usage:    "Get best block.",
+	Description: "Returns the latest block hash and height from the " +
+		"valid most-work chain.",
+	Action: actionDecorator(getBestBlock),
+}
+
+func getBestBlock(ctx *cli.Context) error {
+	ctxc := getContext()
+
+	client, cleanUp := getChainClient(ctx)
+	defer cleanUp()
+
+	resp, err := client.GetBestBlock(ctxc, &chainrpc.GetBestBlockRequest{})
+	if err != nil {
+		return err
+	}
+
+	// Cast gRPC block hash bytes as chain hash type.
+	var blockHash chainhash.Hash
+	copy(blockHash[:], resp.BlockHash)
+
+	printJSON(struct {
+		BlockHash   chainhash.Hash `json:"block_hash"`
+		BlockHeight int32          `json:"block_height"`
+	}{
+		BlockHash:   blockHash,
+		BlockHeight: resp.BlockHeight,
+	})
+
+	return nil
+}
+
+var getBlockHashCommand = cli.Command{
+	Name:     "getblockhash",
+	Category: "On-chain",
+	Usage:    "Get block hash by block height.",
+	Description: "Returns the block hash from the best chain at a given " +
+		"height.",
+	Flags: []cli.Flag{
+		cli.Int64Flag{
+			Name:  "height",
+			Usage: "target block height",
+		},
+	},
+	Action: actionDecorator(getBlockHash),
+}
+
+func getBlockHash(ctx *cli.Context) error {
+	ctxc := getContext()
+
+	// Display the command's help message if we do not have the expected
+	// number of arguments/flags.
+	if ctx.NArg()+ctx.NumFlags() != 1 {
+		return cli.ShowCommandHelp(ctx, "getblockhash")
+	}
+
+	var (
+		args        = ctx.Args()
+		blockHeight int64
+	)
+
+	switch {
+	case ctx.IsSet("height"):
+		blockHeight = ctx.Int64("height")
+
+	case args.Present():
+		blockHeightString := args.First()
+
+		// Convert block height positional argument from string to
+		// int64.
+		var err error
+		blockHeight, err = strconv.ParseInt(blockHeightString, 10, 64)
+		if err != nil {
+			return err
+		}
+
+	default:
+		return fmt.Errorf("block height argument missing")
+	}
+
+	client, cleanUp := getChainClient(ctx)
+	defer cleanUp()
+
+	req := &chainrpc.GetBlockHashRequest{BlockHeight: blockHeight}
+	resp, err := client.GetBlockHash(ctxc, req)
+	if err != nil {
+		return err
+	}
+
+	// Cast gRPC block hash bytes as chain hash type.
+	var blockHash chainhash.Hash
+	copy(blockHash[:], resp.BlockHash)
+
+	printJSON(struct {
+		BlockHash chainhash.Hash `json:"block_hash"`
+	}{
+		BlockHash: blockHash,
+	})
+
+	return nil
+}
diff --git a/cmd/lncli/chainrpc_default.go b/cmd/lncli/chainrpc_default.go
new file mode 100644
index 000000000..fa1ea99e2
--- /dev/null
+++ b/cmd/lncli/chainrpc_default.go
@@ -0,0 +1,11 @@
+//go:build !chainrpc
+// +build !chainrpc
+
+package main
+
+import "github.com/urfave/cli"
+
+// chainCommands will return nil for non-chainrpc builds.
+func chainCommands() []cli.Command {
+	return nil
+}
diff --git a/cmd/lncli/main.go b/cmd/lncli/main.go
index 116a42b13..4783ec06d 100644
--- a/cmd/lncli/main.go
+++ b/cmd/lncli/main.go
@@ -500,6 +500,7 @@ func main() {
 	app.Commands = append(app.Commands, wtclientCommands()...)
 	app.Commands = append(app.Commands, devCommands()...)
 	app.Commands = append(app.Commands, peersCommands()...)
+	app.Commands = append(app.Commands, chainCommands()...)
 
 	if err := app.Run(os.Args); err != nil {
 		fatal(err)
diff --git a/cmd/lncli/neutrino_active.go b/cmd/lncli/neutrino_active.go
index 093821a3d..4267738e1 100644
--- a/cmd/lncli/neutrino_active.go
+++ b/cmd/lncli/neutrino_active.go
@@ -4,8 +4,6 @@
 package main
 
 import (
-	"strconv"
-
 	"github.com/lightningnetwork/lnd/lnrpc/neutrinorpc"
 	"github.com/urfave/cli"
 )
@@ -193,42 +191,6 @@ func getBlockHeader(ctx *cli.Context) error {
 	return nil
 }
 
-var getBlockCommand = cli.Command{
-	Name:        "getblock",
-	Usage:       "Get a block.",
-	Category:    "Neutrino",
-	Description: "Returns a block with a particular block hash.",
-	ArgsUsage:   "hash",
-	Action:      actionDecorator(getBlock),
-}
-
-func getBlock(ctx *cli.Context) error {
-	ctxc := getContext()
-	args := ctx.Args()
-
-	// Display the command's help message if we do not have the expected
-	// number of arguments/flags.
-	if !args.Present() {
-		return cli.ShowCommandHelp(ctx, "getblock")
-	}
-
-	client, cleanUp := getNeutrinoKitClient(ctx)
-	defer cleanUp()
-
-	req := &neutrinorpc.GetBlockRequest{
-		Hash: args.First(),
-	}
-
-	resp, err := client.GetBlock(ctxc, req)
-	if err != nil {
-		return err
-	}
-
-	printRespJSON(resp)
-
-	return nil
-}
-
 var getCFilterCommand = cli.Command{
 	Name:        "getcfilter",
 	Usage:       "Get a compact filter.",
@@ -263,47 +225,6 @@ func getCFilter(ctx *cli.Context) error {
 	return nil
 }
 
-var getBlockHashCommand = cli.Command{
-	Name:        "getblockhash",
-	Usage:       "Get a block hash.",
-	Category:    "Neutrino",
-	Description: "Returns the header hash of a block at a given height.",
-	ArgsUsage:   "height",
-	Action:      actionDecorator(getBlockHash),
-}
-
-func getBlockHash(ctx *cli.Context) error {
-	ctxc := getContext()
-	args := ctx.Args()
-
-	// Display the command's help message if we do not have the expected
-	// number of arguments/flags.
-	if !args.Present() {
-		return cli.ShowCommandHelp(ctx, "getblockhash")
-	}
-
-	client, cleanUp := getNeutrinoKitClient(ctx)
-	defer cleanUp()
-
-	height, err := strconv.ParseInt(args.First(), 10, 32)
-	if err != nil {
-		return err
-	}
-
-	req := &neutrinorpc.GetBlockHashRequest{
-		Height: int32(height),
-	}
-
-	resp, err := client.GetBlockHash(ctxc, req)
-	if err != nil {
-		return err
-	}
-
-	printRespJSON(resp)
-
-	return nil
-}
-
 // neutrinoCommands will return the set of commands to enable for neutrinorpc
 // builds.
 func neutrinoCommands() []cli.Command {
@@ -318,10 +239,8 @@ func neutrinoCommands() []cli.Command {
 				addPeerCommand,
 				disconnectPeerCommand,
 				isBannedCommand,
-				getBlockCommand,
 				getBlockHeaderCommand,
 				getCFilterCommand,
-				getBlockHashCommand,
 			},
 		},
 	}
diff --git a/docs/release-notes/release-notes-0.16.0.md b/docs/release-notes/release-notes-0.16.0.md
index e05966fb8..4afff6d7b 100644
--- a/docs/release-notes/release-notes-0.16.0.md
+++ b/docs/release-notes/release-notes-0.16.0.md
@@ -73,6 +73,11 @@
   `ListInvoiceRequest` and
   `ListPaymentsRequest`](https://github.com/lightningnetwork/lnd/pull/7159).
 
+* [Add chainkit RPC endpoints](https://github.com/lightningnetwork/lnd/pull/7197):
+  GetBlock, GetBestBlock, GetBlockHash. These endpoints provide access to chain
+  block data.
+
+
 ## Wallet
 
 * [Allows Taproot public keys and tap scripts to be imported as watch-only
@@ -205,6 +210,11 @@ certain large transactions](https://github.com/lightningnetwork/lnd/pull/7100).
 * [Allow lncli to read binary PSBTs](https://github.com/lightningnetwork/lnd/pull/7122)
   from a file during PSBT channel funding flow to comply with [BIP 174](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#specification)
 
+* [Add interface to chainkit RPC](https://github.com/lightningnetwork/lnd/pull/7197).
+  This addition consists of the `chain` subcommand group: `getblock`,
+  `getblockhash`, and `getbestblock`. These commands provide access to chain
+  block data.
+
 ## Code Health
 
 * [test: use `T.TempDir` to create temporary test
@@ -222,13 +232,6 @@ certain large transactions](https://github.com/lightningnetwork/lnd/pull/7100).
 
 * [Fix loop and other temporary variables being accessed in
   goroutines](https://github.com/lightningnetwork/lnd/pull/7188).
-
-* [CI: update test coverage library
-  go-acc](https://github.com/lightningnetwork/lnd/pull/7221) v0.2.6 -> v0.2.8
-
-* Payment related code [has been
-  refactored](https://github.com/lightningnetwork/lnd/pull/7174) to allow the
-  usage of new payment statuses.
  
 ## Watchtowers
 
diff --git a/go.mod b/go.mod
index 689eb764b..048f0de24 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ require (
 	github.com/btcsuite/btcd/btcec/v2 v2.2.2
 	github.com/btcsuite/btcd/btcutil v1.1.3
 	github.com/btcsuite/btcd/btcutil/psbt v1.1.5
-	github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1
+	github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2
 	github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
 	github.com/btcsuite/btcwallet v0.16.6-0.20221203002441-6c7480c8a46b
 	github.com/btcsuite/btcwallet/wallet/txauthor v1.3.2
diff --git a/go.sum b/go.sum
index 023e5fd69..1c4856482 100644
--- a/go.sum
+++ b/go.sum
@@ -96,6 +96,8 @@ github.com/btcsuite/btcd/btcutil/psbt v1.1.5/go.mod h1:kA6FLH/JfUx++j9pYU0pyu+Z8
 github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
 github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U=
 github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
+github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 h1:KdUfX2zKommPRa+PD0sWZUyXe9w277ABlgELO7H04IM=
+github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
 github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
 github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
 github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
diff --git a/htlcswitch/switch.go b/htlcswitch/switch.go
index ee1712c77..34997ac82 100644
--- a/htlcswitch/switch.go
+++ b/htlcswitch/switch.go
@@ -427,16 +427,16 @@ func (s *Switch) ProcessContractResolution(msg contractcourt.ResolutionMsg) erro
 	}
 }
 
-// GetAttemptResult returns the result of the payment attempt with the given
-// attemptID. The paymentHash should be set to the payment's overall hash, or
-// in case of AMP payments the payment's unique identifier.
+// GetPaymentResult returns the the result of the payment attempt with the
+// given attemptID. The paymentHash should be set to the payment's overall
+// hash, or in case of AMP payments the payment's unique identifier.
 //
 // The method returns a channel where the payment result will be sent when
 // available, or an error is encountered during forwarding. When a result is
 // received on the channel, the HTLC is guaranteed to no longer be in flight.
 // The switch shutting down is signaled by closing the channel. If the
 // attemptID is unknown, ErrPaymentIDNotFound will be returned.
-func (s *Switch) GetAttemptResult(attemptID uint64, paymentHash lntypes.Hash,
+func (s *Switch) GetPaymentResult(attemptID uint64, paymentHash lntypes.Hash,
 	deobfuscator ErrorDecrypter) (<-chan *PaymentResult, error) {
 
 	var (
diff --git a/lnrpc/chainrpc/chainnotifier_server.go b/lnrpc/chainrpc/chain_server.go
similarity index 85%
rename from lnrpc/chainrpc/chainnotifier_server.go
rename to lnrpc/chainrpc/chain_server.go
index 67e6c5057..38ef358d6 100644
--- a/lnrpc/chainrpc/chainnotifier_server.go
+++ b/lnrpc/chainrpc/chain_server.go
@@ -42,6 +42,18 @@ var (
 
 	// macPermissions maps RPC calls to the permissions they require.
 	macPermissions = map[string][]bakery.Op{
+		"/chainrpc.ChainKit/GetBlock": {{
+			Entity: "onchain",
+			Action: "read",
+		}},
+		"/chainrpc.ChainKit/GetBestBlock": {{
+			Entity: "onchain",
+			Action: "read",
+		}},
+		"/chainrpc.ChainKit/GetBlockHash": {{
+			Entity: "onchain",
+			Action: "read",
+		}},
 		"/chainrpc.ChainNotifier/RegisterConfirmationsNtfn": {{
 			Entity: "onchain",
 			Action: "read",
@@ -77,17 +89,19 @@ var (
 // It is used to register the gRPC sub-server with the root server before we
 // have the necessary dependencies to populate the actual sub-server.
 type ServerShell struct {
+	ChainKitServer
 	ChainNotifierServer
 }
 
-// Server is a sub-server of the main RPC server: the chain notifier RPC. This
-// RPC sub-server allows external callers to access the full chain notifier
-// capabilities of lnd. This allows callers to create custom protocols, external
-// to lnd, even backed by multiple distinct lnd across independent failure
-// domains.
+// Server is a sub-server of the main RPC server. It serves the chainkit RPC
+// and chain notifier RPC. This RPC sub-server allows external callers to access
+// the full chainkit and chain notifier capabilities of lnd. This allows callers
+// to create custom protocols, external to lnd, even backed by multiple distinct
+// lnd across independent failure domains.
 type Server struct {
 	// Required by the grpc-gateway/v2 library for forward compatibility.
 	UnimplementedChainNotifierServer
+	UnimplementedChainKitServer
 
 	started sync.Once
 	stopped sync.Once
@@ -149,8 +163,10 @@ func New(cfg *Config) (*Server, lnrpc.MacaroonPerms, error) {
 }
 
 // Compile-time checks to ensure that Server fully implements the
-// ChainNotifierServer gRPC service and lnrpc.SubServer interface.
+// ChainNotifierServer gRPC service, ChainKitServer gRPC service, and
+// lnrpc.SubServer interface.
 var _ ChainNotifierServer = (*Server)(nil)
+var _ ChainKitServer = (*Server)(nil)
 var _ lnrpc.SubServer = (*Server)(nil)
 
 // Start launches any helper goroutines required for the server to function.
@@ -188,10 +204,13 @@ func (r *ServerShell) RegisterWithRootServer(grpcServer *grpc.Server) error {
 	// We make sure that we register it with the main gRPC server to ensure
 	// all our methods are routed properly.
 	RegisterChainNotifierServer(grpcServer, r)
-
 	log.Debug("ChainNotifier RPC server successfully register with root " +
 		"gRPC server")
 
+	RegisterChainKitServer(grpcServer, r)
+	log.Debug("ChainKit RPC server successfully register with root gRPC " +
+		"server")
+
 	return nil
 }
 
@@ -214,6 +233,18 @@ func (r *ServerShell) RegisterWithRestServer(ctx context.Context,
 
 	log.Debugf("ChainNotifier REST server successfully registered with " +
 		"root REST server")
+
+	// Register chainkit with the main REST server to ensure all our methods
+	// are routed properly.
+	err = RegisterChainKitHandlerFromEndpoint(ctx, mux, dest, opts)
+	if err != nil {
+		log.Errorf("Could not register ChainKit REST server with root "+
+			"REST server: %v", err)
+		return err
+	}
+	log.Debugf("ChainKit REST server successfully registered with root " +
+		"REST server")
+
 	return nil
 }
 
@@ -233,9 +264,66 @@ func (r *ServerShell) CreateSubServer(configRegistry lnrpc.SubServerConfigDispat
 	}
 
 	r.ChainNotifierServer = subServer
+	r.ChainKitServer = subServer
 	return subServer, macPermissions, nil
 }
 
+// GetBlock returns a block given the corresponding block hash.
+func (s *Server) GetBlock(ctx context.Context,
+	in *GetBlockRequest) (*GetBlockResponse, error) {
+
+	// We'll start by reconstructing the RPC request into what the
+	// underlying chain functionality expects.
+	var blockHash chainhash.Hash
+	copy(blockHash[:], in.BlockHash)
+
+	block, err := s.cfg.Chain.GetBlock(&blockHash)
+	if err != nil {
+		return nil, err
+	}
+
+	// Serialize block for RPC response.
+	var blockBuf bytes.Buffer
+	err = block.Serialize(&blockBuf)
+	if err != nil {
+		return nil, err
+	}
+	rawBlock := blockBuf.Bytes()
+
+	return &GetBlockResponse{RawBlock: rawBlock}, nil
+}
+
+// GetBestBlock returns the latest block hash and current height of the valid
+// most-work chain.
+func (s *Server) GetBestBlock(ctx context.Context,
+	req *GetBestBlockRequest) (*GetBestBlockResponse, error) {
+
+	blockHash, blockHeight, err := s.cfg.Chain.GetBestBlock()
+	if err != nil {
+		return nil, err
+	}
+
+	return &GetBestBlockResponse{
+		BlockHash:   blockHash[:],
+		BlockHeight: blockHeight,
+	}, nil
+}
+
+// GetBlockHash returns the hash of the block in the best blockchain
+// at the given height.
+func (s *Server) GetBlockHash(ctx context.Context,
+	req *GetBlockHashRequest) (*GetBlockHashResponse, error) {
+
+	blockHash, err := s.cfg.Chain.GetBlockHash(req.BlockHeight)
+	if err != nil {
+		return nil, err
+	}
+
+	return &GetBlockHashResponse{
+		BlockHash: blockHash[:],
+	}, nil
+}
+
 // RegisterConfirmationsNtfn is a synchronous response-streaming RPC that
 // registers an intent for a client to be notified once a confirmation request
 // has reached its required number of confirmations on-chain.
diff --git a/lnrpc/chainrpc/chainkit.pb.go b/lnrpc/chainrpc/chainkit.pb.go
new file mode 100644
index 000000000..be6eafe4d
--- /dev/null
+++ b/lnrpc/chainrpc/chainkit.pb.go
@@ -0,0 +1,490 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.26.0
+// 	protoc        v3.6.1
+// source: chainrpc/chainkit.proto
+
+package chainrpc
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type GetBlockRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// The hash of the requested block.
+	BlockHash []byte `protobuf:"bytes,1,opt,name=block_hash,json=blockHash,proto3" json:"block_hash,omitempty"`
+}
+
+func (x *GetBlockRequest) Reset() {
+	*x = GetBlockRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_chainrpc_chainkit_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *GetBlockRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetBlockRequest) ProtoMessage() {}
+
+func (x *GetBlockRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_chainrpc_chainkit_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetBlockRequest.ProtoReflect.Descriptor instead.
+func (*GetBlockRequest) Descriptor() ([]byte, []int) {
+	return file_chainrpc_chainkit_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *GetBlockRequest) GetBlockHash() []byte {
+	if x != nil {
+		return x.BlockHash
+	}
+	return nil
+}
+
+// TODO(ffranr): The neutrino GetBlock response includes many
+// additional helpful fields. Consider adding them here also.
+type GetBlockResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// The raw bytes of the requested block.
+	RawBlock []byte `protobuf:"bytes,1,opt,name=raw_block,json=rawBlock,proto3" json:"raw_block,omitempty"`
+}
+
+func (x *GetBlockResponse) Reset() {
+	*x = GetBlockResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_chainrpc_chainkit_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *GetBlockResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetBlockResponse) ProtoMessage() {}
+
+func (x *GetBlockResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_chainrpc_chainkit_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetBlockResponse.ProtoReflect.Descriptor instead.
+func (*GetBlockResponse) Descriptor() ([]byte, []int) {
+	return file_chainrpc_chainkit_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *GetBlockResponse) GetRawBlock() []byte {
+	if x != nil {
+		return x.RawBlock
+	}
+	return nil
+}
+
+type GetBestBlockRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+}
+
+func (x *GetBestBlockRequest) Reset() {
+	*x = GetBestBlockRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_chainrpc_chainkit_proto_msgTypes[2]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *GetBestBlockRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetBestBlockRequest) ProtoMessage() {}
+
+func (x *GetBestBlockRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_chainrpc_chainkit_proto_msgTypes[2]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetBestBlockRequest.ProtoReflect.Descriptor instead.
+func (*GetBestBlockRequest) Descriptor() ([]byte, []int) {
+	return file_chainrpc_chainkit_proto_rawDescGZIP(), []int{2}
+}
+
+type GetBestBlockResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// The hash of the best block.
+	BlockHash []byte `protobuf:"bytes,1,opt,name=block_hash,json=blockHash,proto3" json:"block_hash,omitempty"`
+	// The height of the best block.
+	BlockHeight int32 `protobuf:"varint,2,opt,name=block_height,json=blockHeight,proto3" json:"block_height,omitempty"`
+}
+
+func (x *GetBestBlockResponse) Reset() {
+	*x = GetBestBlockResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_chainrpc_chainkit_proto_msgTypes[3]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *GetBestBlockResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetBestBlockResponse) ProtoMessage() {}
+
+func (x *GetBestBlockResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_chainrpc_chainkit_proto_msgTypes[3]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetBestBlockResponse.ProtoReflect.Descriptor instead.
+func (*GetBestBlockResponse) Descriptor() ([]byte, []int) {
+	return file_chainrpc_chainkit_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *GetBestBlockResponse) GetBlockHash() []byte {
+	if x != nil {
+		return x.BlockHash
+	}
+	return nil
+}
+
+func (x *GetBestBlockResponse) GetBlockHeight() int32 {
+	if x != nil {
+		return x.BlockHeight
+	}
+	return 0
+}
+
+type GetBlockHashRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// Block height of the target best chain block.
+	BlockHeight int64 `protobuf:"varint,1,opt,name=block_height,json=blockHeight,proto3" json:"block_height,omitempty"`
+}
+
+func (x *GetBlockHashRequest) Reset() {
+	*x = GetBlockHashRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_chainrpc_chainkit_proto_msgTypes[4]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *GetBlockHashRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetBlockHashRequest) ProtoMessage() {}
+
+func (x *GetBlockHashRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_chainrpc_chainkit_proto_msgTypes[4]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetBlockHashRequest.ProtoReflect.Descriptor instead.
+func (*GetBlockHashRequest) Descriptor() ([]byte, []int) {
+	return file_chainrpc_chainkit_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *GetBlockHashRequest) GetBlockHeight() int64 {
+	if x != nil {
+		return x.BlockHeight
+	}
+	return 0
+}
+
+type GetBlockHashResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// The hash of the best block at the specified height.
+	BlockHash []byte `protobuf:"bytes,1,opt,name=block_hash,json=blockHash,proto3" json:"block_hash,omitempty"`
+}
+
+func (x *GetBlockHashResponse) Reset() {
+	*x = GetBlockHashResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_chainrpc_chainkit_proto_msgTypes[5]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *GetBlockHashResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetBlockHashResponse) ProtoMessage() {}
+
+func (x *GetBlockHashResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_chainrpc_chainkit_proto_msgTypes[5]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetBlockHashResponse.ProtoReflect.Descriptor instead.
+func (*GetBlockHashResponse) Descriptor() ([]byte, []int) {
+	return file_chainrpc_chainkit_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *GetBlockHashResponse) GetBlockHash() []byte {
+	if x != nil {
+		return x.BlockHash
+	}
+	return nil
+}
+
+var File_chainrpc_chainkit_proto protoreflect.FileDescriptor
+
+var file_chainrpc_chainkit_proto_rawDesc = []byte{
+	0x0a, 0x17, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x72, 0x70, 0x63, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e,
+	0x6b, 0x69, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e,
+	0x72, 0x70, 0x63, 0x22, 0x30, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52,
+	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f,
+	0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x62, 0x6c, 0x6f, 0x63,
+	0x6b, 0x48, 0x61, 0x73, 0x68, 0x22, 0x2f, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63,
+	0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x61, 0x77,
+	0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x72, 0x61,
+	0x77, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x22, 0x15, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x42, 0x65, 0x73,
+	0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x58, 0x0a,
+	0x14, 0x47, 0x65, 0x74, 0x42, 0x65, 0x73, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x73,
+	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x68,
+	0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x62, 0x6c, 0x6f, 0x63, 0x6b,
+	0x48, 0x61, 0x73, 0x68, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x68, 0x65,
+	0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x62, 0x6c, 0x6f, 0x63,
+	0x6b, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x22, 0x38, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x42, 0x6c,
+	0x6f, 0x63, 0x6b, 0x48, 0x61, 0x73, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21,
+	0x0a, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x01,
+	0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x65, 0x69, 0x67, 0x68,
+	0x74, 0x22, 0x35, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x61, 0x73,
+	0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x6c, 0x6f,
+	0x63, 0x6b, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x62,
+	0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x61, 0x73, 0x68, 0x32, 0xeb, 0x01, 0x0a, 0x08, 0x43, 0x68, 0x61,
+	0x69, 0x6e, 0x4b, 0x69, 0x74, 0x12, 0x41, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63,
+	0x6b, 0x12, 0x19, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74,
+	0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x63,
+	0x68, 0x61, 0x69, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b,
+	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x42,
+	0x65, 0x73, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x1d, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e,
+	0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x65, 0x73, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b,
+	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x72,
+	0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x65, 0x73, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52,
+	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x42, 0x6c,
+	0x6f, 0x63, 0x6b, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1d, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x72,
+	0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x61, 0x73, 0x68, 0x52,
+	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x72, 0x70,
+	0x63, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x61, 0x73, 0x68, 0x52, 0x65,
+	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x30, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
+	0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6e, 0x65,
+	0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6e, 0x64, 0x2f, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2f,
+	0x63, 0x68, 0x61, 0x69, 0x6e, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_chainrpc_chainkit_proto_rawDescOnce sync.Once
+	file_chainrpc_chainkit_proto_rawDescData = file_chainrpc_chainkit_proto_rawDesc
+)
+
+func file_chainrpc_chainkit_proto_rawDescGZIP() []byte {
+	file_chainrpc_chainkit_proto_rawDescOnce.Do(func() {
+		file_chainrpc_chainkit_proto_rawDescData = protoimpl.X.CompressGZIP(file_chainrpc_chainkit_proto_rawDescData)
+	})
+	return file_chainrpc_chainkit_proto_rawDescData
+}
+
+var file_chainrpc_chainkit_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
+var file_chainrpc_chainkit_proto_goTypes = []interface{}{
+	(*GetBlockRequest)(nil),      // 0: chainrpc.GetBlockRequest
+	(*GetBlockResponse)(nil),     // 1: chainrpc.GetBlockResponse
+	(*GetBestBlockRequest)(nil),  // 2: chainrpc.GetBestBlockRequest
+	(*GetBestBlockResponse)(nil), // 3: chainrpc.GetBestBlockResponse
+	(*GetBlockHashRequest)(nil),  // 4: chainrpc.GetBlockHashRequest
+	(*GetBlockHashResponse)(nil), // 5: chainrpc.GetBlockHashResponse
+}
+var file_chainrpc_chainkit_proto_depIdxs = []int32{
+	0, // 0: chainrpc.ChainKit.GetBlock:input_type -> chainrpc.GetBlockRequest
+	2, // 1: chainrpc.ChainKit.GetBestBlock:input_type -> chainrpc.GetBestBlockRequest
+	4, // 2: chainrpc.ChainKit.GetBlockHash:input_type -> chainrpc.GetBlockHashRequest
+	1, // 3: chainrpc.ChainKit.GetBlock:output_type -> chainrpc.GetBlockResponse
+	3, // 4: chainrpc.ChainKit.GetBestBlock:output_type -> chainrpc.GetBestBlockResponse
+	5, // 5: chainrpc.ChainKit.GetBlockHash:output_type -> chainrpc.GetBlockHashResponse
+	3, // [3:6] is the sub-list for method output_type
+	0, // [0:3] is the sub-list for method input_type
+	0, // [0:0] is the sub-list for extension type_name
+	0, // [0:0] is the sub-list for extension extendee
+	0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_chainrpc_chainkit_proto_init() }
+func file_chainrpc_chainkit_proto_init() {
+	if File_chainrpc_chainkit_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_chainrpc_chainkit_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*GetBlockRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_chainrpc_chainkit_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*GetBlockResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_chainrpc_chainkit_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*GetBestBlockRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_chainrpc_chainkit_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*GetBestBlockResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_chainrpc_chainkit_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*GetBlockHashRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_chainrpc_chainkit_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*GetBlockHashResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_chainrpc_chainkit_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   6,
+			NumExtensions: 0,
+			NumServices:   1,
+		},
+		GoTypes:           file_chainrpc_chainkit_proto_goTypes,
+		DependencyIndexes: file_chainrpc_chainkit_proto_depIdxs,
+		MessageInfos:      file_chainrpc_chainkit_proto_msgTypes,
+	}.Build()
+	File_chainrpc_chainkit_proto = out.File
+	file_chainrpc_chainkit_proto_rawDesc = nil
+	file_chainrpc_chainkit_proto_goTypes = nil
+	file_chainrpc_chainkit_proto_depIdxs = nil
+}
diff --git a/lnrpc/chainrpc/chainkit.pb.gw.go b/lnrpc/chainrpc/chainkit.pb.gw.go
new file mode 100644
index 000000000..d49ed54ff
--- /dev/null
+++ b/lnrpc/chainrpc/chainkit.pb.gw.go
@@ -0,0 +1,317 @@
+// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.
+// source: chainrpc/chainkit.proto
+
+/*
+Package chainrpc is a reverse proxy.
+
+It translates gRPC into RESTful JSON APIs.
+*/
+package chainrpc
+
+import (
+	"context"
+	"io"
+	"net/http"
+
+	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
+	"github.com/grpc-ecosystem/grpc-gateway/v2/utilities"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/grpclog"
+	"google.golang.org/grpc/metadata"
+	"google.golang.org/grpc/status"
+	"google.golang.org/protobuf/proto"
+)
+
+// Suppress "imported and not used" errors
+var _ codes.Code
+var _ io.Reader
+var _ status.Status
+var _ = runtime.String
+var _ = utilities.NewDoubleArray
+var _ = metadata.Join
+
+var (
+	filter_ChainKit_GetBlock_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
+)
+
+func request_ChainKit_GetBlock_0(ctx context.Context, marshaler runtime.Marshaler, client ChainKitClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq GetBlockRequest
+	var metadata runtime.ServerMetadata
+
+	if err := req.ParseForm(); err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+	if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ChainKit_GetBlock_0); err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+
+	msg, err := client.GetBlock(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
+	return msg, metadata, err
+
+}
+
+func local_request_ChainKit_GetBlock_0(ctx context.Context, marshaler runtime.Marshaler, server ChainKitServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq GetBlockRequest
+	var metadata runtime.ServerMetadata
+
+	if err := req.ParseForm(); err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+	if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ChainKit_GetBlock_0); err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+
+	msg, err := server.GetBlock(ctx, &protoReq)
+	return msg, metadata, err
+
+}
+
+func request_ChainKit_GetBestBlock_0(ctx context.Context, marshaler runtime.Marshaler, client ChainKitClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq GetBestBlockRequest
+	var metadata runtime.ServerMetadata
+
+	msg, err := client.GetBestBlock(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
+	return msg, metadata, err
+
+}
+
+func local_request_ChainKit_GetBestBlock_0(ctx context.Context, marshaler runtime.Marshaler, server ChainKitServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq GetBestBlockRequest
+	var metadata runtime.ServerMetadata
+
+	msg, err := server.GetBestBlock(ctx, &protoReq)
+	return msg, metadata, err
+
+}
+
+var (
+	filter_ChainKit_GetBlockHash_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
+)
+
+func request_ChainKit_GetBlockHash_0(ctx context.Context, marshaler runtime.Marshaler, client ChainKitClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq GetBlockHashRequest
+	var metadata runtime.ServerMetadata
+
+	if err := req.ParseForm(); err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+	if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ChainKit_GetBlockHash_0); err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+
+	msg, err := client.GetBlockHash(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
+	return msg, metadata, err
+
+}
+
+func local_request_ChainKit_GetBlockHash_0(ctx context.Context, marshaler runtime.Marshaler, server ChainKitServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq GetBlockHashRequest
+	var metadata runtime.ServerMetadata
+
+	if err := req.ParseForm(); err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+	if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ChainKit_GetBlockHash_0); err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+
+	msg, err := server.GetBlockHash(ctx, &protoReq)
+	return msg, metadata, err
+
+}
+
+// RegisterChainKitHandlerServer registers the http handlers for service ChainKit to "mux".
+// UnaryRPC     :call ChainKitServer directly.
+// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
+// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterChainKitHandlerFromEndpoint instead.
+func RegisterChainKitHandlerServer(ctx context.Context, mux *runtime.ServeMux, server ChainKitServer) error {
+
+	mux.Handle("GET", pattern_ChainKit_GetBlock_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		var stream runtime.ServerTransportStream
+		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/chainrpc.ChainKit/GetBlock", runtime.WithHTTPPathPattern("/v2/chainkit/block"))
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := local_request_ChainKit_GetBlock_0(rctx, inboundMarshaler, server, req, pathParams)
+		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
+		ctx = runtime.NewServerMetadataContext(ctx, md)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+
+		forward_ChainKit_GetBlock_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
+	mux.Handle("GET", pattern_ChainKit_GetBestBlock_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		var stream runtime.ServerTransportStream
+		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/chainrpc.ChainKit/GetBestBlock", runtime.WithHTTPPathPattern("/v2/chainkit/bestblock"))
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := local_request_ChainKit_GetBestBlock_0(rctx, inboundMarshaler, server, req, pathParams)
+		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
+		ctx = runtime.NewServerMetadataContext(ctx, md)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+
+		forward_ChainKit_GetBestBlock_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
+	mux.Handle("GET", pattern_ChainKit_GetBlockHash_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		var stream runtime.ServerTransportStream
+		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/chainrpc.ChainKit/GetBlockHash", runtime.WithHTTPPathPattern("/v2/chainkit/blockhash"))
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := local_request_ChainKit_GetBlockHash_0(rctx, inboundMarshaler, server, req, pathParams)
+		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
+		ctx = runtime.NewServerMetadataContext(ctx, md)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+
+		forward_ChainKit_GetBlockHash_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
+	return nil
+}
+
+// RegisterChainKitHandlerFromEndpoint is same as RegisterChainKitHandler but
+// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
+func RegisterChainKitHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
+	conn, err := grpc.Dial(endpoint, opts...)
+	if err != nil {
+		return err
+	}
+	defer func() {
+		if err != nil {
+			if cerr := conn.Close(); cerr != nil {
+				grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
+			}
+			return
+		}
+		go func() {
+			<-ctx.Done()
+			if cerr := conn.Close(); cerr != nil {
+				grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
+			}
+		}()
+	}()
+
+	return RegisterChainKitHandler(ctx, mux, conn)
+}
+
+// RegisterChainKitHandler registers the http handlers for service ChainKit to "mux".
+// The handlers forward requests to the grpc endpoint over "conn".
+func RegisterChainKitHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
+	return RegisterChainKitHandlerClient(ctx, mux, NewChainKitClient(conn))
+}
+
+// RegisterChainKitHandlerClient registers the http handlers for service ChainKit
+// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "ChainKitClient".
+// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "ChainKitClient"
+// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
+// "ChainKitClient" to call the correct interceptors.
+func RegisterChainKitHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ChainKitClient) error {
+
+	mux.Handle("GET", pattern_ChainKit_GetBlock_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/chainrpc.ChainKit/GetBlock", runtime.WithHTTPPathPattern("/v2/chainkit/block"))
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := request_ChainKit_GetBlock_0(rctx, inboundMarshaler, client, req, pathParams)
+		ctx = runtime.NewServerMetadataContext(ctx, md)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+
+		forward_ChainKit_GetBlock_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
+	mux.Handle("GET", pattern_ChainKit_GetBestBlock_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/chainrpc.ChainKit/GetBestBlock", runtime.WithHTTPPathPattern("/v2/chainkit/bestblock"))
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := request_ChainKit_GetBestBlock_0(rctx, inboundMarshaler, client, req, pathParams)
+		ctx = runtime.NewServerMetadataContext(ctx, md)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+
+		forward_ChainKit_GetBestBlock_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
+	mux.Handle("GET", pattern_ChainKit_GetBlockHash_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/chainrpc.ChainKit/GetBlockHash", runtime.WithHTTPPathPattern("/v2/chainkit/blockhash"))
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := request_ChainKit_GetBlockHash_0(rctx, inboundMarshaler, client, req, pathParams)
+		ctx = runtime.NewServerMetadataContext(ctx, md)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+
+		forward_ChainKit_GetBlockHash_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
+	return nil
+}
+
+var (
+	pattern_ChainKit_GetBlock_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v2", "chainkit", "block"}, ""))
+
+	pattern_ChainKit_GetBestBlock_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v2", "chainkit", "bestblock"}, ""))
+
+	pattern_ChainKit_GetBlockHash_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v2", "chainkit", "blockhash"}, ""))
+)
+
+var (
+	forward_ChainKit_GetBlock_0 = runtime.ForwardResponseMessage
+
+	forward_ChainKit_GetBestBlock_0 = runtime.ForwardResponseMessage
+
+	forward_ChainKit_GetBlockHash_0 = runtime.ForwardResponseMessage
+)
diff --git a/lnrpc/chainrpc/chainkit.pb.json.go b/lnrpc/chainrpc/chainkit.pb.json.go
new file mode 100644
index 000000000..e05648a2f
--- /dev/null
+++ b/lnrpc/chainrpc/chainkit.pb.json.go
@@ -0,0 +1,98 @@
+// Code generated by falafel 0.9.1. DO NOT EDIT.
+// source: chainkit.proto
+
+package chainrpc
+
+import (
+	"context"
+
+	gateway "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
+	"google.golang.org/grpc"
+	"google.golang.org/protobuf/encoding/protojson"
+)
+
+func RegisterChainKitJSONCallbacks(registry map[string]func(ctx context.Context,
+	conn *grpc.ClientConn, reqJSON string, callback func(string, error))) {
+
+	marshaler := &gateway.JSONPb{
+		MarshalOptions: protojson.MarshalOptions{
+			UseProtoNames:   true,
+			EmitUnpopulated: true,
+		},
+	}
+
+	registry["chainrpc.ChainKit.GetBlock"] = func(ctx context.Context,
+		conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
+
+		req := &GetBlockRequest{}
+		err := marshaler.Unmarshal([]byte(reqJSON), req)
+		if err != nil {
+			callback("", err)
+			return
+		}
+
+		client := NewChainKitClient(conn)
+		resp, err := client.GetBlock(ctx, req)
+		if err != nil {
+			callback("", err)
+			return
+		}
+
+		respBytes, err := marshaler.Marshal(resp)
+		if err != nil {
+			callback("", err)
+			return
+		}
+		callback(string(respBytes), nil)
+	}
+
+	registry["chainrpc.ChainKit.GetBestBlock"] = func(ctx context.Context,
+		conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
+
+		req := &GetBestBlockRequest{}
+		err := marshaler.Unmarshal([]byte(reqJSON), req)
+		if err != nil {
+			callback("", err)
+			return
+		}
+
+		client := NewChainKitClient(conn)
+		resp, err := client.GetBestBlock(ctx, req)
+		if err != nil {
+			callback("", err)
+			return
+		}
+
+		respBytes, err := marshaler.Marshal(resp)
+		if err != nil {
+			callback("", err)
+			return
+		}
+		callback(string(respBytes), nil)
+	}
+
+	registry["chainrpc.ChainKit.GetBlockHash"] = func(ctx context.Context,
+		conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
+
+		req := &GetBlockHashRequest{}
+		err := marshaler.Unmarshal([]byte(reqJSON), req)
+		if err != nil {
+			callback("", err)
+			return
+		}
+
+		client := NewChainKitClient(conn)
+		resp, err := client.GetBlockHash(ctx, req)
+		if err != nil {
+			callback("", err)
+			return
+		}
+
+		respBytes, err := marshaler.Marshal(resp)
+		if err != nil {
+			callback("", err)
+			return
+		}
+		callback(string(respBytes), nil)
+	}
+}
diff --git a/lnrpc/chainrpc/chainkit.proto b/lnrpc/chainrpc/chainkit.proto
new file mode 100644
index 000000000..8db7c52d2
--- /dev/null
+++ b/lnrpc/chainrpc/chainkit.proto
@@ -0,0 +1,59 @@
+syntax = "proto3";
+
+package chainrpc;
+
+option go_package = "github.com/lightningnetwork/lnd/lnrpc/chainrpc";
+
+// ChainKit is a service that can be used to get information from the
+// chain backend.
+service ChainKit {
+    /* lncli: `chain getblock`
+    GetBlock returns a block given the corresponding block hash.
+    */
+    rpc GetBlock (GetBlockRequest) returns (GetBlockResponse);
+
+    /* lncli: `chain getbestblock`
+    GetBestBlock returns the block hash and current height from the valid
+    most-work chain.
+    */
+    rpc GetBestBlock (GetBestBlockRequest) returns (GetBestBlockResponse);
+
+    /* lncli: `chain getblockhash`
+    GetBlockHash returns the hash of the block in the best blockchain
+    at the given height.
+    */
+    rpc GetBlockHash (GetBlockHashRequest) returns (GetBlockHashResponse);
+}
+
+message GetBlockRequest {
+    // The hash of the requested block.
+    bytes block_hash = 1;
+}
+
+// TODO(ffranr): The neutrino GetBlock response includes many
+// additional helpful fields. Consider adding them here also.
+message GetBlockResponse {
+    // The raw bytes of the requested block.
+    bytes raw_block = 1;
+}
+
+message GetBestBlockRequest {
+}
+
+message GetBestBlockResponse {
+    // The hash of the best block.
+    bytes block_hash = 1;
+
+    // The height of the best block.
+    int32 block_height = 2;
+}
+
+message GetBlockHashRequest {
+    // Block height of the target best chain block.
+    int64 block_height = 1;
+}
+
+message GetBlockHashResponse {
+    // The hash of the best block at the specified height.
+    bytes block_hash = 1;
+}
\ No newline at end of file
diff --git a/lnrpc/chainrpc/chainkit.swagger.json b/lnrpc/chainrpc/chainkit.swagger.json
new file mode 100644
index 000000000..c5bae06c0
--- /dev/null
+++ b/lnrpc/chainrpc/chainkit.swagger.json
@@ -0,0 +1,177 @@
+{
+  "swagger": "2.0",
+  "info": {
+    "title": "chainrpc/chainkit.proto",
+    "version": "version not set"
+  },
+  "tags": [
+    {
+      "name": "ChainKit"
+    }
+  ],
+  "consumes": [
+    "application/json"
+  ],
+  "produces": [
+    "application/json"
+  ],
+  "paths": {
+    "/v2/chainkit/bestblock": {
+      "get": {
+        "summary": "lncli: `chain getbestblock`\nGetBestBlock returns the block hash and current height from the valid\nmost-work chain.",
+        "operationId": "ChainKit_GetBestBlock",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/chainrpcGetBestBlockResponse"
+            }
+          },
+          "default": {
+            "description": "An unexpected error response.",
+            "schema": {
+              "$ref": "#/definitions/rpcStatus"
+            }
+          }
+        },
+        "tags": [
+          "ChainKit"
+        ]
+      }
+    },
+    "/v2/chainkit/block": {
+      "get": {
+        "summary": "lncli: `chain getblock`\nGetBlock returns a block given the corresponding block hash.",
+        "operationId": "ChainKit_GetBlock",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/chainrpcGetBlockResponse"
+            }
+          },
+          "default": {
+            "description": "An unexpected error response.",
+            "schema": {
+              "$ref": "#/definitions/rpcStatus"
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "block_hash",
+            "description": "The hash of the requested block.",
+            "in": "query",
+            "required": false,
+            "type": "string",
+            "format": "byte"
+          }
+        ],
+        "tags": [
+          "ChainKit"
+        ]
+      }
+    },
+    "/v2/chainkit/blockhash": {
+      "get": {
+        "summary": "lncli: `chain getblockhash`\nGetBlockHash returns the hash of the block in the best blockchain\nat the given height.",
+        "operationId": "ChainKit_GetBlockHash",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/chainrpcGetBlockHashResponse"
+            }
+          },
+          "default": {
+            "description": "An unexpected error response.",
+            "schema": {
+              "$ref": "#/definitions/rpcStatus"
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "block_height",
+            "description": "Block height of the target best chain block.",
+            "in": "query",
+            "required": false,
+            "type": "string",
+            "format": "int64"
+          }
+        ],
+        "tags": [
+          "ChainKit"
+        ]
+      }
+    }
+  },
+  "definitions": {
+    "chainrpcGetBestBlockResponse": {
+      "type": "object",
+      "properties": {
+        "block_hash": {
+          "type": "string",
+          "format": "byte",
+          "description": "The hash of the best block."
+        },
+        "block_height": {
+          "type": "integer",
+          "format": "int32",
+          "description": "The height of the best block."
+        }
+      }
+    },
+    "chainrpcGetBlockHashResponse": {
+      "type": "object",
+      "properties": {
+        "block_hash": {
+          "type": "string",
+          "format": "byte",
+          "description": "The hash of the best block at the specified height."
+        }
+      }
+    },
+    "chainrpcGetBlockResponse": {
+      "type": "object",
+      "properties": {
+        "raw_block": {
+          "type": "string",
+          "format": "byte",
+          "description": "The raw bytes of the requested block."
+        }
+      },
+      "description": "TODO(ffranr): The neutrino GetBlock response includes many\nadditional helpful fields. Consider adding them here also."
+    },
+    "protobufAny": {
+      "type": "object",
+      "properties": {
+        "type_url": {
+          "type": "string"
+        },
+        "value": {
+          "type": "string",
+          "format": "byte"
+        }
+      }
+    },
+    "rpcStatus": {
+      "type": "object",
+      "properties": {
+        "code": {
+          "type": "integer",
+          "format": "int32"
+        },
+        "message": {
+          "type": "string"
+        },
+        "details": {
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/protobufAny"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lnrpc/chainrpc/chainkit.yaml b/lnrpc/chainrpc/chainkit.yaml
new file mode 100644
index 000000000..815dfc5a7
--- /dev/null
+++ b/lnrpc/chainrpc/chainkit.yaml
@@ -0,0 +1,11 @@
+type: google.api.Service
+config_version: 3
+
+http:
+  rules:
+    - selector: chainrpc.ChainKit.GetBlock
+      get: "/v2/chainkit/block"
+    - selector: chainrpc.ChainKit.GetBestBlock
+      get: "/v2/chainkit/bestblock"
+    - selector: chainrpc.ChainKit.GetBlockHash
+      get: "/v2/chainkit/blockhash"
\ No newline at end of file
diff --git a/lnrpc/chainrpc/chainkit_grpc.pb.go b/lnrpc/chainrpc/chainkit_grpc.pb.go
new file mode 100644
index 000000000..47585f7f3
--- /dev/null
+++ b/lnrpc/chainrpc/chainkit_grpc.pb.go
@@ -0,0 +1,189 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+
+package chainrpc
+
+import (
+	context "context"
+	grpc "google.golang.org/grpc"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+// ChainKitClient is the client API for ChainKit service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type ChainKitClient interface {
+	// lncli: `chain getblock`
+	// GetBlock returns a block given the corresponding block hash.
+	GetBlock(ctx context.Context, in *GetBlockRequest, opts ...grpc.CallOption) (*GetBlockResponse, error)
+	// lncli: `chain getbestblock`
+	// GetBestBlock returns the block hash and current height from the valid
+	// most-work chain.
+	GetBestBlock(ctx context.Context, in *GetBestBlockRequest, opts ...grpc.CallOption) (*GetBestBlockResponse, error)
+	// lncli: `chain getblockhash`
+	// GetBlockHash returns the hash of the block in the best blockchain
+	// at the given height.
+	GetBlockHash(ctx context.Context, in *GetBlockHashRequest, opts ...grpc.CallOption) (*GetBlockHashResponse, error)
+}
+
+type chainKitClient struct {
+	cc grpc.ClientConnInterface
+}
+
+func NewChainKitClient(cc grpc.ClientConnInterface) ChainKitClient {
+	return &chainKitClient{cc}
+}
+
+func (c *chainKitClient) GetBlock(ctx context.Context, in *GetBlockRequest, opts ...grpc.CallOption) (*GetBlockResponse, error) {
+	out := new(GetBlockResponse)
+	err := c.cc.Invoke(ctx, "/chainrpc.ChainKit/GetBlock", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *chainKitClient) GetBestBlock(ctx context.Context, in *GetBestBlockRequest, opts ...grpc.CallOption) (*GetBestBlockResponse, error) {
+	out := new(GetBestBlockResponse)
+	err := c.cc.Invoke(ctx, "/chainrpc.ChainKit/GetBestBlock", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *chainKitClient) GetBlockHash(ctx context.Context, in *GetBlockHashRequest, opts ...grpc.CallOption) (*GetBlockHashResponse, error) {
+	out := new(GetBlockHashResponse)
+	err := c.cc.Invoke(ctx, "/chainrpc.ChainKit/GetBlockHash", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+// ChainKitServer is the server API for ChainKit service.
+// All implementations must embed UnimplementedChainKitServer
+// for forward compatibility
+type ChainKitServer interface {
+	// lncli: `chain getblock`
+	// GetBlock returns a block given the corresponding block hash.
+	GetBlock(context.Context, *GetBlockRequest) (*GetBlockResponse, error)
+	// lncli: `chain getbestblock`
+	// GetBestBlock returns the block hash and current height from the valid
+	// most-work chain.
+	GetBestBlock(context.Context, *GetBestBlockRequest) (*GetBestBlockResponse, error)
+	// lncli: `chain getblockhash`
+	// GetBlockHash returns the hash of the block in the best blockchain
+	// at the given height.
+	GetBlockHash(context.Context, *GetBlockHashRequest) (*GetBlockHashResponse, error)
+	mustEmbedUnimplementedChainKitServer()
+}
+
+// UnimplementedChainKitServer must be embedded to have forward compatible implementations.
+type UnimplementedChainKitServer struct {
+}
+
+func (UnimplementedChainKitServer) GetBlock(context.Context, *GetBlockRequest) (*GetBlockResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method GetBlock not implemented")
+}
+func (UnimplementedChainKitServer) GetBestBlock(context.Context, *GetBestBlockRequest) (*GetBestBlockResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method GetBestBlock not implemented")
+}
+func (UnimplementedChainKitServer) GetBlockHash(context.Context, *GetBlockHashRequest) (*GetBlockHashResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method GetBlockHash not implemented")
+}
+func (UnimplementedChainKitServer) mustEmbedUnimplementedChainKitServer() {}
+
+// UnsafeChainKitServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to ChainKitServer will
+// result in compilation errors.
+type UnsafeChainKitServer interface {
+	mustEmbedUnimplementedChainKitServer()
+}
+
+func RegisterChainKitServer(s grpc.ServiceRegistrar, srv ChainKitServer) {
+	s.RegisterService(&ChainKit_ServiceDesc, srv)
+}
+
+func _ChainKit_GetBlock_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(GetBlockRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(ChainKitServer).GetBlock(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/chainrpc.ChainKit/GetBlock",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(ChainKitServer).GetBlock(ctx, req.(*GetBlockRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _ChainKit_GetBestBlock_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(GetBestBlockRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(ChainKitServer).GetBestBlock(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/chainrpc.ChainKit/GetBestBlock",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(ChainKitServer).GetBestBlock(ctx, req.(*GetBestBlockRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _ChainKit_GetBlockHash_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(GetBlockHashRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(ChainKitServer).GetBlockHash(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/chainrpc.ChainKit/GetBlockHash",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(ChainKitServer).GetBlockHash(ctx, req.(*GetBlockHashRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+// ChainKit_ServiceDesc is the grpc.ServiceDesc for ChainKit service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var ChainKit_ServiceDesc = grpc.ServiceDesc{
+	ServiceName: "chainrpc.ChainKit",
+	HandlerType: (*ChainKitServer)(nil),
+	Methods: []grpc.MethodDesc{
+		{
+			MethodName: "GetBlock",
+			Handler:    _ChainKit_GetBlock_Handler,
+		},
+		{
+			MethodName: "GetBestBlock",
+			Handler:    _ChainKit_GetBestBlock_Handler,
+		},
+		{
+			MethodName: "GetBlockHash",
+			Handler:    _ChainKit_GetBlockHash_Handler,
+		},
+	},
+	Streams:  []grpc.StreamDesc{},
+	Metadata: "chainrpc/chainkit.proto",
+}
diff --git a/lnrpc/chainrpc/config_active.go b/lnrpc/chainrpc/config_active.go
index 636798c72..c84e1ba49 100644
--- a/lnrpc/chainrpc/config_active.go
+++ b/lnrpc/chainrpc/config_active.go
@@ -5,6 +5,7 @@ package chainrpc
 
 import (
 	"github.com/lightningnetwork/lnd/chainntnfs"
+	"github.com/lightningnetwork/lnd/lnwallet"
 	"github.com/lightningnetwork/lnd/macaroons"
 )
 
@@ -32,4 +33,7 @@ type Config struct {
 	// notifier RPC server. The job of the chain notifier RPC server is
 	// simply to proxy valid requests to the active chain notifier instance.
 	ChainNotifier chainntnfs.ChainNotifier
+
+	// Chain provides access to the most up-to-date blockchain data.
+	Chain lnwallet.BlockChainIO
 }
diff --git a/lnrpc/chainrpc/driver.go b/lnrpc/chainrpc/driver.go
index c25ffbff4..89a6a116a 100644
--- a/lnrpc/chainrpc/driver.go
+++ b/lnrpc/chainrpc/driver.go
@@ -45,9 +45,14 @@ func createNewSubServer(configRegistry lnrpc.SubServerConfigDispatcher) (
 	case config.MacService != nil && config.NetworkDir == "":
 		return nil, nil, fmt.Errorf("NetworkDir must be set to create " +
 			"chainrpc")
+
 	case config.ChainNotifier == nil:
 		return nil, nil, fmt.Errorf("ChainNotifier must be set to " +
 			"create chainrpc")
+
+	case config.Chain == nil:
+		return nil, nil, fmt.Errorf("field Chain must be set to " +
+			"create chainrpc")
 	}
 
 	return New(config)
diff --git a/lnrpc/gen_protos.sh b/lnrpc/gen_protos.sh
index 83f9871cc..9919c11b5 100755
--- a/lnrpc/gen_protos.sh
+++ b/lnrpc/gen_protos.sh
@@ -67,7 +67,7 @@ function generate() {
       --plugin=protoc-gen-custom=$falafel\
       --custom_out=. \
       --custom_opt="$opts" \
-      "$(find . -name '*.proto')"
+      $(find . -name '*.proto')
     popd
   done
 }
diff --git a/lntemp/rpc/harness_rpc.go b/lntemp/rpc/harness_rpc.go
index ca566035e..855227c60 100644
--- a/lntemp/rpc/harness_rpc.go
+++ b/lntemp/rpc/harness_rpc.go
@@ -39,6 +39,7 @@ type HarnessRPC struct {
 	WatchtowerClient wtclientrpc.WatchtowerClientClient
 	State            lnrpc.StateClient
 	ChainClient      chainrpc.ChainNotifierClient
+	ChainKit         chainrpc.ChainKitClient
 	Peer             peersrpc.PeersClient
 
 	// Name is the HarnessNode's name.
@@ -68,6 +69,7 @@ func NewHarnessRPC(ctxt context.Context, t *testing.T, c *grpc.ClientConn,
 		Signer:           signrpc.NewSignerClient(c),
 		State:            lnrpc.NewStateClient(c),
 		ChainClient:      chainrpc.NewChainNotifierClient(c),
+		ChainKit:         chainrpc.NewChainKitClient(c),
 		Peer:             peersrpc.NewPeersClient(c),
 		Name:             name,
 	}
diff --git a/routing/control_tower.go b/routing/control_tower.go
index a0c5b1df7..9f95bb252 100644
--- a/routing/control_tower.go
+++ b/routing/control_tower.go
@@ -46,13 +46,13 @@ type ControlTower interface {
 	// hash.
 	FetchPayment(paymentHash lntypes.Hash) (*channeldb.MPPayment, error)
 
-	// FailPayment transitions a payment into the Failed state, and records
-	// the ultimate reason the payment failed. Note that this should only
-	// be called when all active attempts are already failed. After
+	// Fail transitions a payment into the Failed state, and records the
+	// ultimate reason the payment failed. Note that this should only be
+	// called when all active active attempts are already failed. After
 	// invoking this method, InitPayment should return nil on its next call
 	// for this payment hash, allowing the user to make a subsequent
 	// payment.
-	FailPayment(lntypes.Hash, channeldb.FailureReason) error
+	Fail(lntypes.Hash, channeldb.FailureReason) error
 
 	// FetchInFlightPayments returns all payments with status InFlight.
 	FetchInFlightPayments() ([]*channeldb.MPPayment, error)
@@ -229,11 +229,11 @@ func (p *controlTower) FetchPayment(paymentHash lntypes.Hash) (
 	return p.db.FetchPayment(paymentHash)
 }
 
-// FailPayment transitions a payment into the Failed state, and records the
-// reason the payment failed. After invoking this method, InitPayment should
-// return nil on its next call for this payment hash, allowing the switch to
-// make a subsequent payment.
-func (p *controlTower) FailPayment(paymentHash lntypes.Hash,
+// Fail transitions a payment into the Failed state, and records the reason the
+// payment failed. After invoking this method, InitPayment should return nil on
+// its next call for this payment hash, allowing the switch to make a
+// subsequent payment.
+func (p *controlTower) Fail(paymentHash lntypes.Hash,
 	reason channeldb.FailureReason) error {
 
 	p.paymentsMtx.Lock(paymentHash)
diff --git a/routing/payment_lifecycle.go b/routing/payment_lifecycle.go
index 32f0d811d..b87dddef1 100644
--- a/routing/payment_lifecycle.go
+++ b/routing/payment_lifecycle.go
@@ -23,6 +23,7 @@ var errShardHandlerExiting = fmt.Errorf("shard handler exiting")
 // needed to resume if from any point.
 type paymentLifecycle struct {
 	router        *ChannelRouter
+	totalAmount   lnwire.MilliSatoshi
 	feeLimit      lnwire.MilliSatoshi
 	identifier    lntypes.Hash
 	paySession    PaymentSession
@@ -82,10 +83,9 @@ func (p *paymentLifecycle) fetchPaymentState() (*channeldb.MPPayment,
 	sentAmt, fees := payment.SentAmt()
 
 	// Sanity check we haven't sent a value larger than the payment amount.
-	totalAmt := payment.Info.Value
-	if sentAmt > totalAmt {
+	if sentAmt > p.totalAmount {
 		return nil, nil, fmt.Errorf("amount sent %v exceeds "+
-			"total amount %v", sentAmt, totalAmt)
+			"total amount %v", sentAmt, p.totalAmount)
 	}
 
 	// We'll subtract the used fee from our fee budget, but allow the fees
@@ -109,7 +109,7 @@ func (p *paymentLifecycle) fetchPaymentState() (*channeldb.MPPayment,
 	// Update the payment state.
 	state := &paymentState{
 		numShardsInFlight: len(payment.InFlightHTLCs()),
-		remainingAmt:      totalAmt - sentAmt,
+		remainingAmt:      p.totalAmount - sentAmt,
 		remainingFees:     feeBudget,
 		terminate:         terminate,
 	}
@@ -233,7 +233,7 @@ lifecycle:
 			// tower, no further shards will be launched and we'll
 			// return with an error the moment all active shards
 			// have finished.
-			saveErr := p.router.cfg.Control.FailPayment(
+			saveErr := p.router.cfg.Control.Fail(
 				p.identifier, channeldb.FailureReasonTimeout,
 			)
 			if saveErr != nil {
@@ -273,7 +273,7 @@ lifecycle:
 					"failed with no route: %v",
 					p.identifier, failureCode)
 
-				saveErr := p.router.cfg.Control.FailPayment(
+				saveErr := p.router.cfg.Control.Fail(
 					p.identifier, failureCode,
 				)
 				if saveErr != nil {
@@ -337,6 +337,7 @@ lifecycle:
 		// Now that the shard was successfully sent, launch a go
 		// routine that will handle its result when its back.
 		shardHandler.collectResultAsync(attempt)
+
 	}
 }
 
@@ -562,7 +563,7 @@ func (p *shardHandler) collectResult(attempt *channeldb.HTLCAttemptInfo) (
 
 	// Now ask the switch to return the result of the payment when
 	// available.
-	resultChan, err := p.router.cfg.Payer.GetAttemptResult(
+	resultChan, err := p.router.cfg.Payer.GetPaymentResult(
 		attempt.AttemptID, p.identifier, errorDecryptor,
 	)
 	switch {
@@ -784,7 +785,7 @@ func (p *shardHandler) handleSendError(attempt *channeldb.HTLCAttemptInfo,
 			p.identifier, *reason, sendErr)
 
 		// Fail the payment via control tower.
-		if err := p.router.cfg.Control.FailPayment(
+		if err := p.router.cfg.Control.Fail(
 			p.identifier, *reason,
 		); err != nil {
 			log.Errorf("unable to report failure to control "+
diff --git a/routing/router.go b/routing/router.go
index 01d4b5022..cf6357c68 100644
--- a/routing/router.go
+++ b/routing/router.go
@@ -169,7 +169,7 @@ type PaymentAttemptDispatcher interface {
 		attemptID uint64,
 		htlcAdd *lnwire.UpdateAddHTLC) error
 
-	// GetAttemptResult returns the result of the payment attempt with
+	// GetPaymentResult returns the result of the payment attempt with
 	// the given attemptID. The paymentHash should be set to the payment's
 	// overall hash, or in case of AMP payments the payment's unique
 	// identifier.
@@ -180,7 +180,7 @@ type PaymentAttemptDispatcher interface {
 	// longer be in flight.  The switch shutting down is signaled by
 	// closing the channel. If the attemptID is unknown,
 	// ErrPaymentIDNotFound will be returned.
-	GetAttemptResult(attemptID uint64, paymentHash lntypes.Hash,
+	GetPaymentResult(attemptID uint64, paymentHash lntypes.Hash,
 		deobfuscator htlcswitch.ErrorDecrypter) (
 		<-chan *htlcswitch.PaymentResult, error)
 
@@ -667,8 +667,9 @@ func (r *ChannelRouter) Start() error {
 			// also set a zero fee limit, as no more routes should
 			// be tried.
 			_, _, err := r.sendPayment(
-				0, payment.Info.PaymentIdentifier, 0,
-				paySession, shardTracker,
+				payment.Info.Value, 0,
+				payment.Info.PaymentIdentifier, 0, paySession,
+				shardTracker,
 			)
 			if err != nil {
 				log.Errorf("Resuming payment %v failed: %v.",
@@ -2047,7 +2048,7 @@ func (r *ChannelRouter) SendPayment(payment *LightningPayment) ([32]byte,
 	// Since this is the first time this payment is being made, we pass nil
 	// for the existing attempt.
 	return r.sendPayment(
-		payment.FeeLimit, payment.Identifier(),
+		payment.Amount, payment.FeeLimit, payment.Identifier(),
 		payment.PayAttemptTimeout, paySession, shardTracker,
 	)
 }
@@ -2070,7 +2071,7 @@ func (r *ChannelRouter) SendPaymentAsync(payment *LightningPayment) error {
 			spewPayment(payment))
 
 		_, _, err := r.sendPayment(
-			payment.FeeLimit, payment.Identifier(),
+			payment.Amount, payment.FeeLimit, payment.Identifier(),
 			payment.PayAttemptTimeout, paySession, shardTracker,
 		)
 		if err != nil {
@@ -2252,7 +2253,7 @@ func (r *ChannelRouter) sendToRoute(htlcHash lntypes.Hash, rt *route.Route,
 		log.Debugf("Invalid route provided for payment %x: %v",
 			paymentIdentifier, err)
 
-		controlErr := r.cfg.Control.FailPayment(
+		controlErr := r.cfg.Control.Fail(
 			paymentIdentifier, channeldb.FailureReasonError,
 		)
 		if controlErr != nil {
@@ -2300,16 +2301,14 @@ func (r *ChannelRouter) sendToRoute(htlcHash lntypes.Hash, rt *route.Route,
 	// If a non-terminal error is returned and `skipTempErr` is false, then
 	// we'll use the normal no route error.
 	case err == nil && !skipTempErr:
-		err = r.cfg.Control.FailPayment(
+		err = r.cfg.Control.Fail(
 			paymentIdentifier, channeldb.FailureReasonNoRoute,
 		)
 
 	// If this is a failure reason, then we'll apply the failure directly
 	// to the control tower, and return the normal response to the caller.
 	case goErrors.As(err, &failureReason):
-		err = r.cfg.Control.FailPayment(
-			paymentIdentifier, *failureReason,
-		)
+		err = r.cfg.Control.Fail(paymentIdentifier, *failureReason)
 	}
 	if err != nil {
 		return nil, err
@@ -2334,9 +2333,9 @@ func (r *ChannelRouter) sendToRoute(htlcHash lntypes.Hash, rt *route.Route,
 // carry out its execution. After restarts it is safe, and assumed, that the
 // router will call this method for every payment still in-flight according to
 // the ControlTower.
-func (r *ChannelRouter) sendPayment(feeLimit lnwire.MilliSatoshi,
-	identifier lntypes.Hash, timeout time.Duration,
-	paySession PaymentSession,
+func (r *ChannelRouter) sendPayment(
+	totalAmt, feeLimit lnwire.MilliSatoshi, identifier lntypes.Hash,
+	timeout time.Duration, paySession PaymentSession,
 	shardTracker shards.ShardTracker) ([32]byte, *route.Route, error) {
 
 	// We'll also fetch the current block height so we can properly
@@ -2350,6 +2349,7 @@ func (r *ChannelRouter) sendPayment(feeLimit lnwire.MilliSatoshi,
 	// can resume the payment from the current state.
 	p := &paymentLifecycle{
 		router:        r,
+		totalAmount:   totalAmt,
 		feeLimit:      feeLimit,
 		identifier:    identifier,
 		paySession:    paySession,
diff --git a/subrpcserver_config.go b/subrpcserver_config.go
index 8911d1eb3..2723efaea 100644
--- a/subrpcserver_config.go
+++ b/subrpcserver_config.go
@@ -217,6 +217,9 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config,
 			subCfgValue.FieldByName("ChainNotifier").Set(
 				reflect.ValueOf(cc.ChainNotifier),
 			)
+			subCfgValue.FieldByName("Chain").Set(
+				reflect.ValueOf(cc.ChainIO),
+			)
 
 		case *invoicesrpc.Config:
 			subCfgValue := extractReflectValue(subCfg)
diff --git a/tools/go.mod b/tools/go.mod
index 9662a0b37..4859b68b0 100644
--- a/tools/go.mod
+++ b/tools/go.mod
@@ -5,6 +5,6 @@ go 1.16
 require (
 	github.com/btcsuite/btcd v0.23.3
 	github.com/golangci/golangci-lint v1.50.1
-	github.com/ory/go-acc v0.2.8
+	github.com/ory/go-acc v0.2.6
 	github.com/rinchsan/gosimports v0.1.5
 )
diff --git a/tools/go.sum b/tools/go.sum
index d36525869..def758fc5 100644
--- a/tools/go.sum
+++ b/tools/go.sum
@@ -725,8 +725,6 @@ github.com/onsi/gomega v1.20.0/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeR
 github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
 github.com/ory/go-acc v0.2.6 h1:YfI+L9dxI7QCtWn2RbawqO0vXhiThdXu/RgizJBbaq0=
 github.com/ory/go-acc v0.2.6/go.mod h1:4Kb/UnPcT8qRAk3IAxta+hvVapdxTLWtrr7bFLlEgpw=
-github.com/ory/go-acc v0.2.8 h1:rOHHAPQjf0u7eHFGWpiXK+gIu/e0GRSJNr9pDukdNC4=
-github.com/ory/go-acc v0.2.8/go.mod h1:iCRZUdGb/7nqvSn8xWZkhfVrtXRZ9Wru2E5rabCjFPI=
 github.com/ory/viper v1.7.5 h1:+xVdq7SU3e1vNaCsk/ixsfxE4zylk1TJUiJrY647jUE=
 github.com/ory/viper v1.7.5/go.mod h1:ypOuyJmEUb3oENywQZRgeAMwqgOyDqwboO1tj3DjTaM=
 github.com/otiai10/copy v1.2.0 h1:HvG945u96iNadPoG2/Ja2+AUJeW5YuFQMixq9yirC+k=


Test patch:
diff --git a/htlcswitch/link_test.go b/htlcswitch/link_test.go
index bf3916651..cd3890d17 100644
--- a/htlcswitch/link_test.go
+++ b/htlcswitch/link_test.go
@@ -1306,7 +1306,7 @@ func TestChannelLinkMultiHopUnknownPaymentHash(t *testing.T) {
 	)
 	require.NoError(t, err, "unable to get send payment")
 
-	resultChan, err := n.aliceServer.htlcSwitch.GetAttemptResult(
+	resultChan, err := n.aliceServer.htlcSwitch.GetPaymentResult(
 		pid, htlc.PaymentHash, newMockDeobfuscator(),
 	)
 	require.NoError(t, err, "unable to get payment result")
@@ -3979,7 +3979,7 @@ func TestChannelLinkAcceptDuplicatePayment(t *testing.T) {
 	)
 	require.NoError(t, err, "unable to send payment to carol")
 
-	resultChan, err := n.aliceServer.htlcSwitch.GetAttemptResult(
+	resultChan, err := n.aliceServer.htlcSwitch.GetPaymentResult(
 		pid, htlc.PaymentHash, newMockDeobfuscator(),
 	)
 	require.NoError(t, err, "unable to get payment result")
diff --git a/htlcswitch/switch_test.go b/htlcswitch/switch_test.go
index a8c7aa4ab..4d666b760 100644
--- a/htlcswitch/switch_test.go
+++ b/htlcswitch/switch_test.go
@@ -2699,7 +2699,7 @@ func TestSwitchSendPayment(t *testing.T) {
 
 	// First check that the switch will correctly respond that this payment
 	// ID is unknown.
-	_, err = s.GetAttemptResult(
+	_, err = s.GetPaymentResult(
 		paymentID, rhash, newMockDeobfuscator(),
 	)
 	if err != ErrPaymentIDNotFound {
@@ -2717,7 +2717,7 @@ func TestSwitchSendPayment(t *testing.T) {
 			return
 		}
 
-		resultChan, err := s.GetAttemptResult(
+		resultChan, err := s.GetPaymentResult(
 			paymentID, rhash, newMockDeobfuscator(),
 		)
 		if err != nil {
@@ -3072,11 +3072,11 @@ func TestUpdateFailMalformedHTLCErrorConversion(t *testing.T) {
 	})
 }
 
-// TestSwitchGetAttemptResult tests that the switch interacts as expected with
+// TestSwitchGetPaymentResult tests that the switch interacts as expected with
 // the circuit map and network result store when looking up the result of a
 // payment ID. This is important for not to lose results under concurrent
 // lookup and receiving results.
-func TestSwitchGetAttemptResult(t *testing.T) {
+func TestSwitchGetPaymentResult(t *testing.T) {
 	t.Parallel()
 
 	const paymentID = 123
@@ -3100,7 +3100,7 @@ func TestSwitchGetAttemptResult(t *testing.T) {
 	// added anything to the store yet, ErrPaymentIDNotFound should be
 	// returned.
 	lookup <- nil
-	_, err = s.GetAttemptResult(
+	_, err = s.GetPaymentResult(
 		paymentID, lntypes.Hash{}, newMockDeobfuscator(),
 	)
 	if err != ErrPaymentIDNotFound {
@@ -3110,7 +3110,7 @@ func TestSwitchGetAttemptResult(t *testing.T) {
 	// Next let the lookup find the circuit in the circuit map. It should
 	// subscribe to payment results, and return the result when available.
 	lookup <- &PaymentCircuit{}
-	resultChan, err := s.GetAttemptResult(
+	resultChan, err := s.GetPaymentResult(
 		paymentID, lntypes.Hash{}, newMockDeobfuscator(),
 	)
 	require.NoError(t, err, "unable to get payment result")
@@ -3151,7 +3151,7 @@ func TestSwitchGetAttemptResult(t *testing.T) {
 	// in the circuit map, it should be immediately available from the
 	// store.
 	lookup <- nil
-	resultChan, err = s.GetAttemptResult(
+	resultChan, err = s.GetPaymentResult(
 		paymentID, lntypes.Hash{}, newMockDeobfuscator(),
 	)
 	require.NoError(t, err, "unable to get payment result")
@@ -3257,7 +3257,7 @@ func TestInvalidFailure(t *testing.T) {
 		},
 	}
 
-	resultChan, err := s.GetAttemptResult(
+	resultChan, err := s.GetPaymentResult(
 		paymentID, rhash, &deobfuscator,
 	)
 	if err != nil {
@@ -3283,7 +3283,7 @@ func TestInvalidFailure(t *testing.T) {
 		},
 	}
 
-	resultChan, err = s.GetAttemptResult(
+	resultChan, err = s.GetPaymentResult(
 		paymentID, rhash, &deobfuscator,
 	)
 	if err != nil {
@@ -4415,7 +4415,7 @@ func TestSwitchDustForwarding(t *testing.T) {
 	require.NoError(t, err)
 	carolAttemptID++
 
-	carolResultChan, err := n.carolServer.htlcSwitch.GetAttemptResult(
+	carolResultChan, err := n.carolServer.htlcSwitch.GetPaymentResult(
 		uint64(carolAttemptID-1), carolHash, newMockDeobfuscator(),
 	)
 	require.NoError(t, err)
diff --git a/htlcswitch/test_utils.go b/htlcswitch/test_utils.go
index b9ba325ab..e4568fc0e 100644
--- a/htlcswitch/test_utils.go
+++ b/htlcswitch/test_utils.go
@@ -787,7 +787,7 @@ func preparePayment(sendingPeer, receivingPeer lnpeer.Peer,
 		if err != nil {
 			return err
 		}
-		resultChan, err := sender.htlcSwitch.GetAttemptResult(
+		resultChan, err := sender.htlcSwitch.GetPaymentResult(
 			pid, hash, newMockDeobfuscator(),
 		)
 		if err != nil {
@@ -1345,7 +1345,7 @@ func (n *twoHopNetwork) makeHoldPayment(sendingPeer, receivingPeer lnpeer.Peer,
 	}
 
 	go func() {
-		resultChan, err := sender.htlcSwitch.GetAttemptResult(
+		resultChan, err := sender.htlcSwitch.GetPaymentResult(
 			pid, rhash, newMockDeobfuscator(),
 		)
 		if err != nil {
diff --git a/lntest/harness_node.go b/lntest/harness_node.go
index 3b1e8bebc..74bb544f1 100644
--- a/lntest/harness_node.go
+++ b/lntest/harness_node.go
@@ -367,6 +367,7 @@ type HarnessNode struct {
 	WatchtowerClient wtclientrpc.WatchtowerClientClient
 	StateClient      lnrpc.StateClient
 	ChainClient      chainrpc.ChainNotifierClient
+	ChainKit         chainrpc.ChainKitClient
 }
 
 // RPCClients wraps a list of RPC clients into a single struct for easier
@@ -385,6 +386,7 @@ type RPCClients struct {
 	WatchtowerClient wtclientrpc.WatchtowerClientClient
 	State            lnrpc.StateClient
 	ChainClient      chainrpc.ChainNotifierClient
+	ChainKit         chainrpc.ChainKitClient
 }
 
 // Assert *HarnessNode implements the lnrpc.LightningClient interface.
@@ -938,6 +940,7 @@ func (hn *HarnessNode) InitRPCClients(c *grpc.ClientConn) {
 		Signer:           signrpc.NewSignerClient(c),
 		State:            lnrpc.NewStateClient(c),
 		ChainClient:      chainrpc.NewChainNotifierClient(c),
+		ChainKit:         chainrpc.NewChainKitClient(c),
 	}
 }
 
@@ -960,6 +963,7 @@ func (hn *HarnessNode) initLightningClient() error {
 	hn.PeersClient = peersrpc.NewPeersClient(conn)
 	hn.StateClient = lnrpc.NewStateClient(conn)
 	hn.ChainClient = chainrpc.NewChainNotifierClient(conn)
+	hn.ChainKit = chainrpc.NewChainKitClient(conn)
 
 	// Wait until the server is fully started.
 	if err := hn.WaitUntilServerActive(); err != nil {
diff --git a/lntest/itest/list_on_test.go b/lntest/itest/list_on_test.go
index c8a2d0266..d08aab674 100644
--- a/lntest/itest/list_on_test.go
+++ b/lntest/itest/list_on_test.go
@@ -219,4 +219,8 @@ var allTestCasesTemp = []*lntemp.TestCase{
 		Name:     "failing link",
 		TestFunc: testFailingChannel,
 	},
+	{
+		Name:     "chain kit",
+		TestFunc: testChainKit,
+	},
 }
diff --git a/lntest/itest/lnd_onchain_test.go b/lntest/itest/lnd_onchain_test.go
index add35d8a0..31af68363 100644
--- a/lntest/itest/lnd_onchain_test.go
+++ b/lntest/itest/lnd_onchain_test.go
@@ -12,7 +12,9 @@ import (
 	"github.com/btcsuite/btcd/txscript"
 	"github.com/btcsuite/btcd/wire"
 	"github.com/lightningnetwork/lnd/lnrpc"
+	"github.com/lightningnetwork/lnd/lnrpc/chainrpc"
 	"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
+	"github.com/lightningnetwork/lnd/lntemp"
 	"github.com/lightningnetwork/lnd/lntest"
 	"github.com/lightningnetwork/lnd/lntest/wait"
 	"github.com/lightningnetwork/lnd/lnwallet"
@@ -20,6 +22,72 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+// testChainKit tests ChainKit RPC endpoints.
+func testChainKit(ht *lntemp.HarnessTest) {
+	ctx := context.Background()
+
+	// Test functions registered as test cases spin up separate nodes during
+	// execution. By calling sub-test functions as seen below we avoid the
+	// need to start separate nodes.
+	testChainKitGetBlock(ctx, ht)
+	testChainKitGetBlockHash(ctx, ht)
+}
+
+// testChainKitGetBlock ensures that given a block hash, the RPC endpoint
+// returns the correct target block.
+func testChainKitGetBlock(ctx context.Context, ht *lntemp.HarnessTest) {
+	// Get best block hash.
+	bestBlockRes, err := ht.Alice.RPC.ChainKit.GetBestBlock(
+		ctx, &chainrpc.GetBestBlockRequest{},
+	)
+	require.NoError(ht, err)
+	var bestBlockHash chainhash.Hash
+	err = bestBlockHash.SetBytes(bestBlockRes.BlockHash)
+	require.NoError(ht, err)
+
+	// Retrieve the best block by hash.
+	getBlockRes, err := ht.Alice.RPC.ChainKit.GetBlock(
+		ctx, &chainrpc.GetBlockRequest{
+			BlockHash: bestBlockHash.CloneBytes(),
+		},
+	)
+	require.NoError(ht, err)
+
+	// Deserialize the block which was retrieved by hash.
+	msgBlock := &wire.MsgBlock{}
+	blockReader := bytes.NewReader(getBlockRes.RawBlock)
+	err = msgBlock.Deserialize(blockReader)
+	require.NoError(ht, err)
+
+	// Ensure best block hash is the same as retrieved block hash.
+	expected := bestBlockHash
+	actual := msgBlock.BlockHash()
+	require.Equal(ht, expected, actual)
+}
+
+// testChainKitGetBlockHash ensures that given a block height, the RPC endpoint
+// returns the correct target block hash.
+func testChainKitGetBlockHash(ctx context.Context, ht *lntemp.HarnessTest) {
+	// Get best block hash.
+	bestBlockRes, err := ht.Alice.RPC.ChainKit.GetBestBlock(
+		ctx, &chainrpc.GetBestBlockRequest{},
+	)
+	require.NoError(ht, err)
+
+	// Retrieve the block hash at best block height.
+	getBlockHashRes, err := ht.Alice.RPC.ChainKit.GetBlockHash(
+		ctx, &chainrpc.GetBlockHashRequest{
+			BlockHeight: int64(bestBlockRes.BlockHeight),
+		},
+	)
+	require.NoError(ht, err)
+
+	// Ensure best block hash is the same as retrieved block hash.
+	expected := bestBlockRes.BlockHash
+	actual := getBlockHashRes.BlockHash
+	require.Equal(ht, expected, actual)
+}
+
 // testCPFP ensures that the daemon can bump an unconfirmed  transaction's fee
 // rate by broadcasting a Child-Pays-For-Parent (CPFP) transaction.
 //
diff --git a/routing/control_tower_test.go b/routing/control_tower_test.go
index 05f049fa2..3dd2c5234 100644
--- a/routing/control_tower_test.go
+++ b/routing/control_tower_test.go
@@ -457,10 +457,7 @@ func testPaymentControlSubscribeFail(t *testing.T, registerAttempt,
 	}
 
 	// Mark the payment as failed.
-	err = pControl.FailPayment(
-		info.PaymentIdentifier, channeldb.FailureReasonTimeout,
-	)
-	if err != nil {
+	if err := pControl.Fail(info.PaymentIdentifier, channeldb.FailureReasonTimeout); err != nil {
 		t.Fatal(err)
 	}
 
diff --git a/routing/mock_test.go b/routing/mock_test.go
index afd0126dd..b0a9ba4ff 100644
--- a/routing/mock_test.go
+++ b/routing/mock_test.go
@@ -56,7 +56,7 @@ func (m *mockPaymentAttemptDispatcherOld) SendHTLC(
 	return nil
 }
 
-func (m *mockPaymentAttemptDispatcherOld) GetAttemptResult(paymentID uint64,
+func (m *mockPaymentAttemptDispatcherOld) GetPaymentResult(paymentID uint64,
 	_ lntypes.Hash, _ htlcswitch.ErrorDecrypter) (
 	<-chan *htlcswitch.PaymentResult, error) {
 
@@ -205,7 +205,7 @@ func (m *mockPayerOld) SendHTLC(_ lnwire.ShortChannelID,
 
 }
 
-func (m *mockPayerOld) GetAttemptResult(paymentID uint64, _ lntypes.Hash,
+func (m *mockPayerOld) GetPaymentResult(paymentID uint64, _ lntypes.Hash,
 	_ htlcswitch.ErrorDecrypter) (<-chan *htlcswitch.PaymentResult, error) {
 
 	select {
@@ -469,7 +469,7 @@ func (m *mockControlTowerOld) FailAttempt(phash lntypes.Hash, pid uint64,
 	return nil, fmt.Errorf("pid not found")
 }
 
-func (m *mockControlTowerOld) FailPayment(phash lntypes.Hash,
+func (m *mockControlTowerOld) Fail(phash lntypes.Hash,
 	reason channeldb.FailureReason) error {
 
 	m.Lock()
@@ -578,7 +578,7 @@ func (m *mockPaymentAttemptDispatcher) SendHTLC(firstHop lnwire.ShortChannelID,
 	return args.Error(0)
 }
 
-func (m *mockPaymentAttemptDispatcher) GetAttemptResult(attemptID uint64,
+func (m *mockPaymentAttemptDispatcher) GetPaymentResult(attemptID uint64,
 	paymentHash lntypes.Hash, deobfuscator htlcswitch.ErrorDecrypter) (
 	<-chan *htlcswitch.PaymentResult, error) {
 
@@ -733,7 +733,7 @@ func (m *mockControlTower) FailAttempt(phash lntypes.Hash, pid uint64,
 	return args.Get(0).(*channeldb.HTLCAttempt), args.Error(1)
 }
 
-func (m *mockControlTower) FailPayment(phash lntypes.Hash,
+func (m *mockControlTower) Fail(phash lntypes.Hash,
 	reason channeldb.FailureReason) error {
 
 	m.Lock()
@@ -758,7 +758,6 @@ func (m *mockControlTower) FetchPayment(phash lntypes.Hash) (
 	// Make a copy of the payment here to avoid data race.
 	p := args.Get(0).(*channeldb.MPPayment)
 	payment := &channeldb.MPPayment{
-		Info:          p.Info,
 		FailureReason: p.FailureReason,
 	}
 	payment.HTLCs = make([]channeldb.HTLCAttempt, len(p.HTLCs))
diff --git a/routing/payment_lifecycle_test.go b/routing/payment_lifecycle_test.go
index 102aa4c01..b69df3e79 100644
--- a/routing/payment_lifecycle_test.go
+++ b/routing/payment_lifecycle_test.go
@@ -107,20 +107,20 @@ const (
 	sendToSwitchResultFailure = "SendToSwitch:failure"
 
 	// getPaymentResultSuccess is a test step where we expect the
-	// router to call the GetAttemptResult method, and we will
+	// router to call the GetPaymentResult method, and we will
 	// respond with a successful payment result.
-	getPaymentResultSuccess = "GetAttemptResult:success"
+	getPaymentResultSuccess = "GetPaymentResult:success"
 
 	// getPaymentResultTempFailure is a test step where we expect the
-	// router to call the GetAttemptResult method, and we will
+	// router to call the GetPaymentResult method, and we will
 	// respond with a forwarding error, expecting the router to retry.
-	getPaymentResultTempFailure = "GetAttemptResult:temp-failure"
+	getPaymentResultTempFailure = "GetPaymentResult:temp-failure"
 
 	// getPaymentResultTerminalFailure is a test step where we
-	// expect the router to call the GetAttemptResult method, and
+	// expect the router to call the GetPaymentResult method, and
 	// we will respond with a terminal error, expecting the router
 	// to stop making payment attempts.
-	getPaymentResultTerminalFailure = "GetAttemptResult:terminal-failure"
+	getPaymentResultTerminalFailure = "GetPaymentResult:terminal-failure"
 
 	// resendPayment is a test step where we manually try to resend
 	// the same payment, making sure the router responds with an
@@ -652,7 +652,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
 				fatal("unable to send result")
 			}
 
-		// In this step we expect the GetAttemptResult method
+		// In this step we expect the GetPaymentResult method
 		// to be called, and we respond with the preimage to
 		// complete the payment.
 		case getPaymentResultSuccess:
@@ -664,7 +664,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
 				fatal("unable to send result")
 			}
 
-		// In this state we expect the GetAttemptResult method
+		// In this state we expect the GetPaymentResult method
 		// to be called, and we respond with a forwarding
 		// error, indicating that the router should retry.
 		case getPaymentResultTempFailure:
@@ -682,7 +682,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
 			}
 
 		// In this state we expect the router to call the
-		// GetAttemptResult method, and we will respond with a
+		// GetPaymentResult method, and we will respond with a
 		// terminal error, indicating the router should stop
 		// making payment attempts.
 		case getPaymentResultTerminalFailure:
@@ -1052,9 +1052,10 @@ func TestUpdatePaymentState(t *testing.T) {
 			ct := &mockControlTower{}
 			rt := &ChannelRouter{cfg: &Config{Control: ct}}
 			pl := &paymentLifecycle{
-				router:     rt,
-				identifier: paymentHash,
-				feeLimit:   lnwire.MilliSatoshi(tc.feeLimit),
+				router:      rt,
+				identifier:  paymentHash,
+				totalAmount: lnwire.MilliSatoshi(tc.totalAmt),
+				feeLimit:    lnwire.MilliSatoshi(tc.feeLimit),
 			}
 
 			if tc.payment == nil {
@@ -1065,12 +1066,6 @@ func TestUpdatePaymentState(t *testing.T) {
 					nil, dummyErr,
 				)
 			} else {
-				// Attach the payment info.
-				info := &channeldb.PaymentCreationInfo{
-					Value: lnwire.MilliSatoshi(tc.totalAmt),
-				}
-				tc.payment.Info = info
-
 				// Otherwise we will return the payment.
 				ct.On("FetchPayment", paymentHash).Return(
 					tc.payment, nil,
diff --git a/routing/router_test.go b/routing/router_test.go
index ea76140a2..a3f832b82 100644
--- a/routing/router_test.go
+++ b/routing/router_test.go
@@ -3421,9 +3421,7 @@ func TestSendMPPaymentSucceed(t *testing.T) {
 	// The following mocked methods are called inside resumePayment. Note
 	// that the payment object below will determine the state of the
 	// paymentLifecycle.
-	payment := &channeldb.MPPayment{
-		Info: &channeldb.PaymentCreationInfo{Value: paymentAmt},
-	}
+	payment := &channeldb.MPPayment{}
 	controlTower.On("FetchPayment", identifier).Return(payment, nil)
 
 	// Create a route that can send 1/4 of the total amount. This value
@@ -3443,9 +3441,9 @@ func TestSendMPPaymentSucceed(t *testing.T) {
 		payment.HTLCs = append(payment.HTLCs, activeAttempt)
 	})
 
-	// Create a buffered chan and it will be returned by GetAttemptResult.
+	// Create a buffered chan and it will be returned by GetPaymentResult.
 	payer.resultChan = make(chan *htlcswitch.PaymentResult, 10)
-	payer.On("GetAttemptResult",
+	payer.On("GetPaymentResult",
 		mock.Anything, identifier, mock.Anything,
 	).Run(func(args mock.Arguments) {
 		// Before the mock method is returned, we send the result to
@@ -3590,9 +3588,7 @@ func TestSendMPPaymentSucceedOnExtraShards(t *testing.T) {
 	// The following mocked methods are called inside resumePayment. Note
 	// that the payment object below will determine the state of the
 	// paymentLifecycle.
-	payment := &channeldb.MPPayment{
-		Info: &channeldb.PaymentCreationInfo{Value: paymentAmt},
-	}
+	payment := &channeldb.MPPayment{}
 	controlTower.On("FetchPayment", identifier).Return(payment, nil)
 
 	// Create a route that can send 1/4 of the total amount. This value
@@ -3612,14 +3608,14 @@ func TestSendMPPaymentSucceedOnExtraShards(t *testing.T) {
 		payment.HTLCs = append(payment.HTLCs, activeAttempt)
 	})
 
-	// Create a buffered chan and it will be returned by GetAttemptResult.
+	// Create a buffered chan and it will be returned by GetPaymentResult.
 	payer.resultChan = make(chan *htlcswitch.PaymentResult, 10)
 
 	// We use the failAttemptCount to track how many attempts we want to
 	// fail. Each time the following mock method is called, the count gets
 	// updated.
 	failAttemptCount := 0
-	payer.On("GetAttemptResult",
+	payer.On("GetPaymentResult",
 		mock.Anything, identifier, mock.Anything,
 	).Run(func(args mock.Arguments) {
 		// Before the mock method is returned, we send the result to
@@ -3804,9 +3800,7 @@ func TestSendMPPaymentFailed(t *testing.T) {
 	// The following mocked methods are called inside resumePayment. Note
 	// that the payment object below will determine the state of the
 	// paymentLifecycle.
-	payment := &channeldb.MPPayment{
-		Info: &channeldb.PaymentCreationInfo{Value: paymentAmt},
-	}
+	payment := &channeldb.MPPayment{}
 	controlTower.On("FetchPayment", identifier).Return(payment, nil)
 
 	// Create a route that can send 1/4 of the total amount. This value
@@ -3826,14 +3820,14 @@ func TestSendMPPaymentFailed(t *testing.T) {
 		payment.HTLCs = append(payment.HTLCs, activeAttempt)
 	})
 
-	// Create a buffered chan and it will be returned by GetAttemptResult.
+	// Create a buffered chan and it will be returned by GetPaymentResult.
 	payer.resultChan = make(chan *htlcswitch.PaymentResult, 10)
 
 	// We use the failAttemptCount to track how many attempts we want to
 	// fail. Each time the following mock method is called, the count gets
 	// updated.
 	failAttemptCount := 0
-	payer.On("GetAttemptResult",
+	payer.On("GetPaymentResult",
 		mock.Anything, identifier, mock.Anything,
 	).Run(func(args mock.Arguments) {
 		// Before the mock method is returned, we send the result to
@@ -3902,7 +3896,7 @@ func TestSendMPPaymentFailed(t *testing.T) {
 	})
 
 	// Simple mocking the rest.
-	controlTower.On("FailPayment", identifier, failureReason).Return(nil)
+	controlTower.On("Fail", identifier, failureReason).Return(nil)
 	payer.On("SendHTLC",
 		mock.Anything, mock.Anything, mock.Anything,
 	).Return(nil)
@@ -4010,9 +4004,7 @@ func TestSendMPPaymentFailedWithShardsInFlight(t *testing.T) {
 	// The following mocked methods are called inside resumePayment. Note
 	// that the payment object below will determine the state of the
 	// paymentLifecycle.
-	payment := &channeldb.MPPayment{
-		Info: &channeldb.PaymentCreationInfo{Value: paymentAmt},
-	}
+	payment := &channeldb.MPPayment{}
 	controlTower.On("FetchPayment", identifier).Return(payment, nil)
 
 	// Create a route that can send 1/4 of the total amount. This value
@@ -4032,18 +4024,18 @@ func TestSendMPPaymentFailedWithShardsInFlight(t *testing.T) {
 		payment.HTLCs = append(payment.HTLCs, activeAttempt)
 	})
 
-	// Create a buffered chan and it will be returned by GetAttemptResult.
+	// Create a buffered chan and it will be returned by GetPaymentResult.
 	payer.resultChan = make(chan *htlcswitch.PaymentResult, 10)
 
 	// We use the getPaymentResultCnt to track how many times we called
-	// GetAttemptResult. As shard launch is sequential, and we fail the
-	// first shard that calls GetAttemptResult, we may end up with different
+	// GetPaymentResult. As shard launch is sequential, and we fail the
+	// first shard that calls GetPaymentResult, we may end up with different
 	// counts since the lifecycle itself is asynchronous. To avoid flakes
 	// due to this undeterminsitic behavior, we'll compare the final
 	// getPaymentResultCnt with other counters to create a final test
 	// expectation.
 	getPaymentResultCnt := 0
-	payer.On("GetAttemptResult",
+	payer.On("GetPaymentResult",
 		mock.Anything, identifier, mock.Anything,
 	).Run(func(args mock.Arguments) {
 		// Before the mock method is returned, we send the result to
@@ -4099,7 +4091,7 @@ func TestSendMPPaymentFailedWithShardsInFlight(t *testing.T) {
 
 	// Simple mocking the rest.
 	cntFail := 0
-	controlTower.On("FailPayment", identifier, failureReason).Return(nil)
+	controlTower.On("Fail", identifier, failureReason).Return(nil)
 	payer.On("SendHTLC",
 		mock.Anything, mock.Anything, mock.Anything,
 	).Return(nil).Run(func(args mock.Arguments) {
@@ -4262,9 +4254,9 @@ func TestSendToRouteSkipTempErrSuccess(t *testing.T) {
 		mock.Anything, mock.Anything, mock.Anything,
 	).Return(nil)
 
-	// Create a buffered chan and it will be returned by GetAttemptResult.
+	// Create a buffered chan and it will be returned by GetPaymentResult.
 	payer.resultChan = make(chan *htlcswitch.PaymentResult, 1)
-	payer.On("GetAttemptResult",
+	payer.On("GetPaymentResult",
 		mock.Anything, mock.Anything, mock.Anything,
 	).Run(func(_ mock.Arguments) {
 		// Send a successful payment result.
@@ -4337,7 +4329,7 @@ func TestSendToRouteSkipTempErrTempFailure(t *testing.T) {
 		mock.Anything, mock.Anything, mock.Anything,
 	).Return(nil)
 
-	// Create a buffered chan and it will be returned by GetAttemptResult.
+	// Create a buffered chan and it will be returned by GetPaymentResult.
 	payer.resultChan = make(chan *htlcswitch.PaymentResult, 1)
 
 	// Create the error to be returned.
@@ -4346,8 +4338,8 @@ func TestSendToRouteSkipTempErrTempFailure(t *testing.T) {
 		1,
 	)
 
-	// Mock GetAttemptResult to return a failure.
-	payer.On("GetAttemptResult",
+	// Mock GetPaymentResult to return a failure.
+	payer.On("GetPaymentResult",
 		mock.Anything, mock.Anything, mock.Anything,
 	).Run(func(_ mock.Arguments) {
 		// Send an attempt failure.
@@ -4420,13 +4412,13 @@ func TestSendToRouteSkipTempErrPermanentFailure(t *testing.T) {
 	).Return(testAttempt, nil)
 
 	// Expect the payment to be failed.
-	controlTower.On("FailPayment", payHash, mock.Anything).Return(nil)
+	controlTower.On("Fail", payHash, mock.Anything).Return(nil)
 
 	payer.On("SendHTLC",
 		mock.Anything, mock.Anything, mock.Anything,
 	).Return(nil)
 
-	// Create a buffered chan and it will be returned by GetAttemptResult.
+	// Create a buffered chan and it will be returned by GetPaymentResult.
 	payer.resultChan = make(chan *htlcswitch.PaymentResult, 1)
 
 	// Create the error to be returned.
@@ -4434,8 +4426,8 @@ func TestSendToRouteSkipTempErrPermanentFailure(t *testing.T) {
 		&lnwire.FailIncorrectDetails{}, 1,
 	)
 
-	// Mock GetAttemptResult to return a failure.
-	payer.On("GetAttemptResult",
+	// Mock GetPaymentResult to return a failure.
+	payer.On("GetPaymentResult",
 		mock.Anything, mock.Anything, mock.Anything,
 	).Run(func(_ mock.Arguments) {
 		// Send a permanent failure.
@@ -4509,13 +4501,13 @@ func TestSendToRouteTempFailure(t *testing.T) {
 	).Return(testAttempt, nil)
 
 	// Expect the payment to be failed.
-	controlTower.On("FailPayment", payHash, mock.Anything).Return(nil)
+	controlTower.On("Fail", payHash, mock.Anything).Return(nil)
 
 	payer.On("SendHTLC",
 		mock.Anything, mock.Anything, mock.Anything,
 	).Return(nil)
 
-	// Create a buffered chan and it will be returned by GetAttemptResult.
+	// Create a buffered chan and it will be returned by GetPaymentResult.
 	payer.resultChan = make(chan *htlcswitch.PaymentResult, 1)
 
 	// Create the error to be returned.
@@ -4524,8 +4516,8 @@ func TestSendToRouteTempFailure(t *testing.T) {
 		1,
 	)
 
-	// Mock GetAttemptResult to return a failure.
-	payer.On("GetAttemptResult",
+	// Mock GetPaymentResult to return a failure.
+	payer.On("GetPaymentResult",
 		mock.Anything, mock.Anything, mock.Anything,
 	).Run(func(_ mock.Arguments) {
 		// Send an attempt failure.