diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a379d1cd6..d71a47da4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,7 @@ jobs: run: RUSTFLAGS="-C link-dead-code" cargo build --verbose --color always - name: Build on Rust ${{ matrix.toolchain }} if: "! matrix.build-net-tokio" - run: cargo build --verbose --color always -p lightning + run: cargo build --verbose --color always -p lightning && cargo build --verbose --color always -p lightning-invoice - name: Build Block Sync Clients on Rust ${{ matrix.toolchain }} with features if: "matrix.build-net-tokio && !matrix.coverage" run: | @@ -74,7 +74,7 @@ jobs: run: RUSTFLAGS="-C link-dead-code" cargo test --verbose --color always - name: Test on Rust ${{ matrix.toolchain }} if: "! matrix.build-net-tokio" - run: cargo test --verbose --color always -p lightning + run: cargo test --verbose --color always -p lightning && cargo test --verbose --color always -p lightning-invoice - name: Test Block Sync Clients on Rust ${{ matrix.toolchain }} with features if: "matrix.build-net-tokio && !matrix.coverage" run: | diff --git a/fuzz/src/router.rs b/fuzz/src/router.rs index fb720c991..e80e080f3 100644 --- a/fuzz/src/router.rs +++ b/fuzz/src/router.rs @@ -15,7 +15,7 @@ use lightning::chain; use lightning::ln::channelmanager::ChannelDetails; use lightning::ln::features::InitFeatures; use lightning::ln::msgs; -use lightning::routing::router::{get_route, RouteHint}; +use lightning::routing::router::{get_route, RouteHintHop}; use lightning::util::logger::Logger; use lightning::util::ser::Readable; use lightning::routing::network_graph::{NetworkGraph, RoutingFees}; @@ -224,7 +224,7 @@ pub fn do_test(data: &[u8], out: Out) { for _ in 0..count { scid += 1; let rnid = node_pks.iter().skip(slice_to_be16(get_slice!(2))as usize % node_pks.len()).next().unwrap(); - last_hops_vec.push(RouteHint { + last_hops_vec.push(RouteHintHop { src_node_id: *rnid, short_channel_id: scid, fees: RoutingFees { diff --git a/lightning-invoice/Cargo.toml b/lightning-invoice/Cargo.toml index 47bf9d744..758578ca4 100644 --- a/lightning-invoice/Cargo.toml +++ b/lightning-invoice/Cargo.toml @@ -10,6 +10,7 @@ readme = "README.md" [dependencies] bech32 = "0.7" +lightning = { version = "0.0.13", path = "../lightning" } secp256k1 = { version = "0.20", features = ["recovery"] } num-traits = "0.2.8" bitcoin_hashes = "0.9.4" diff --git a/lightning-invoice/src/de.rs b/lightning-invoice/src/de.rs index d6cb92072..fe77a93a9 100644 --- a/lightning-invoice/src/de.rs +++ b/lightning-invoice/src/de.rs @@ -10,6 +10,8 @@ use bech32::{u5, FromBase32}; use bitcoin_hashes::Hash; use bitcoin_hashes::sha256; +use lightning::routing::network_graph::RoutingFees; +use lightning::routing::router::RouteHintHop; use num_traits::{CheckedAdd, CheckedMul}; @@ -353,7 +355,7 @@ impl FromBase32 for Signature { } } -fn parse_int_be(digits: &[U], base: T) -> Option +pub(crate) fn parse_int_be(digits: &[U], base: T) -> Option where T: CheckedAdd + CheckedMul + From + Default, U: Into + Copy { @@ -428,7 +430,7 @@ impl FromBase32 for TaggedField { constants::TAG_FALLBACK => Ok(TaggedField::Fallback(Fallback::from_base32(field_data)?)), constants::TAG_ROUTE => - Ok(TaggedField::Route(Route::from_base32(field_data)?)), + Ok(TaggedField::Route(RouteHint::from_base32(field_data)?)), constants::TAG_PAYMENT_SECRET => Ok(TaggedField::PaymentSecret(PaymentSecret::from_base32(field_data)?)), _ => { @@ -565,17 +567,17 @@ impl FromBase32 for Fallback { } } -impl FromBase32 for Route { +impl FromBase32 for RouteHint { type Err = ParseError; - fn from_base32(field_data: &[u5]) -> Result { + fn from_base32(field_data: &[u5]) -> Result { let bytes = Vec::::from_base32(field_data)?; if bytes.len() % 51 != 0 { return Err(ParseError::UnexpectedEndOfTaggedFields); } - let mut route_hops = Vec::::new(); + let mut route_hops = Vec::::new(); let mut bytes = bytes.as_slice(); while !bytes.is_empty() { @@ -585,18 +587,22 @@ impl FromBase32 for Route { let mut channel_id: [u8; 8] = Default::default(); channel_id.copy_from_slice(&hop_bytes[33..41]); - let hop = RouteHop { - pubkey: PublicKey::from_slice(&hop_bytes[0..33])?, - short_channel_id: channel_id, - fee_base_msat: parse_int_be(&hop_bytes[41..45], 256).expect("slice too big?"), - fee_proportional_millionths: parse_int_be(&hop_bytes[45..49], 256).expect("slice too big?"), - cltv_expiry_delta: parse_int_be(&hop_bytes[49..51], 256).expect("slice too big?") + let hop = RouteHintHop { + src_node_id: PublicKey::from_slice(&hop_bytes[0..33])?, + short_channel_id: parse_int_be(&channel_id, 256).expect("short chan ID slice too big?"), + fees: RoutingFees { + base_msat: parse_int_be(&hop_bytes[41..45], 256).expect("slice too big?"), + proportional_millionths: parse_int_be(&hop_bytes[45..49], 256).expect("slice too big?"), + }, + cltv_expiry_delta: parse_int_be(&hop_bytes[49..51], 256).expect("slice too big?"), + htlc_minimum_msat: None, + htlc_maximum_msat: None, }; route_hops.push(hop); } - Ok(Route(route_hops)) + Ok(RouteHint(route_hops)) } } @@ -931,47 +937,57 @@ mod test { #[test] fn test_parse_route() { - use RouteHop; - use ::Route; + use lightning::routing::network_graph::RoutingFees; + use lightning::routing::router::RouteHintHop; + use ::RouteHint; use bech32::FromBase32; + use de::parse_int_be; let input = from_bech32( "q20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqa\ fqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzq".as_bytes() ); - let mut expected = Vec::::new(); - expected.push(RouteHop { - pubkey: PublicKey::from_slice( + let mut expected = Vec::::new(); + expected.push(RouteHintHop { + src_node_id: PublicKey::from_slice( &[ 0x02u8, 0x9e, 0x03, 0xa9, 0x01, 0xb8, 0x55, 0x34, 0xff, 0x1e, 0x92, 0xc4, 0x3c, 0x74, 0x43, 0x1f, 0x7c, 0xe7, 0x20, 0x46, 0x06, 0x0f, 0xcf, 0x7a, 0x95, 0xc3, 0x7e, 0x14, 0x8f, 0x78, 0xc7, 0x72, 0x55 ][..] ).unwrap(), - short_channel_id: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08], - fee_base_msat: 1, - fee_proportional_millionths: 20, - cltv_expiry_delta: 3 + short_channel_id: parse_int_be(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08], 256).expect("short chan ID slice too big?"), + fees: RoutingFees { + base_msat: 1, + proportional_millionths: 20, + }, + cltv_expiry_delta: 3, + htlc_minimum_msat: None, + htlc_maximum_msat: None }); - expected.push(RouteHop { - pubkey: PublicKey::from_slice( + expected.push(RouteHintHop { + src_node_id: PublicKey::from_slice( &[ 0x03u8, 0x9e, 0x03, 0xa9, 0x01, 0xb8, 0x55, 0x34, 0xff, 0x1e, 0x92, 0xc4, 0x3c, 0x74, 0x43, 0x1f, 0x7c, 0xe7, 0x20, 0x46, 0x06, 0x0f, 0xcf, 0x7a, 0x95, 0xc3, 0x7e, 0x14, 0x8f, 0x78, 0xc7, 0x72, 0x55 ][..] ).unwrap(), - short_channel_id: [0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a], - fee_base_msat: 2, - fee_proportional_millionths: 30, - cltv_expiry_delta: 4 + short_channel_id: parse_int_be(&[0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a], 256).expect("short chan ID slice too big?"), + fees: RoutingFees { + base_msat: 2, + proportional_millionths: 30, + }, + cltv_expiry_delta: 4, + htlc_minimum_msat: None, + htlc_maximum_msat: None }); - assert_eq!(Route::from_base32(&input), Ok(Route(expected))); + assert_eq!(RouteHint::from_base32(&input), Ok(RouteHint(expected))); assert_eq!( - Route::from_base32(&[u5::try_from_u8(0).unwrap(); 40][..]), + RouteHint::from_base32(&[u5::try_from_u8(0).unwrap(); 40][..]), Err(ParseError::UnexpectedEndOfTaggedFields) ); } diff --git a/lightning-invoice/src/lib.rs b/lightning-invoice/src/lib.rs index e9ca442f2..2ce022248 100644 --- a/lightning-invoice/src/lib.rs +++ b/lightning-invoice/src/lib.rs @@ -17,12 +17,16 @@ extern crate bech32; extern crate bitcoin_hashes; +extern crate lightning; extern crate num_traits; extern crate secp256k1; use bech32::u5; use bitcoin_hashes::Hash; use bitcoin_hashes::sha256; +#[cfg(any(doc, test))] +use lightning::routing::network_graph::RoutingFees; +use lightning::routing::router::RouteHintHop; use secp256k1::key::PublicKey; use secp256k1::{Message, Secp256k1}; @@ -323,7 +327,7 @@ pub enum TaggedField { ExpiryTime(ExpiryTime), MinFinalCltvExpiry(MinFinalCltvExpiry), Fallback(Fallback), - Route(Route), + Route(RouteHint), PaymentSecret(PaymentSecret), } @@ -383,26 +387,7 @@ pub struct Signature(pub RecoverableSignature); /// The encoded route has to be <1024 5bit characters long (<=639 bytes or <=12 hops) /// #[derive(Eq, PartialEq, Debug, Clone)] -pub struct Route(Vec); - -/// Node on a private route -#[derive(Eq, PartialEq, Debug, Clone)] -pub struct RouteHop { - /// Node's public key - pub pubkey: PublicKey, - - /// Which channel of this node we would be using - pub short_channel_id: [u8; 8], - - /// Fee charged by this node per transaction - pub fee_base_msat: u32, - - /// Fee charged by this node proportional to the amount routed - pub fee_proportional_millionths: u32, - - /// Delta substracted by this node from incoming cltv_expiry value - pub cltv_expiry_delta: u16, -} +pub struct RouteHint(Vec); /// Tag constants as specified in BOLT11 #[allow(missing_docs)] @@ -499,8 +484,8 @@ impl InvoiceBuilder { } /// Adds a private route. - pub fn route(mut self, route: Vec) -> Self { - match Route::new(route) { + pub fn route(mut self, route: Vec) -> Self { + match RouteHint::new(route) { Ok(r) => self.tagged_fields.push(TaggedField::Route(r)), Err(e) => self.error = Some(e), } @@ -832,11 +817,11 @@ impl RawInvoice { }).collect::>() } - pub fn routes(&self) -> Vec<&Route> { + pub fn routes(&self) -> Vec<&RouteHint> { self.known_tagged_fields().filter_map(|tf| match tf { &TaggedField::Route(ref r) => Some(r), _ => None, - }).collect::>() + }).collect::>() } pub fn amount_pico_btc(&self) -> Option { @@ -1035,7 +1020,7 @@ impl Invoice { } /// Returns a list of all routes included in the invoice - pub fn routes(&self) -> Vec<&Route> { + pub fn routes(&self) -> Vec<&RouteHint> { self.signed_invoice.routes() } @@ -1157,32 +1142,32 @@ impl ExpiryTime { } } -impl Route { +impl RouteHint { /// Create a new (partial) route from a list of hops - pub fn new(hops: Vec) -> Result { + pub fn new(hops: Vec) -> Result { if hops.len() <= 12 { - Ok(Route(hops)) + Ok(RouteHint(hops)) } else { Err(CreationError::RouteTooLong) } } /// Returrn the underlying vector of hops - pub fn into_inner(self) -> Vec { + pub fn into_inner(self) -> Vec { self.0 } } -impl Into> for Route { - fn into(self) -> Vec { +impl Into> for RouteHint { + fn into(self) -> Vec { self.into_inner() } } -impl Deref for Route { - type Target = Vec; +impl Deref for RouteHint { + type Target = Vec; - fn deref(&self) -> &Vec { + fn deref(&self) -> &Vec { &self.0 } } @@ -1458,18 +1443,22 @@ mod test { .build_raw(); assert_eq!(long_desc_res, Err(CreationError::DescriptionTooLong)); - let route_hop = RouteHop { - pubkey: PublicKey::from_slice( + let route_hop = RouteHintHop { + src_node_id: PublicKey::from_slice( &[ 0x03, 0x9e, 0x03, 0xa9, 0x01, 0xb8, 0x55, 0x34, 0xff, 0x1e, 0x92, 0xc4, 0x3c, 0x74, 0x43, 0x1f, 0x7c, 0xe7, 0x20, 0x46, 0x06, 0x0f, 0xcf, 0x7a, 0x95, 0xc3, 0x7e, 0x14, 0x8f, 0x78, 0xc7, 0x72, 0x55 ][..] ).unwrap(), - short_channel_id: [0; 8], - fee_base_msat: 0, - fee_proportional_millionths: 0, + short_channel_id: 0, + fees: RoutingFees { + base_msat: 0, + proportional_millionths: 0, + }, cltv_expiry_delta: 0, + htlc_minimum_msat: None, + htlc_maximum_msat: None, }; let too_long_route = vec![route_hop; 13]; let long_route_res = builder.clone() @@ -1505,36 +1494,52 @@ mod test { let public_key = PublicKey::from_secret_key(&secp_ctx, &private_key); let route_1 = vec![ - RouteHop { - pubkey: public_key.clone(), - short_channel_id: [123; 8], - fee_base_msat: 2, - fee_proportional_millionths: 1, + RouteHintHop { + src_node_id: public_key.clone(), + short_channel_id: de::parse_int_be(&[123; 8], 256).expect("short chan ID slice too big?"), + fees: RoutingFees { + base_msat: 2, + proportional_millionths: 1, + }, cltv_expiry_delta: 145, + htlc_minimum_msat: None, + htlc_maximum_msat: None, }, - RouteHop { - pubkey: public_key.clone(), - short_channel_id: [42; 8], - fee_base_msat: 3, - fee_proportional_millionths: 2, + RouteHintHop { + src_node_id: public_key.clone(), + short_channel_id: de::parse_int_be(&[42; 8], 256).expect("short chan ID slice too big?"), + fees: RoutingFees { + base_msat: 3, + proportional_millionths: 2, + }, cltv_expiry_delta: 146, + htlc_minimum_msat: None, + htlc_maximum_msat: None, } ]; let route_2 = vec![ - RouteHop { - pubkey: public_key.clone(), - short_channel_id: [0; 8], - fee_base_msat: 4, - fee_proportional_millionths: 3, + RouteHintHop { + src_node_id: public_key.clone(), + short_channel_id: 0, + fees: RoutingFees { + base_msat: 4, + proportional_millionths: 3, + }, cltv_expiry_delta: 147, + htlc_minimum_msat: None, + htlc_maximum_msat: None, }, - RouteHop { - pubkey: public_key.clone(), - short_channel_id: [1; 8], - fee_base_msat: 5, - fee_proportional_millionths: 4, + RouteHintHop { + src_node_id: public_key.clone(), + short_channel_id: de::parse_int_be(&[1; 8], 256).expect("short chan ID slice too big?"), + fees: RoutingFees { + base_msat: 5, + proportional_millionths: 4, + }, cltv_expiry_delta: 148, + htlc_minimum_msat: None, + htlc_maximum_msat: None, } ]; @@ -1568,7 +1573,7 @@ mod test { assert_eq!(invoice.expiry_time(), Duration::from_secs(54321)); assert_eq!(invoice.min_final_cltv_expiry(), Some(&144)); assert_eq!(invoice.fallbacks(), vec![&Fallback::PubKeyHash([0;20])]); - assert_eq!(invoice.routes(), vec![&Route(route_1), &Route(route_2)]); + assert_eq!(invoice.routes(), vec![&RouteHint(route_1), &RouteHint(route_2)]); assert_eq!( invoice.description(), InvoiceDescription::Hash(&Sha256(sha256::Hash::from_slice(&[3;32][..]).unwrap())) diff --git a/lightning-invoice/src/ser.rs b/lightning-invoice/src/ser.rs index 2b4332f86..83888e826 100644 --- a/lightning-invoice/src/ser.rs +++ b/lightning-invoice/src/ser.rs @@ -365,22 +365,26 @@ impl Base32Len for Fallback { } } -impl ToBase32 for Route { +impl ToBase32 for RouteHint { fn write_base32(&self, writer: &mut W) -> Result<(), ::Err> { let mut converter = BytesToBase32::new(writer); for hop in self.iter() { - converter.append(&hop.pubkey.serialize()[..])?; - converter.append(&hop.short_channel_id[..])?; + converter.append(&hop.src_node_id.serialize()[..])?; + let short_channel_id = try_stretch( + encode_int_be_base256(hop.short_channel_id), + 8 + ).expect("sizeof(u64) == 8"); + converter.append(&short_channel_id)?; let fee_base_msat = try_stretch( - encode_int_be_base256(hop.fee_base_msat), + encode_int_be_base256(hop.fees.base_msat), 4 ).expect("sizeof(u32) == 4"); converter.append(&fee_base_msat)?; let fee_proportional_millionths = try_stretch( - encode_int_be_base256(hop.fee_proportional_millionths), + encode_int_be_base256(hop.fees.proportional_millionths), 4 ).expect("sizeof(u32) == 4"); converter.append(&fee_proportional_millionths)?; @@ -397,7 +401,7 @@ impl ToBase32 for Route { } } -impl Base32Len for Route { +impl Base32Len for RouteHint { fn base32_len(&self) -> usize { bytes_size_to_base32_size(self.0.len() * 51) } diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 0ad307cdf..08fe95d23 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -117,8 +117,8 @@ impl Readable for Route { } /// A channel descriptor which provides a last-hop route to get_route -#[derive(Clone)] -pub struct RouteHint { +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct RouteHintHop { /// The node_id of the non-target end of the route pub src_node_id: PublicKey, /// The short_channel_id of this channel @@ -176,7 +176,7 @@ struct DummyDirectionalChannelInfo { /// These fee values are useful to choose hops as we traverse the graph "payee-to-payer". #[derive(Clone)] struct PathBuildingHop<'a> { - // The RouteHint fields which will eventually be used if this hop is used in a final Route. + // The RouteHintHop fields which will eventually be used if this hop is used in a final Route. // Note that node_features is calculated separately after our initial graph walk. pubkey: PublicKey, short_channel_id: u64, @@ -353,7 +353,7 @@ fn compute_fees(amount_msat: u64, channel_fees: RoutingFees) -> Option { /// equal), however the enabled/disabled bit on such channels as well as the /// htlc_minimum_msat/htlc_maximum_msat *are* checked as they may change based on the receiving node. pub fn get_route(our_node_id: &PublicKey, network: &NetworkGraph, payee: &PublicKey, payee_features: Option, first_hops: Option<&[&ChannelDetails]>, - last_hops: &[&RouteHint], final_value_msat: u64, final_cltv: u32, logger: L) -> Result where L::Target: Logger { + last_hops: &[&RouteHintHop], final_value_msat: u64, final_cltv: u32, logger: L) -> Result where L::Target: Logger { // TODO: Obviously *only* using total fee cost sucks. We should consider weighting by // uptime/success in using a node in the past. if *payee == *our_node_id { @@ -1163,7 +1163,7 @@ pub fn get_route(our_node_id: &PublicKey, network: &NetworkGraph, paye #[cfg(test)] mod tests { - use routing::router::{get_route, RouteHint, RoutingFees}; + use routing::router::{get_route, RouteHintHop, RoutingFees}; use routing::network_graph::{NetworkGraph, NetGraphMsgHandler}; use ln::features::{ChannelFeatures, InitFeatures, InvoiceFeatures, NodeFeatures}; use ln::msgs::{ErrorAction, LightningError, OptionalField, UnsignedChannelAnnouncement, ChannelAnnouncement, RoutingMessageHandler, @@ -2084,19 +2084,19 @@ mod tests { assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags(13)); } - fn last_hops(nodes: &Vec) -> Vec { + fn last_hops(nodes: &Vec) -> Vec { let zero_fees = RoutingFees { base_msat: 0, proportional_millionths: 0, }; - vec!(RouteHint { + vec!(RouteHintHop { src_node_id: nodes[3].clone(), short_channel_id: 8, fees: zero_fees, cltv_expiry_delta: (8 << 8) | 1, htlc_minimum_msat: None, htlc_maximum_msat: None, - }, RouteHint { + }, RouteHintHop { src_node_id: nodes[4].clone(), short_channel_id: 9, fees: RoutingFees { @@ -2106,7 +2106,7 @@ mod tests { cltv_expiry_delta: (9 << 8) | 1, htlc_minimum_msat: None, htlc_maximum_msat: None, - }, RouteHint { + }, RouteHintHop { src_node_id: nodes[5].clone(), short_channel_id: 10, fees: zero_fees, @@ -2124,7 +2124,7 @@ mod tests { // Simple test across 2, 3, 5, and 4 via a last_hop channel // First check that lst hop can't have its source as the payee. - let invalid_last_hop = RouteHint { + let invalid_last_hop = RouteHintHop { src_node_id: nodes[6], short_channel_id: 8, fees: RoutingFees { @@ -2309,7 +2309,7 @@ mod tests { let target_node_id = PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&hex::decode(format!("{:02}", 43).repeat(32)).unwrap()[..]).unwrap()); // If we specify a channel to a middle hop, that overrides our local channel view and that gets used - let last_hops = vec![RouteHint { + let last_hops = vec![RouteHintHop { src_node_id: middle_node_id, short_channel_id: 8, fees: RoutingFees {