From 7aa4a7c7fcbbe5058960ad8edfea9a8228bd1c12 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Wed, 11 Dec 2019 10:20:55 +0100 Subject: [PATCH] channeldb/migration_01_to_11: isolate route structure Before we change the Hop struct, isolate the code that is used in older migrations to prevent breaking them. route.go was taken from commit 6e463c1634061d595953f20813860207e5d485ce --- .../migration_09_legacy_serialization.go | 15 +- .../migration_10_route_tlv_records.go | 15 +- channeldb/migration_01_to_11/migrations.go | 7 +- .../migration_01_to_11/migrations_test.go | 15 +- channeldb/migration_01_to_11/payments.go | 17 +- channeldb/migration_01_to_11/route.go | 330 ++++++++++++++++++ 6 files changed, 362 insertions(+), 37 deletions(-) create mode 100644 channeldb/migration_01_to_11/route.go diff --git a/channeldb/migration_01_to_11/migration_09_legacy_serialization.go b/channeldb/migration_01_to_11/migration_09_legacy_serialization.go index cc6614c9..cc0568a3 100644 --- a/channeldb/migration_01_to_11/migration_09_legacy_serialization.go +++ b/channeldb/migration_01_to_11/migration_09_legacy_serialization.go @@ -10,7 +10,6 @@ import ( "github.com/coreos/bbolt" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" - "github.com/lightningnetwork/lnd/routing/route" ) var ( @@ -274,7 +273,7 @@ func serializePaymentAttemptInfoMigration9(w io.Writer, a *PaymentAttemptInfo) e return nil } -func serializeHopMigration9(w io.Writer, h *route.Hop) error { +func serializeHopMigration9(w io.Writer, h *Hop) error { if err := WriteElements(w, h.PubKeyBytes[:], h.ChannelID, h.OutgoingTimeLock, h.AmtToForward, @@ -285,7 +284,7 @@ func serializeHopMigration9(w io.Writer, h *route.Hop) error { return nil } -func serializeRouteMigration9(w io.Writer, r route.Route) error { +func serializeRouteMigration9(w io.Writer, r Route) error { if err := WriteElements(w, r.TotalTimeLock, r.TotalAmount, r.SourcePubKey[:], ); err != nil { @@ -318,8 +317,8 @@ func deserializePaymentAttemptInfoMigration9(r io.Reader) (*PaymentAttemptInfo, return a, nil } -func deserializeRouteMigration9(r io.Reader) (route.Route, error) { - rt := route.Route{} +func deserializeRouteMigration9(r io.Reader) (Route, error) { + rt := Route{} if err := ReadElements(r, &rt.TotalTimeLock, &rt.TotalAmount, ); err != nil { @@ -337,7 +336,7 @@ func deserializeRouteMigration9(r io.Reader) (route.Route, error) { return rt, err } - var hops []*route.Hop + var hops []*Hop for i := uint32(0); i < numHops; i++ { hop, err := deserializeHopMigration9(r) if err != nil { @@ -350,8 +349,8 @@ func deserializeRouteMigration9(r io.Reader) (route.Route, error) { return rt, nil } -func deserializeHopMigration9(r io.Reader) (*route.Hop, error) { - h := &route.Hop{} +func deserializeHopMigration9(r io.Reader) (*Hop, error) { + h := &Hop{} var pub []byte if err := ReadElements(r, &pub); err != nil { diff --git a/channeldb/migration_01_to_11/migration_10_route_tlv_records.go b/channeldb/migration_01_to_11/migration_10_route_tlv_records.go index 648d85ad..e404f575 100644 --- a/channeldb/migration_01_to_11/migration_10_route_tlv_records.go +++ b/channeldb/migration_01_to_11/migration_10_route_tlv_records.go @@ -5,7 +5,6 @@ import ( "io" "github.com/coreos/bbolt" - "github.com/lightningnetwork/lnd/routing/route" ) // MigrateRouteSerialization migrates the way we serialize routes across the @@ -154,8 +153,8 @@ func serializePaymentAttemptInfoLegacy(w io.Writer, a *PaymentAttemptInfo) error return nil } -func deserializeHopLegacy(r io.Reader) (*route.Hop, error) { - h := &route.Hop{} +func deserializeHopLegacy(r io.Reader) (*Hop, error) { + h := &Hop{} var pub []byte if err := ReadElements(r, &pub); err != nil { @@ -172,7 +171,7 @@ func deserializeHopLegacy(r io.Reader) (*route.Hop, error) { return h, nil } -func serializeHopLegacy(w io.Writer, h *route.Hop) error { +func serializeHopLegacy(w io.Writer, h *Hop) error { if err := WriteElements(w, h.PubKeyBytes[:], h.ChannelID, h.OutgoingTimeLock, h.AmtToForward, @@ -183,8 +182,8 @@ func serializeHopLegacy(w io.Writer, h *route.Hop) error { return nil } -func deserializeRouteLegacy(r io.Reader) (route.Route, error) { - rt := route.Route{} +func deserializeRouteLegacy(r io.Reader) (Route, error) { + rt := Route{} if err := ReadElements(r, &rt.TotalTimeLock, &rt.TotalAmount, ); err != nil { @@ -202,7 +201,7 @@ func deserializeRouteLegacy(r io.Reader) (route.Route, error) { return rt, err } - var hops []*route.Hop + var hops []*Hop for i := uint32(0); i < numHops; i++ { hop, err := deserializeHopLegacy(r) if err != nil { @@ -215,7 +214,7 @@ func deserializeRouteLegacy(r io.Reader) (route.Route, error) { return rt, nil } -func serializeRouteLegacy(w io.Writer, r route.Route) error { +func serializeRouteLegacy(w io.Writer, r Route) error { if err := WriteElements(w, r.TotalTimeLock, r.TotalAmount, r.SourcePubKey[:], ); err != nil { diff --git a/channeldb/migration_01_to_11/migrations.go b/channeldb/migration_01_to_11/migrations.go index 3f841009..511633d8 100644 --- a/channeldb/migration_01_to_11/migrations.go +++ b/channeldb/migration_01_to_11/migrations.go @@ -9,7 +9,6 @@ import ( "github.com/btcsuite/btcd/btcec" "github.com/coreos/bbolt" "github.com/lightningnetwork/lnd/lnwire" - "github.com/lightningnetwork/lnd/routing/route" ) // MigrateNodeAndEdgeUpdateIndex is a migration function that will update the @@ -817,14 +816,14 @@ func MigrateOutgoingPayments(tx *bbolt.Tx) error { // Do the same for the PaymentAttemptInfo. totalAmt := payment.Terms.Value + payment.Fee - rt := route.Route{ + rt := Route{ TotalTimeLock: payment.TimeLockLength, TotalAmount: totalAmt, SourcePubKey: sourcePubKey, - Hops: []*route.Hop{}, + Hops: []*Hop{}, } for _, hop := range payment.Path { - rt.Hops = append(rt.Hops, &route.Hop{ + rt.Hops = append(rt.Hops, &Hop{ PubKeyBytes: hop, AmtToForward: totalAmt, }) diff --git a/channeldb/migration_01_to_11/migrations_test.go b/channeldb/migration_01_to_11/migrations_test.go index 980b029c..fd2c2565 100644 --- a/channeldb/migration_01_to_11/migrations_test.go +++ b/channeldb/migration_01_to_11/migrations_test.go @@ -16,7 +16,6 @@ import ( "github.com/go-errors/errors" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" - "github.com/lightningnetwork/lnd/routing/route" ) // TestPaymentStatusesMigration checks that already completed payments will have @@ -714,25 +713,25 @@ func makeRandPaymentCreationInfo() (*PaymentCreationInfo, error) { func TestPaymentRouteSerialization(t *testing.T) { t.Parallel() - legacyHop1 := &route.Hop{ - PubKeyBytes: route.NewVertex(pub), + legacyHop1 := &Hop{ + PubKeyBytes: NewVertex(pub), ChannelID: 12345, OutgoingTimeLock: 111, LegacyPayload: true, AmtToForward: 555, } - legacyHop2 := &route.Hop{ - PubKeyBytes: route.NewVertex(pub), + legacyHop2 := &Hop{ + PubKeyBytes: NewVertex(pub), ChannelID: 12345, OutgoingTimeLock: 111, LegacyPayload: true, AmtToForward: 555, } - legacyRoute := route.Route{ + legacyRoute := Route{ TotalTimeLock: 123, TotalAmount: 1234567, - SourcePubKey: route.NewVertex(pub), - Hops: []*route.Hop{legacyHop1, legacyHop2}, + SourcePubKey: NewVertex(pub), + Hops: []*Hop{legacyHop1, legacyHop2}, } const numPayments = 4 diff --git a/channeldb/migration_01_to_11/payments.go b/channeldb/migration_01_to_11/payments.go index 1195ca82..16e4a71c 100644 --- a/channeldb/migration_01_to_11/payments.go +++ b/channeldb/migration_01_to_11/payments.go @@ -14,7 +14,6 @@ import ( "github.com/coreos/bbolt" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" - "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/tlv" ) @@ -213,7 +212,7 @@ type PaymentAttemptInfo struct { SessionKey *btcec.PrivateKey // Route is the route attempted to send the HTLC. - Route route.Route + Route Route } // Payment is a wrapper around a payment's PaymentCreationInfo, @@ -464,7 +463,7 @@ func deserializePaymentAttemptInfo(r io.Reader) (*PaymentAttemptInfo, error) { return a, nil } -func serializeHop(w io.Writer, h *route.Hop) error { +func serializeHop(w io.Writer, h *Hop) error { if err := WriteElements(w, h.PubKeyBytes[:], h.ChannelID, h.OutgoingTimeLock, h.AmtToForward, @@ -513,8 +512,8 @@ func serializeHop(w io.Writer, h *route.Hop) error { // to read/write a TLV stream larger than this. const maxOnionPayloadSize = 1300 -func deserializeHop(r io.Reader) (*route.Hop, error) { - h := &route.Hop{} +func deserializeHop(r io.Reader) (*Hop, error) { + h := &Hop{} var pub []byte if err := ReadElements(r, &pub); err != nil { @@ -568,7 +567,7 @@ func deserializeHop(r io.Reader) (*route.Hop, error) { } // SerializeRoute serializes a route. -func SerializeRoute(w io.Writer, r route.Route) error { +func SerializeRoute(w io.Writer, r Route) error { if err := WriteElements(w, r.TotalTimeLock, r.TotalAmount, r.SourcePubKey[:], ); err != nil { @@ -589,8 +588,8 @@ func SerializeRoute(w io.Writer, r route.Route) error { } // DeserializeRoute deserializes a route. -func DeserializeRoute(r io.Reader) (route.Route, error) { - rt := route.Route{} +func DeserializeRoute(r io.Reader) (Route, error) { + rt := Route{} if err := ReadElements(r, &rt.TotalTimeLock, &rt.TotalAmount, ); err != nil { @@ -608,7 +607,7 @@ func DeserializeRoute(r io.Reader) (route.Route, error) { return rt, err } - var hops []*route.Hop + var hops []*Hop for i := uint32(0); i < numHops; i++ { hop, err := deserializeHop(r) if err != nil { diff --git a/channeldb/migration_01_to_11/route.go b/channeldb/migration_01_to_11/route.go new file mode 100644 index 00000000..1dbfff60 --- /dev/null +++ b/channeldb/migration_01_to_11/route.go @@ -0,0 +1,330 @@ +package migration_01_to_11 + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "fmt" + "io" + "strconv" + "strings" + + "github.com/btcsuite/btcd/btcec" + sphinx "github.com/lightningnetwork/lightning-onion" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/record" + "github.com/lightningnetwork/lnd/tlv" +) + +// VertexSize is the size of the array to store a vertex. +const VertexSize = 33 + +// ErrNoRouteHopsProvided is returned when a caller attempts to construct a new +// sphinx packet, but provides an empty set of hops for each route. +var ErrNoRouteHopsProvided = fmt.Errorf("empty route hops provided") + +// Vertex is a simple alias for the serialization of a compressed Bitcoin +// public key. +type Vertex [VertexSize]byte + +// NewVertex returns a new Vertex given a public key. +func NewVertex(pub *btcec.PublicKey) Vertex { + var v Vertex + copy(v[:], pub.SerializeCompressed()) + return v +} + +// NewVertexFromBytes returns a new Vertex based on a serialized pubkey in a +// byte slice. +func NewVertexFromBytes(b []byte) (Vertex, error) { + vertexLen := len(b) + if vertexLen != VertexSize { + return Vertex{}, fmt.Errorf("invalid vertex length of %v, "+ + "want %v", vertexLen, VertexSize) + } + + var v Vertex + copy(v[:], b) + return v, nil +} + +// NewVertexFromStr returns a new Vertex given its hex-encoded string format. +func NewVertexFromStr(v string) (Vertex, error) { + // Return error if hex string is of incorrect length. + if len(v) != VertexSize*2 { + return Vertex{}, fmt.Errorf("invalid vertex string length of "+ + "%v, want %v", len(v), VertexSize*2) + } + + vertex, err := hex.DecodeString(v) + if err != nil { + return Vertex{}, err + } + + return NewVertexFromBytes(vertex) +} + +// String returns a human readable version of the Vertex which is the +// hex-encoding of the serialized compressed public key. +func (v Vertex) String() string { + return fmt.Sprintf("%x", v[:]) +} + +// Hop represents an intermediate or final node of the route. This naming +// is in line with the definition given in BOLT #4: Onion Routing Protocol. +// The struct houses the channel along which this hop can be reached and +// the values necessary to create the HTLC that needs to be sent to the +// next hop. It is also used to encode the per-hop payload included within +// the Sphinx packet. +type Hop struct { + // PubKeyBytes is the raw bytes of the public key of the target node. + PubKeyBytes Vertex + + // ChannelID is the unique channel ID for the channel. The first 3 + // bytes are the block height, the next 3 the index within the block, + // and the last 2 bytes are the output index for the channel. + ChannelID uint64 + + // OutgoingTimeLock is the timelock value that should be used when + // crafting the _outgoing_ HTLC from this hop. + OutgoingTimeLock uint32 + + // AmtToForward is the amount that this hop will forward to the next + // hop. This value is less than the value that the incoming HTLC + // carries as a fee will be subtracted by the hop. + AmtToForward lnwire.MilliSatoshi + + // TLVRecords if non-nil are a set of additional TLV records that + // should be included in the forwarding instructions for this node. + TLVRecords []tlv.Record + + // LegacyPayload if true, then this signals that this node doesn't + // understand the new TLV payload, so we must instead use the legacy + // payload. + LegacyPayload bool +} + +// PackHopPayload writes to the passed io.Writer, the series of byes that can +// be placed directly into the per-hop payload (EOB) for this hop. This will +// include the required routing fields, as well as serializing any of the +// passed optional TLVRecords. nextChanID is the unique channel ID that +// references the _outgoing_ channel ID that follows this hop. This field +// follows the same semantics as the NextAddress field in the onion: it should +// be set to zero to indicate the terminal hop. +func (h *Hop) PackHopPayload(w io.Writer, nextChanID uint64) error { + // If this is a legacy payload, then we'll exit here as this method + // shouldn't be called. + if h.LegacyPayload == true { + return fmt.Errorf("cannot pack hop payloads for legacy " + + "payloads") + } + + // Otherwise, we'll need to make a new stream that includes our + // required routing fields, as well as these optional values. + var records []tlv.Record + + // Every hop must have an amount to forward and CLTV expiry. + amt := uint64(h.AmtToForward) + records = append(records, + record.NewAmtToFwdRecord(&amt), + record.NewLockTimeRecord(&h.OutgoingTimeLock), + ) + + // BOLT 04 says the next_hop_id should be omitted for the final hop, + // but present for all others. + // + // TODO(conner): test using hop.Exit once available + if nextChanID != 0 { + records = append(records, + record.NewNextHopIDRecord(&nextChanID), + ) + } + + // Append any custom types destined for this hop. + records = append(records, h.TLVRecords...) + + // To ensure we produce a canonical stream, we'll sort the records + // before encoding them as a stream in the hop payload. + tlv.SortRecords(records) + + tlvStream, err := tlv.NewStream(records...) + if err != nil { + return err + } + + return tlvStream.Encode(w) +} + +// Route represents a path through the channel graph which runs over one or +// more channels in succession. This struct carries all the information +// required to craft the Sphinx onion packet, and send the payment along the +// first hop in the path. A route is only selected as valid if all the channels +// have sufficient capacity to carry the initial payment amount after fees are +// accounted for. +type Route struct { + // TotalTimeLock is the cumulative (final) time lock across the entire + // route. This is the CLTV value that should be extended to the first + // hop in the route. All other hops will decrement the time-lock as + // advertised, leaving enough time for all hops to wait for or present + // the payment preimage to complete the payment. + TotalTimeLock uint32 + + // TotalAmount is the total amount of funds required to complete a + // payment over this route. This value includes the cumulative fees at + // each hop. As a result, the HTLC extended to the first-hop in the + // route will need to have at least this many satoshis, otherwise the + // route will fail at an intermediate node due to an insufficient + // amount of fees. + TotalAmount lnwire.MilliSatoshi + + // SourcePubKey is the pubkey of the node where this route originates + // from. + SourcePubKey Vertex + + // Hops contains details concerning the specific forwarding details at + // each hop. + Hops []*Hop +} + +// HopFee returns the fee charged by the route hop indicated by hopIndex. +func (r *Route) HopFee(hopIndex int) lnwire.MilliSatoshi { + var incomingAmt lnwire.MilliSatoshi + if hopIndex == 0 { + incomingAmt = r.TotalAmount + } else { + incomingAmt = r.Hops[hopIndex-1].AmtToForward + } + + // Fee is calculated as difference between incoming and outgoing amount. + return incomingAmt - r.Hops[hopIndex].AmtToForward +} + +// TotalFees is the sum of the fees paid at each hop within the final route. In +// the case of a one-hop payment, this value will be zero as we don't need to +// pay a fee to ourself. +func (r *Route) TotalFees() lnwire.MilliSatoshi { + if len(r.Hops) == 0 { + return 0 + } + + return r.TotalAmount - r.Hops[len(r.Hops)-1].AmtToForward +} + +// NewRouteFromHops creates a new Route structure from the minimally required +// information to perform the payment. It infers fee amounts and populates the +// node, chan and prev/next hop maps. +func NewRouteFromHops(amtToSend lnwire.MilliSatoshi, timeLock uint32, + sourceVertex Vertex, hops []*Hop) (*Route, error) { + + if len(hops) == 0 { + return nil, ErrNoRouteHopsProvided + } + + // First, we'll create a route struct and populate it with the fields + // for which the values are provided as arguments of this function. + // TotalFees is determined based on the difference between the amount + // that is send from the source and the final amount that is received + // by the destination. + route := &Route{ + SourcePubKey: sourceVertex, + Hops: hops, + TotalTimeLock: timeLock, + TotalAmount: amtToSend, + } + + return route, nil +} + +// ToSphinxPath converts a complete route into a sphinx PaymentPath that +// contains the per-hop paylods used to encoding the HTLC routing data for each +// hop in the route. This method also accepts an optional EOB payload for the +// final hop. +func (r *Route) ToSphinxPath() (*sphinx.PaymentPath, error) { + var path sphinx.PaymentPath + + // For each hop encoded within the route, we'll convert the hop struct + // to an OnionHop with matching per-hop payload within the path as used + // by the sphinx package. + for i, hop := range r.Hops { + pub, err := btcec.ParsePubKey( + hop.PubKeyBytes[:], btcec.S256(), + ) + if err != nil { + return nil, err + } + + // As a base case, the next hop is set to all zeroes in order + // to indicate that the "last hop" as no further hops after it. + nextHop := uint64(0) + + // If we aren't on the last hop, then we set the "next address" + // field to be the channel that directly follows it. + if i != len(r.Hops)-1 { + nextHop = r.Hops[i+1].ChannelID + } + + var payload sphinx.HopPayload + + // If this is the legacy payload, then we can just include the + // hop data as normal. + if hop.LegacyPayload { + // Before we encode this value, we'll pack the next hop + // into the NextAddress field of the hop info to ensure + // we point to the right now. + hopData := sphinx.HopData{ + ForwardAmount: uint64(hop.AmtToForward), + OutgoingCltv: hop.OutgoingTimeLock, + } + binary.BigEndian.PutUint64( + hopData.NextAddress[:], nextHop, + ) + + payload, err = sphinx.NewHopPayload(&hopData, nil) + if err != nil { + return nil, err + } + } else { + // For non-legacy payloads, we'll need to pack the + // routing information, along with any extra TLV + // information into the new per-hop payload format. + // We'll also pass in the chan ID of the hop this + // channel should be forwarded to so we can construct a + // valid payload. + var b bytes.Buffer + err := hop.PackHopPayload(&b, nextHop) + if err != nil { + return nil, err + } + + // TODO(roasbeef): make better API for NewHopPayload? + payload, err = sphinx.NewHopPayload(nil, b.Bytes()) + if err != nil { + return nil, err + } + } + + path[i] = sphinx.OnionHop{ + NodePub: *pub, + HopPayload: payload, + } + } + + return &path, nil +} + +// String returns a human readable representation of the route. +func (r *Route) String() string { + var b strings.Builder + + for i, hop := range r.Hops { + if i > 0 { + b.WriteString(",") + } + b.WriteString(strconv.FormatUint(hop.ChannelID, 10)) + } + + return fmt.Sprintf("amt=%v, fees=%v, tl=%v, chans=%v", + r.TotalAmount-r.TotalFees(), r.TotalFees(), r.TotalTimeLock, + b.String(), + ) +}