**/*.rs.bk
Cargo.lock
.idea
-
+lightning/target
use lightning::ln::features::{ChannelFeatures, InitFeatures, NodeFeatures};
use lightning::ln::msgs::{CommitmentUpdate, ChannelMessageHandler, DecodeError, UpdateAddHTLC, Init};
use lightning::ln::script::ShutdownScript;
-use lightning::util::enforcing_trait_impls::{EnforcingSigner, INITIAL_REVOKED_COMMITMENT_NUMBER};
+use lightning::util::enforcing_trait_impls::{EnforcingSigner, EnforcementState};
use lightning::util::errors::APIError;
use lightning::util::events;
use lightning::util::logger::Logger;
struct KeyProvider {
node_id: u8,
rand_bytes_id: atomic::AtomicU32,
- revoked_commitments: Mutex<HashMap<[u8;32], Arc<Mutex<u64>>>>,
+ enforcement_states: Mutex<HashMap<[u8;32], Arc<Mutex<EnforcementState>>>>,
}
impl KeysInterface for KeyProvider {
type Signer = EnforcingSigner;
channel_value_satoshis,
[0; 32],
);
- let revoked_commitment = self.make_revoked_commitment_cell(keys.commitment_seed);
+ let revoked_commitment = self.make_enforcement_state_cell(keys.commitment_seed);
EnforcingSigner::new_with_revoked(keys, revoked_commitment, false)
}
let mut reader = std::io::Cursor::new(buffer);
let inner: InMemorySigner = Readable::read(&mut reader)?;
- let revoked_commitment = self.make_revoked_commitment_cell(inner.commitment_seed);
-
- let last_commitment_number = Readable::read(&mut reader)?;
+ let state = self.make_enforcement_state_cell(inner.commitment_seed);
Ok(EnforcingSigner {
inner,
- last_commitment_number: Arc::new(Mutex::new(last_commitment_number)),
- revoked_commitment,
+ state,
disable_revocation_policy_check: false,
})
}
}
impl KeyProvider {
- fn make_revoked_commitment_cell(&self, commitment_seed: [u8; 32]) -> Arc<Mutex<u64>> {
- let mut revoked_commitments = self.revoked_commitments.lock().unwrap();
+ fn make_enforcement_state_cell(&self, commitment_seed: [u8; 32]) -> Arc<Mutex<EnforcementState>> {
+ let mut revoked_commitments = self.enforcement_states.lock().unwrap();
if !revoked_commitments.contains_key(&commitment_seed) {
- revoked_commitments.insert(commitment_seed, Arc::new(Mutex::new(INITIAL_REVOKED_COMMITMENT_NUMBER)));
+ revoked_commitments.insert(commitment_seed, Arc::new(Mutex::new(EnforcementState::new())));
}
let cell = revoked_commitments.get(&commitment_seed).unwrap();
Arc::clone(cell)
macro_rules! make_node {
($node_id: expr, $fee_estimator: expr) => { {
let logger: Arc<dyn Logger> = Arc::new(test_logger::TestLogger::new($node_id.to_string(), out.clone()));
- let keys_manager = Arc::new(KeyProvider { node_id: $node_id, rand_bytes_id: atomic::AtomicU32::new(0), revoked_commitments: Mutex::new(HashMap::new()) });
+ let keys_manager = Arc::new(KeyProvider { node_id: $node_id, rand_bytes_id: atomic::AtomicU32::new(0), enforcement_states: Mutex::new(HashMap::new()) });
let monitor = Arc::new(TestChainMonitor::new(broadcast.clone(), logger.clone(), $fee_estimator.clone(), Arc::new(TestPersister{}), Arc::clone(&keys_manager)));
let mut config = UserConfig::default();
use lightning::util::config::UserConfig;
use lightning::util::errors::APIError;
use lightning::util::events::Event;
-use lightning::util::enforcing_trait_impls::EnforcingSigner;
+use lightning::util::enforcing_trait_impls::{EnforcingSigner, EnforcementState};
use lightning::util::logger::Logger;
use lightning::util::ser::Readable;
(ctr >> 8*7) as u8, (ctr >> 8*6) as u8, (ctr >> 8*5) as u8, (ctr >> 8*4) as u8, (ctr >> 8*3) as u8, (ctr >> 8*2) as u8, (ctr >> 8*1) as u8, 14, (ctr >> 8*0) as u8]
}
- fn read_chan_signer(&self, data: &[u8]) -> Result<EnforcingSigner, DecodeError> {
- EnforcingSigner::read(&mut std::io::Cursor::new(data))
+ fn read_chan_signer(&self, mut data: &[u8]) -> Result<EnforcingSigner, DecodeError> {
+ let inner: InMemorySigner = Readable::read(&mut data)?;
+ let state = Arc::new(Mutex::new(EnforcementState::new()));
+
+ Ok(EnforcingSigner::new_with_revoked(
+ inner,
+ state,
+ false
+ ))
}
fn sign_invoice(&self, _invoice_preimage: Vec<u8>) -> Result<RecoverableSignature, ()> {
bitcoin_hashes = "0.10"
[dev-dependencies]
+hex = "0.3"
lightning = { version = "0.0.100", path = "../lightning", features = ["_test_utils"] }
} else if ['m', 'u', 'n', 'p'].contains(&read_symbol) {
Ok(States::ParseAmountSiPrefix)
} else {
- Err(super::ParseError::MalformedHRP)
+ Err(super::ParseError::UnknownSiPrefix)
}
},
States::ParseAmountSiPrefix => Err(super::ParseError::MalformedHRP),
/// ```
/// use lightning_invoice::Invoice;
///
-/// let invoice = "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp\
-/// l2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d7\
-/// 3gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ec\
-/// ky03ylcqca784w";
+///
+/// let invoice = "lnbc100p1psj9jhxdqud3jxktt5w46x7unfv9kz6mn0v3jsnp4q0d3p2sfluzdx45tqcs\
+/// h2pu5qc7lgq0xs578ngs6s0s68ua4h7cvspp5q6rmq35js88zp5dvwrv9m459tnk2zunwj5jalqtyxqulh0l\
+/// 5gflssp5nf55ny5gcrfl30xuhzj3nphgj27rstekmr9fw3ny5989s300gyus9qyysgqcqpcrzjqw2sxwe993\
+/// h5pcm4dxzpvttgza8zhkqxpgffcrf5v25nwpr3cmfg7z54kuqq8rgqqqqqqqq2qqqqq9qq9qrzjqd0ylaqcl\
+/// j9424x9m8h2vcukcgnm6s56xfgu3j78zyqzhgs4hlpzvznlugqq9vsqqqqqqqlgqqqqqeqq9qrzjqwldmj9d\
+/// ha74df76zhx6l9we0vjdquygcdt3kssupehe64g6yyp5yz5rhuqqwccqqyqqqqlgqqqqjcqq9qrzjqf9e58a\
+/// guqr0rcun0ajlvmzq3ek63cw2w282gv3z5uupmuwvgjtq2z55qsqqg6qqqyqqqrtnqqqzq3cqygrzjqvphms\
+/// ywntrrhqjcraumvc4y6r8v4z5v593trte429v4hredj7ms5z52usqq9ngqqqqqqqlgqqqqqqgq9qrzjq2v0v\
+/// p62g49p7569ev48cmulecsxe59lvaw3wlxm7r982zxa9zzj7z5l0cqqxusqqyqqqqlgqqqqqzsqygarl9fh3\
+/// 8s0gyuxjjgux34w75dnc6xp2l35j7es3jd4ugt3lu0xzre26yg5m7ke54n2d5sym4xcmxtl8238xxvw5h5h5\
+/// j5r6drg6k6zcqj0fcwg";
///
/// assert!(invoice.parse::<Invoice>().is_ok());
/// ```
/// ```
/// use lightning_invoice::*;
///
-/// let invoice = "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp\
-/// l2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d7\
-/// 3gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ec\
-/// ky03ylcqca784w";
+/// let invoice = "lnbc100p1psj9jhxdqud3jxktt5w46x7unfv9kz6mn0v3jsnp4q0d3p2sfluzdx45tqcs\
+/// h2pu5qc7lgq0xs578ngs6s0s68ua4h7cvspp5q6rmq35js88zp5dvwrv9m459tnk2zunwj5jalqtyxqulh0l\
+/// 5gflssp5nf55ny5gcrfl30xuhzj3nphgj27rstekmr9fw3ny5989s300gyus9qyysgqcqpcrzjqw2sxwe993\
+/// h5pcm4dxzpvttgza8zhkqxpgffcrf5v25nwpr3cmfg7z54kuqq8rgqqqqqqqq2qqqqq9qq9qrzjqd0ylaqcl\
+/// j9424x9m8h2vcukcgnm6s56xfgu3j78zyqzhgs4hlpzvznlugqq9vsqqqqqqqlgqqqqqeqq9qrzjqwldmj9d\
+/// ha74df76zhx6l9we0vjdquygcdt3kssupehe64g6yyp5yz5rhuqqwccqqyqqqqlgqqqqjcqq9qrzjqf9e58a\
+/// guqr0rcun0ajlvmzq3ek63cw2w282gv3z5uupmuwvgjtq2z55qsqqg6qqqyqqqrtnqqqzq3cqygrzjqvphms\
+/// ywntrrhqjcraumvc4y6r8v4z5v593trte429v4hredj7ms5z52usqq9ngqqqqqqqlgqqqqqqgq9qrzjq2v0v\
+/// p62g49p7569ev48cmulecsxe59lvaw3wlxm7r982zxa9zzj7z5l0cqqxusqqyqqqqlgqqqqqzsqygarl9fh3\
+/// 8s0gyuxjjgux34w75dnc6xp2l35j7es3jd4ugt3lu0xzre26yg5m7ke54n2d5sym4xcmxtl8238xxvw5h5h5\
+/// j5r6drg6k6zcqj0fcwg";
///
/// let parsed_1 = invoice.parse::<Invoice>();
///
Ok(field) => {
parts.push(RawTaggedField::KnownSemantics(field))
},
- Err(ParseError::Skip) => {
+ Err(ParseError::Skip)|Err(ParseError::Bech32Error(bech32::Error::InvalidLength)) => {
parts.push(RawTaggedField::UnknownSemantics(field.into()))
},
Err(e) => {return Err(e)}
///
/// ```
/// extern crate secp256k1;
+/// extern crate lightning;
/// extern crate lightning_invoice;
/// extern crate bitcoin_hashes;
///
/// use secp256k1::Secp256k1;
/// use secp256k1::key::SecretKey;
///
+/// use lightning::ln::PaymentSecret;
+///
/// use lightning_invoice::{Currency, InvoiceBuilder};
///
/// # fn main() {
/// ).unwrap();
///
/// let payment_hash = sha256::Hash::from_slice(&[0; 32][..]).unwrap();
+/// let payment_secret = PaymentSecret([42u8; 32]);
///
/// let invoice = InvoiceBuilder::new(Currency::Bitcoin)
/// .description("Coins pls!".into())
/// .payment_hash(payment_hash)
+/// .payment_secret(payment_secret)
/// .current_timestamp()
/// .min_final_cltv_expiry(144)
/// .build_signed(|hash| {
}
/// Enum representing the crypto currencies (or networks) supported by this library
-#[derive(Eq, PartialEq, Debug, Clone)]
+#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub enum Currency {
/// Bitcoin mainnet
Bitcoin,
/// Tagged field which may have an unknown tag
///
/// (C-not exported) as we don't currently support TaggedField
-#[derive(Eq, PartialEq, Debug, Clone)]
+#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub enum RawTaggedField {
/// Parsed tagged field with known tag
KnownSemantics(TaggedField),
/// (C-not exported) As we don't yet support enum variants with the same name the struct contained
/// in the variant.
#[allow(missing_docs)]
-#[derive(Eq, PartialEq, Debug, Clone)]
+#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub enum TaggedField {
PaymentHash(Sha256),
Description(Description),
}
/// SHA-256 hash
-#[derive(Eq, PartialEq, Debug, Clone)]
+#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct Sha256(pub sha256::Hash);
/// Description string
///
/// # Invariants
/// The description can be at most 639 __bytes__ long
-#[derive(Eq, PartialEq, Debug, Clone)]
+#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct Description(String);
/// Payee public key
-#[derive(Eq, PartialEq, Debug, Clone)]
+#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct PayeePubKey(pub PublicKey);
/// Positive duration that defines when (relatively to the timestamp) in the future the invoice
/// The number of seconds this expiry time represents has to be in the range
/// `0...(SYSTEM_TIME_MAX_UNIX_TIMESTAMP - MAX_EXPIRY_TIME)` to avoid overflows when adding it to a
/// timestamp
-#[derive(Eq, PartialEq, Debug, Clone)]
+#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct ExpiryTime(Duration);
/// `min_final_cltv_expiry` to use for the last HTLC in the route
-#[derive(Eq, PartialEq, Debug, Clone)]
+#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct MinFinalCltvExpiry(pub u64);
// TODO: better types instead onf byte arrays
/// Fallback address in case no LN payment is possible
#[allow(missing_docs)]
-#[derive(Eq, PartialEq, Debug, Clone)]
+#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub enum Fallback {
SegWitProgram {
version: u5,
}
/// Recoverable signature
-#[derive(Eq, PartialEq, Debug, Clone)]
+#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InvoiceSignature(pub RecoverableSignature);
/// Private routing information
/// # Invariants
/// The encoded route has to be <1024 5bit characters long (<=639 bytes or <=12 hops)
///
-#[derive(Eq, PartialEq, Debug, Clone)]
+#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct PrivateRoute(RouteHint);
/// Tag constants as specified in BOLT11
}
}
- /// Sets the amount in pico BTC. The optimal SI prefix is choosen automatically.
- pub fn amount_pico_btc(mut self, amount: u64) -> Self {
+ /// Sets the amount in millisatoshis. The optimal SI prefix is chosen automatically.
+ pub fn amount_milli_satoshis(mut self, amount_msat: u64) -> Self {
+ let amount = amount_msat * 10; // Invoices are denominated in "pico BTC"
let biggest_possible_si_prefix = SiPrefix::values_desc()
.iter()
.find(|prefix| amount % prefix.multiplier() == 0)
}
}
-impl<S: tb::Bool> InvoiceBuilder<tb::True, tb::True, tb::True, tb::True, S> {
+impl InvoiceBuilder<tb::True, tb::True, tb::True, tb::True, tb::True> {
/// Builds and signs an invoice using the supplied `sign_function`. This function MAY NOT fail
/// and MUST produce a recoverable signature valid for the given hash and if applicable also for
/// the included payee public key.
invoice.check_field_counts().expect("should be ensured by type signature of builder");
invoice.check_feature_bits().expect("should be ensured by type signature of builder");
+ invoice.check_amount().expect("should be ensured by type signature of builder");
Ok(invoice)
}
return Err(SemanticError::MultipleDescriptions);
}
+ self.check_payment_secret()?;
+
Ok(())
}
- /// Check that feature bits are set as required
- fn check_feature_bits(&self) -> Result<(), SemanticError> {
- // "If the payment_secret feature is set, MUST include exactly one s field."
+ /// Checks that there is exactly one payment secret field
+ fn check_payment_secret(&self) -> Result<(), SemanticError> {
+ // "A writer MUST include exactly one `s` field."
let payment_secret_count = self.tagged_fields().filter(|&tf| match *tf {
TaggedField::PaymentSecret(_) => true,
_ => false,
}).count();
- if payment_secret_count > 1 {
+ if payment_secret_count < 1 {
+ return Err(SemanticError::NoPaymentSecret);
+ } else if payment_secret_count > 1 {
return Err(SemanticError::MultiplePaymentSecrets);
}
+ Ok(())
+ }
+
+ /// Check that amount is a whole number of millisatoshis
+ fn check_amount(&self) -> Result<(), SemanticError> {
+ if let Some(amount_pico_btc) = self.amount_pico_btc() {
+ if amount_pico_btc % 10 != 0 {
+ return Err(SemanticError::ImpreciseAmount);
+ }
+ }
+ Ok(())
+ }
+
+ /// Check that feature bits are set as required
+ fn check_feature_bits(&self) -> Result<(), SemanticError> {
+ self.check_payment_secret()?;
+
// "A writer MUST set an s field if and only if the payment_secret feature is set."
- let has_payment_secret = payment_secret_count == 1;
+ // (this requirement has been since removed, and we now require the payment secret
+ // feature bit always).
let features = self.tagged_fields().find(|&tf| match *tf {
TaggedField::Features(_) => true,
_ => false,
});
match features {
- None if has_payment_secret => Err(SemanticError::InvalidFeatures),
- None => Ok(()),
+ None => Err(SemanticError::InvalidFeatures),
Some(TaggedField::Features(features)) => {
- if features.supports_payment_secret() && has_payment_secret {
- Ok(())
- } else if has_payment_secret {
+ if features.requires_unknown_bits() {
Err(SemanticError::InvalidFeatures)
- } else if features.supports_payment_secret() {
+ } else if !features.supports_payment_secret() {
Err(SemanticError::InvalidFeatures)
} else {
Ok(())
match self.signed_invoice.recover_payee_pub_key() {
Err(secp256k1::Error::InvalidRecoveryId) =>
return Err(SemanticError::InvalidRecoveryId),
- Err(_) => panic!("no other error may occur"),
+ Err(secp256k1::Error::InvalidSignature) =>
+ return Err(SemanticError::InvalidSignature),
+ Err(e) => panic!("no other error may occur, got {:?}", e),
Ok(_) => {},
}
/// ```
/// use lightning_invoice::*;
///
- /// let invoice = "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp\
- /// l2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d7\
- /// 3gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ec\
- /// ky03ylcqca784w";
+ /// let invoice = "lnbc100p1psj9jhxdqud3jxktt5w46x7unfv9kz6mn0v3jsnp4q0d3p2sfluzdx45tqcs\
+ /// h2pu5qc7lgq0xs578ngs6s0s68ua4h7cvspp5q6rmq35js88zp5dvwrv9m459tnk2zunwj5jalqtyxqulh0l\
+ /// 5gflssp5nf55ny5gcrfl30xuhzj3nphgj27rstekmr9fw3ny5989s300gyus9qyysgqcqpcrzjqw2sxwe993\
+ /// h5pcm4dxzpvttgza8zhkqxpgffcrf5v25nwpr3cmfg7z54kuqq8rgqqqqqqqq2qqqqq9qq9qrzjqd0ylaqcl\
+ /// j9424x9m8h2vcukcgnm6s56xfgu3j78zyqzhgs4hlpzvznlugqq9vsqqqqqqqlgqqqqqeqq9qrzjqwldmj9d\
+ /// ha74df76zhx6l9we0vjdquygcdt3kssupehe64g6yyp5yz5rhuqqwccqqyqqqqlgqqqqjcqq9qrzjqf9e58a\
+ /// guqr0rcun0ajlvmzq3ek63cw2w282gv3z5uupmuwvgjtq2z55qsqqg6qqqyqqqrtnqqqzq3cqygrzjqvphms\
+ /// ywntrrhqjcraumvc4y6r8v4z5v593trte429v4hredj7ms5z52usqq9ngqqqqqqqlgqqqqqqgq9qrzjq2v0v\
+ /// p62g49p7569ev48cmulecsxe59lvaw3wlxm7r982zxa9zzj7z5l0cqqxusqqyqqqqlgqqqqqzsqygarl9fh3\
+ /// 8s0gyuxjjgux34w75dnc6xp2l35j7es3jd4ugt3lu0xzre26yg5m7ke54n2d5sym4xcmxtl8238xxvw5h5h5\
+ /// j5r6drg6k6zcqj0fcwg";
///
/// let signed = invoice.parse::<SignedRawInvoice>().unwrap();
///
invoice.check_field_counts()?;
invoice.check_feature_bits()?;
invoice.check_signature()?;
+ invoice.check_amount()?;
Ok(invoice)
}
}
/// Get the payment secret if one was included in the invoice
- pub fn payment_secret(&self) -> Option<&PaymentSecret> {
- self.signed_invoice.payment_secret()
+ pub fn payment_secret(&self) -> &PaymentSecret {
+ self.signed_invoice.payment_secret().expect("was checked by constructor")
}
/// Get the invoice features if they were included in the invoice
/// The invoice contains multiple descriptions and/or description hashes which isn't allowed
MultipleDescriptions,
+ /// The invoice is missing the mandatory payment secret, which all modern lightning nodes
+ /// should provide.
+ NoPaymentSecret,
+
/// The invoice contains multiple payment secrets
MultiplePaymentSecrets,
/// The invoice's signature is invalid
InvalidSignature,
+
+ /// The invoice's amount was not a whole number of millisatoshis
+ ImpreciseAmount,
}
impl Display for SemanticError {
SemanticError::MultiplePaymentHashes => f.write_str("The invoice has multiple payment hashes which isn't allowed"),
SemanticError::NoDescription => f.write_str("No description or description hash are part of the invoice"),
SemanticError::MultipleDescriptions => f.write_str("The invoice contains multiple descriptions and/or description hashes which isn't allowed"),
+ SemanticError::NoPaymentSecret => f.write_str("The invoice is missing the mandatory payment secret"),
SemanticError::MultiplePaymentSecrets => f.write_str("The invoice contains multiple payment secrets"),
SemanticError::InvalidFeatures => f.write_str("The invoice's features are invalid"),
SemanticError::InvalidRecoveryId => f.write_str("The recovery id doesn't fit the signature/pub key"),
SemanticError::InvalidSignature => f.write_str("The invoice's signature is invalid"),
+ SemanticError::ImpreciseAmount => f.write_str("The invoice's amount was not a whole number of millisatoshis"),
}
}
}
let invoice = invoice_template.clone();
invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key)))
}.unwrap();
- assert!(Invoice::from_signed(invoice).is_ok());
+ assert_eq!(Invoice::from_signed(invoice), Err(SemanticError::NoPaymentSecret));
// No payment secret or feature bits
let invoice = {
invoice.data.tagged_fields.push(Features(InvoiceFeatures::empty()).into());
invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key)))
}.unwrap();
- assert!(Invoice::from_signed(invoice).is_ok());
+ assert_eq!(Invoice::from_signed(invoice), Err(SemanticError::NoPaymentSecret));
// Missing payment secret
let invoice = {
invoice.data.tagged_fields.push(Features(InvoiceFeatures::known()).into());
invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key)))
}.unwrap();
- assert_eq!(Invoice::from_signed(invoice), Err(SemanticError::InvalidFeatures));
+ assert_eq!(Invoice::from_signed(invoice), Err(SemanticError::NoPaymentSecret));
// Multiple payment secrets
let invoice = {
.current_timestamp();
let invoice = builder.clone()
- .amount_pico_btc(15000)
+ .amount_milli_satoshis(1500)
.build_raw()
.unwrap();
let invoice = builder.clone()
- .amount_pico_btc(1500)
+ .amount_milli_satoshis(150)
.build_raw()
.unwrap();
let sign_error_res = builder.clone()
.description("Test".into())
+ .payment_secret(PaymentSecret([0; 32]))
.try_build_signed(|_| {
Err("ImaginaryError")
});
]);
let builder = InvoiceBuilder::new(Currency::BitcoinTestnet)
- .amount_pico_btc(123)
+ .amount_milli_satoshis(123)
.timestamp(UNIX_EPOCH + Duration::from_secs(1234567))
.payee_pub_key(public_key.clone())
.expiry_time(Duration::from_secs(54321))
assert!(invoice.check_signature().is_ok());
assert_eq!(invoice.tagged_fields().count(), 10);
- assert_eq!(invoice.amount_pico_btc(), Some(123));
+ assert_eq!(invoice.amount_pico_btc(), Some(1230));
assert_eq!(invoice.currency(), Currency::BitcoinTestnet);
assert_eq!(
invoice.timestamp().duration_since(UNIX_EPOCH).unwrap().as_secs(),
InvoiceDescription::Hash(&Sha256(sha256::Hash::from_slice(&[3;32][..]).unwrap()))
);
assert_eq!(invoice.payment_hash(), &sha256::Hash::from_slice(&[21;32][..]).unwrap());
- assert_eq!(invoice.payment_secret(), Some(&PaymentSecret([42; 32])));
+ assert_eq!(invoice.payment_secret(), &PaymentSecret([42; 32]));
assert_eq!(invoice.features(), Some(&InvoiceFeatures::known()));
let raw_invoice = builder.build_raw().unwrap();
let signed_invoice = InvoiceBuilder::new(Currency::Bitcoin)
.description("Test".into())
.payment_hash(sha256::Hash::from_slice(&[0;32][..]).unwrap())
+ .payment_secret(PaymentSecret([0; 32]))
.current_timestamp()
.build_raw()
.unwrap()
.basic_mpp()
.min_final_cltv_expiry(MIN_FINAL_CLTV_EXPIRY.into());
if let Some(amt) = amt_msat {
- invoice = invoice.amount_pico_btc(amt * 10);
+ invoice = invoice.amount_milli_satoshis(amt);
}
for hint in route_hints {
invoice = invoice.private_route(hint);
let payment_event = {
let mut payment_hash = PaymentHash([0; 32]);
payment_hash.0.copy_from_slice(&invoice.payment_hash().as_ref()[0..32]);
- nodes[0].node.send_payment(&route, payment_hash, &Some(invoice.payment_secret().unwrap().clone())).unwrap();
+ nodes[0].node.send_payment(&route, payment_hash, &Some(invoice.payment_secret().clone())).unwrap();
let mut added_monitors = nodes[0].chain_monitor.added_monitors.lock().unwrap();
assert_eq!(added_monitors.len(), 1);
added_monitors.clear();
+extern crate bech32;
extern crate bitcoin_hashes;
extern crate lightning;
extern crate lightning_invoice;
extern crate secp256k1;
+extern crate hex;
use bitcoin_hashes::hex::FromHex;
-use bitcoin_hashes::sha256;
+use bitcoin_hashes::{sha256, Hash};
+use bech32::u5;
use lightning::ln::PaymentSecret;
+use lightning::routing::router::{RouteHint, RouteHintHop};
+use lightning::routing::network_graph::RoutingFees;
use lightning_invoice::*;
-use secp256k1::Secp256k1;
-use secp256k1::key::SecretKey;
+use secp256k1::PublicKey;
use secp256k1::recovery::{RecoverableSignature, RecoveryId};
+use std::collections::HashSet;
use std::time::{Duration, UNIX_EPOCH};
+use std::str::FromStr;
-// TODO: add more of the examples from BOLT11 and generate ones causing SemanticErrors
-
-fn get_test_tuples() -> Vec<(String, SignedRawInvoice, Option<SemanticError>)> {
+fn get_test_tuples() -> Vec<(String, SignedRawInvoice, bool, bool)> {
vec![
(
- "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmw\
- wd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d73gafnh3cax9rn449d9p5uxz9\
- ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ecky03ylcqca784w".to_owned(),
+ "lnbc1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq9qrsgq357wnc5r2ueh7ck6q93dj32dlqnls087fxdwk8qakdyafkq3yap9us6v52vjjsrvywa6rt52cm9r9zqt8r2t7mlcwspyetp5h2tztugp9lfyql".to_owned(),
InvoiceBuilder::new(Currency::Bitcoin)
.timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
+ .payment_secret(PaymentSecret([0x11; 32]))
.payment_hash(sha256::Hash::from_hex(
"0001020304050607080900010203040506070809000102030405060708090102"
).unwrap())
.unwrap()
.sign(|_| {
RecoverableSignature::from_compact(
- & [
- 0x38u8, 0xec, 0x68, 0x91, 0x34, 0x5e, 0x20, 0x41, 0x45, 0xbe, 0x8a,
- 0x3a, 0x99, 0xde, 0x38, 0xe9, 0x8a, 0x39, 0xd6, 0xa5, 0x69, 0x43,
- 0x4e, 0x18, 0x45, 0xc8, 0xaf, 0x72, 0x05, 0xaf, 0xcf, 0xcc, 0x7f,
- 0x42, 0x5f, 0xcd, 0x14, 0x63, 0xe9, 0x3c, 0x32, 0x88, 0x1e, 0xad,
- 0x0d, 0x6e, 0x35, 0x6d, 0x46, 0x7e, 0xc8, 0xc0, 0x25, 0x53, 0xf9,
- 0xaa, 0xb1, 0x5e, 0x57, 0x38, 0xb1, 0x1f, 0x12, 0x7f
- ],
- RecoveryId::from_i32(0).unwrap()
+ &hex::decode("8d3ce9e28357337f62da0162d9454df827f83cfe499aeb1c1db349d4d81127425e434ca29929406c23bba1ae8ac6ca32880b38d4bf6ff874024cac34ba9625f1").unwrap(),
+ RecoveryId::from_i32(1).unwrap()
)
}).unwrap(),
- None
+ false, // Same features as set in InvoiceBuilder
+ false, // No unknown fields
),
(
- "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3\
- k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch\
- 9zw97j25emudupq63nyw24cg27h2rspfj9srp".to_owned(),
+ "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu9qrsgquk0rl77nj30yxdy8j9vdx85fkpmdla2087ne0xh8nhedh8w27kyke0lp53ut353s06fv3qfegext0eh0ymjpf39tuven09sam30g4vgpfna3rh".to_owned(),
InvoiceBuilder::new(Currency::Bitcoin)
- .amount_pico_btc(2500000000)
+ .amount_milli_satoshis(250_000_000)
.timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
+ .payment_secret(PaymentSecret([0x11; 32]))
.payment_hash(sha256::Hash::from_hex(
"0001020304050607080900010203040506070809000102030405060708090102"
).unwrap())
.unwrap()
.sign(|_| {
RecoverableSignature::from_compact(
- & [
- 0xe8, 0x96, 0x39, 0xba, 0x68, 0x14, 0xe3, 0x66, 0x89, 0xd4, 0xb9, 0x1b,
- 0xf1, 0x25, 0xf1, 0x03, 0x51, 0xb5, 0x5d, 0xa0, 0x57, 0xb0, 0x06, 0x47,
- 0xa8, 0xda, 0xba, 0xeb, 0x8a, 0x90, 0xc9, 0x5f, 0x16, 0x0f, 0x9d, 0x5a,
- 0x6e, 0x0f, 0x79, 0xd1, 0xfc, 0x2b, 0x96, 0x42, 0x38, 0xb9, 0x44, 0xe2,
- 0xfa, 0x4a, 0xa6, 0x77, 0xc6, 0xf0, 0x20, 0xd4, 0x66, 0x47, 0x2a, 0xb8,
- 0x42, 0xbd, 0x75, 0x0e
- ],
+ &hex::decode("e59e3ffbd3945e4334879158d31e89b076dff54f3fa7979ae79df2db9dcaf5896cbfe1a478b8d2307e92c88139464cb7e6ef26e414c4abe33337961ddc5e8ab1").unwrap(),
+ RecoveryId::from_i32(1).unwrap()
+ )
+ }).unwrap(),
+ false, // Same features as set in InvoiceBuilder
+ false, // No unknown fields
+ ),
+ (
+ "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpu9qrsgqhtjpauu9ur7fw2thcl4y9vfvh4m9wlfyz2gem29g5ghe2aak2pm3ps8fdhtceqsaagty2vph7utlgj48u0ged6a337aewvraedendscp573dxr".to_owned(),
+ InvoiceBuilder::new(Currency::Bitcoin)
+ .amount_milli_satoshis(250_000_000)
+ .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
+ .payment_secret(PaymentSecret([0x11; 32]))
+ .payment_hash(sha256::Hash::from_hex(
+ "0001020304050607080900010203040506070809000102030405060708090102"
+ ).unwrap())
+ .description("ナンセンス 1杯".to_owned())
+ .expiry_time(Duration::from_secs(60))
+ .build_raw()
+ .unwrap()
+ .sign(|_| {
+ RecoverableSignature::from_compact(
+ &hex::decode("bae41ef385e0fc972977c7ea42b12cbd76577d2412919da8a8a22f9577b6507710c0e96dd78c821dea16453037f717f44aa7e3d196ebb18fbb97307dcb7336c3").unwrap(),
RecoveryId::from_i32(1).unwrap()
)
}).unwrap(),
- None
+ false, // Same features as set in InvoiceBuilder
+ false, // No unknown fields
),
(
- "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qq\
- dhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqscc6gd6ql3jrc5yzme8v4ntcewwz5cnw92tz0pc8qcuufvq7k\
- hhr8wpald05e92xw006sq94mg8v2ndf4sefvf9sygkshp5zfem29trqq2yxxz7".to_owned(),
+ "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qrsgq7ea976txfraylvgzuxs8kgcw23ezlrszfnh8r6qtfpr6cxga50aj6txm9rxrydzd06dfeawfk6swupvz4erwnyutnjq7x39ymw6j38gp7ynn44".to_owned(),
InvoiceBuilder::new(Currency::Bitcoin)
- .amount_pico_btc(20000000000)
+ .amount_milli_satoshis(2_000_000_000)
.timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
+ .description_hash(sha256::Hash::hash(b"One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon"))
+ .payment_secret(PaymentSecret([0x11; 32]))
.payment_hash(sha256::Hash::from_hex(
"0001020304050607080900010203040506070809000102030405060708090102"
).unwrap())
- .description_hash(sha256::Hash::from_hex(
- "3925b6f67e2c340036ed12093dd44e0368df1b6ea26c53dbe4811f58fd5db8c1"
+ .build_raw()
+ .unwrap()
+ .sign(|_| {
+ RecoverableSignature::from_compact(
+ &hex::decode("f67a5f696648fa4fb102e1a07b230e54722f8e024cee71e80b4847ac191da3fb2d2cdb28cc32344d7e9a9cf5c9b6a0ee0582ae46e9938b9c81e344a4dbb5289d").unwrap(),
+ RecoveryId::from_i32(1).unwrap()
+ )
+ }).unwrap(),
+ false, // Same features as set in InvoiceBuilder
+ false, // No unknown fields
+ ),
+ (
+ "lntb20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfpp3x9et2e20v6pu37c5d9vax37wxq72un989qrsgqdj545axuxtnfemtpwkc45hx9d2ft7x04mt8q7y6t0k2dge9e7h8kpy9p34ytyslj3yu569aalz2xdk8xkd7ltxqld94u8h2esmsmacgpghe9k8".to_owned(),
+ InvoiceBuilder::new(Currency::BitcoinTestnet)
+ .amount_milli_satoshis(2_000_000_000)
+ .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
+ .description_hash(sha256::Hash::hash(b"One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon"))
+ .payment_secret(PaymentSecret([0x11; 32]))
+ .payment_hash(sha256::Hash::from_hex(
+ "0001020304050607080900010203040506070809000102030405060708090102"
+ ).unwrap())
+ .fallback(Fallback::PubKeyHash([49, 114, 181, 101, 79, 102, 131, 200, 251, 20, 105, 89, 211, 71, 206, 48, 60, 174, 76, 167]))
+ .build_raw()
+ .unwrap()
+ .sign(|_| {
+ RecoverableSignature::from_compact(
+ &hex::decode("6ca95a74dc32e69ced6175b15a5cc56a92bf19f5dace0f134b7d94d464b9f5cf6090a18d48b243f289394d17bdf89466d8e6b37df5981f696bc3dd5986e1bee1").unwrap(),
+ RecoveryId::from_i32(1).unwrap()
+ )
+ }).unwrap(),
+ false, // Same features as set in InvoiceBuilder
+ false, // No unknown fields
+ ),
+ (
+ "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzq9qrsgqdfjcdk6w3ak5pca9hwfwfh63zrrz06wwfya0ydlzpgzxkn5xagsqz7x9j4jwe7yj7vaf2k9lqsdk45kts2fd0fkr28am0u4w95tt2nsq76cqw0".to_owned(),
+ InvoiceBuilder::new(Currency::Bitcoin)
+ .amount_milli_satoshis(2_000_000_000)
+ .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
+ .description_hash(sha256::Hash::hash(b"One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon"))
+ .payment_secret(PaymentSecret([0x11; 32]))
+ .payment_hash(sha256::Hash::from_hex(
+ "0001020304050607080900010203040506070809000102030405060708090102"
).unwrap())
+ .fallback(Fallback::PubKeyHash([4, 182, 31, 125, 193, 234, 13, 201, 148, 36, 70, 76, 196, 6, 77, 197, 100, 217, 30, 137]))
+ .private_route(RouteHint(vec![RouteHintHop {
+ src_node_id: PublicKey::from_slice(&hex::decode(
+ "029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"
+ ).unwrap()).unwrap(),
+ short_channel_id: (66051 << 40) | (263430 << 16) | 1800,
+ fees: RoutingFees { base_msat: 1, proportional_millionths: 20 },
+ cltv_expiry_delta: 3,
+ htlc_maximum_msat: None, htlc_minimum_msat: None,
+ }, RouteHintHop {
+ src_node_id: PublicKey::from_slice(&hex::decode(
+ "039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"
+ ).unwrap()).unwrap(),
+ short_channel_id: (197637 << 40) | (395016 << 16) | 2314,
+ fees: RoutingFees { base_msat: 2, proportional_millionths: 30 },
+ cltv_expiry_delta: 4,
+ htlc_maximum_msat: None, htlc_minimum_msat: None,
+ }]))
.build_raw()
.unwrap()
.sign(|_| {
RecoverableSignature::from_compact(
- & [
- 0xc6, 0x34, 0x86, 0xe8, 0x1f, 0x8c, 0x87, 0x8a, 0x10, 0x5b, 0xc9, 0xd9,
- 0x59, 0xaf, 0x19, 0x73, 0x85, 0x4c, 0x4d, 0xc5, 0x52, 0xc4, 0xf0, 0xe0,
- 0xe0, 0xc7, 0x38, 0x96, 0x03, 0xd6, 0xbd, 0xc6, 0x77, 0x07, 0xbf, 0x6b,
- 0xe9, 0x92, 0xa8, 0xce, 0x7b, 0xf5, 0x00, 0x16, 0xbb, 0x41, 0xd8, 0xa9,
- 0xb5, 0x35, 0x86, 0x52, 0xc4, 0x96, 0x04, 0x45, 0xa1, 0x70, 0xd0, 0x49,
- 0xce, 0xd4, 0x55, 0x8c
- ],
+ &hex::decode("6a6586db4e8f6d40e3a5bb92e4df5110c627e9ce493af237e20a046b4e86ea200178c59564ecf892f33a9558bf041b6ad2cb8292d7a6c351fbb7f2ae2d16b54e").unwrap(),
RecoveryId::from_i32(0).unwrap()
)
}).unwrap(),
- None
+ false, // Same features as set in InvoiceBuilder
+ false, // No unknown fields
),
(
- "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp59g4z52329g4z52329g4z52329g4z52329g4z52329g4z52329g4q9qrsgqzfhag3vsafx4e5qssalvw4rn0phsvpp3e5h2xxyk9l8fxsutvndx9t840dqvdrlu2gqmk0q8apqrgnjy9amc07hmjl9e9yzqjks5w2gqgjnyms".to_owned(),
+ "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppj3a24vwu6r8ejrss3axul8rxldph2q7z99qrsgqz6qsgww34xlatfj6e3sngrwfy3ytkt29d2qttr8qz2mnedfqysuqypgqex4haa2h8fx3wnypranf3pdwyluftwe680jjcfp438u82xqphf75ym".to_owned(),
InvoiceBuilder::new(Currency::Bitcoin)
+ .amount_milli_satoshis(2_000_000_000)
+ .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
+ .description_hash(sha256::Hash::hash(b"One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon"))
+ .payment_secret(PaymentSecret([0x11; 32]))
.payment_hash(sha256::Hash::from_hex(
"0001020304050607080900010203040506070809000102030405060708090102"
).unwrap())
- .description("coffee beans".to_string())
- .amount_pico_btc(20000000000)
+ .fallback(Fallback::ScriptHash([143, 85, 86, 59, 154, 25, 243, 33, 194, 17, 233, 185, 243, 140, 223, 104, 110, 160, 120, 69]))
+ .build_raw()
+ .unwrap()
+ .sign(|_| {
+ RecoverableSignature::from_compact(
+ &hex::decode("16810439d1a9bfd5a65acc61340dc92448bb2d456a80b58ce012b73cb5202438020500c9ab7ef5573a4d174c811f669885ae27f895bb3a3be52c243589f87518").unwrap(),
+ RecoveryId::from_i32(1).unwrap()
+ )
+ }).unwrap(),
+ false, // Same features as set in InvoiceBuilder
+ false, // No unknown fields
+ ),
+ (
+ "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppqw508d6qejxtdg4y5r3zarvary0c5xw7k9qrsgqt29a0wturnys2hhxpner2e3plp6jyj8qx7548zr2z7ptgjjc7hljm98xhjym0dg52sdrvqamxdezkmqg4gdrvwwnf0kv2jdfnl4xatsqmrnsse".to_owned(),
+ InvoiceBuilder::new(Currency::Bitcoin)
+ .amount_milli_satoshis(2_000_000_000)
.timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
- .payment_secret(PaymentSecret([42; 32]))
+ .description_hash(sha256::Hash::hash(b"One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon"))
+ .payment_secret(PaymentSecret([0x11; 32]))
+ .payment_hash(sha256::Hash::from_hex(
+ "0001020304050607080900010203040506070809000102030405060708090102"
+ ).unwrap())
+ .fallback(Fallback::SegWitProgram { version: u5::try_from_u8(0).unwrap(),
+ program: vec![117, 30, 118, 232, 25, 145, 150, 212, 84, 148, 28, 69, 209, 179, 163, 35, 241, 67, 59, 214]
+ })
.build_raw()
.unwrap()
- .sign::<_, ()>(|msg_hash| {
- let privkey = SecretKey::from_slice(&[41; 32]).unwrap();
- let secp_ctx = Secp256k1::new();
- Ok(secp_ctx.sign_recoverable(msg_hash, &privkey))
+ .sign(|_| {
+ RecoverableSignature::from_compact(
+ &hex::decode("5a8bd7b97c1cc9055ee60cf2356621f8752248e037a953886a1782b44a58f5ff2d94e6bc89b7b514541a3603bb33722b6c08aa1a3639d34becc549a99fea6eae").unwrap(),
+ RecoveryId::from_i32(0).unwrap()
+ )
+ }).unwrap(),
+ false, // Same features as set in InvoiceBuilder
+ false, // No unknown fields
+ ),
+ (
+ "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq9vlvyj8cqvq6ggvpwd53jncp9nwc47xlrsnenq2zp70fq83qlgesn4u3uyf4tesfkkwwfg3qs54qe426hp3tz7z6sweqdjg05axsrjqp9yrrwc".to_owned(),
+ InvoiceBuilder::new(Currency::Bitcoin)
+ .amount_milli_satoshis(2_000_000_000)
+ .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
+ .description_hash(sha256::Hash::hash(b"One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon"))
+ .payment_secret(PaymentSecret([0x11; 32]))
+ .payment_hash(sha256::Hash::from_hex(
+ "0001020304050607080900010203040506070809000102030405060708090102"
+ ).unwrap())
+ .fallback(Fallback::SegWitProgram { version: u5::try_from_u8(0).unwrap(),
+ program: vec![24, 99, 20, 60, 20, 197, 22, 104, 4, 189, 25, 32, 51, 86, 218, 19, 108, 152, 86, 120, 205, 77, 39, 161, 184, 198, 50, 150, 4, 144, 50, 98]
})
- .unwrap(),
- None
- )
+ .build_raw()
+ .unwrap()
+ .sign(|_| {
+ RecoverableSignature::from_compact(
+ &hex::decode("2b3ec248f80301a421817369194f012cdd8af8df1c279981420f9e901e20fa3309d791e11355e609b59ce4a220852a0cd55ab862b1785a83b206c90fa74d01c8").unwrap(),
+ RecoveryId::from_i32(1).unwrap()
+ )
+ }).unwrap(),
+ false, // Same features as set in InvoiceBuilder
+ false, // No unknown fields
+ ),
+ (
+ "lnbc9678785340p1pwmna7lpp5gc3xfm08u9qy06djf8dfflhugl6p7lgza6dsjxq454gxhj9t7a0sd8dgfkx7cmtwd68yetpd5s9xar0wfjn5gpc8qhrsdfq24f5ggrxdaezqsnvda3kkum5wfjkzmfqf3jkgem9wgsyuctwdus9xgrcyqcjcgpzgfskx6eqf9hzqnteypzxz7fzypfhg6trddjhygrcyqezcgpzfysywmm5ypxxjemgw3hxjmn8yptk7untd9hxwg3q2d6xjcmtv4ezq7pqxgsxzmnyyqcjqmt0wfjjq6t5v4khxsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsxqyjw5qcqp2rzjq0gxwkzc8w6323m55m4jyxcjwmy7stt9hwkwe2qxmy8zpsgg7jcuwz87fcqqeuqqqyqqqqlgqqqqn3qq9q9qrsgqrvgkpnmps664wgkp43l22qsgdw4ve24aca4nymnxddlnp8vh9v2sdxlu5ywdxefsfvm0fq3sesf08uf6q9a2ke0hc9j6z6wlxg5z5kqpu2v9wz".to_owned(),
+ InvoiceBuilder::new(Currency::Bitcoin)
+ .amount_milli_satoshis(967878534)
+ .timestamp(UNIX_EPOCH + Duration::from_secs(1572468703))
+ .payment_secret(PaymentSecret([0x11; 32]))
+ .payment_hash(sha256::Hash::from_hex(
+ "462264ede7e14047e9b249da94fefc47f41f7d02ee9b091815a5506bc8abf75f"
+ ).unwrap())
+ .expiry_time(Duration::from_secs(604800))
+ .min_final_cltv_expiry(10)
+ .description("Blockstream Store: 88.85 USD for Blockstream Ledger Nano S x 1, \"Back In My Day\" Sticker x 2, \"I Got Lightning Working\" Sticker x 2 and 1 more items".to_owned())
+ .private_route(RouteHint(vec![RouteHintHop {
+ src_node_id: PublicKey::from_slice(&hex::decode(
+ "03d06758583bb5154774a6eb221b1276c9e82d65bbaceca806d90e20c108f4b1c7"
+ ).unwrap()).unwrap(),
+ short_channel_id: (589390 << 40) | (3312 << 16) | 1,
+ fees: RoutingFees { base_msat: 1000, proportional_millionths: 2500 },
+ cltv_expiry_delta: 40,
+ htlc_maximum_msat: None, htlc_minimum_msat: None,
+ }]))
+ .build_raw()
+ .unwrap()
+ .sign(|_| {
+ RecoverableSignature::from_compact(
+ &hex::decode("1b1160cf6186b55722c1ac7ea502086baaccaabdc76b326e666b7f309d972b15069bfca11cd365304b36f48230cc12f3f13a017aab65f7c165a169df32282a58").unwrap(),
+ RecoveryId::from_i32(1).unwrap()
+ )
+ }).unwrap(),
+ false, // Same features as set in InvoiceBuilder
+ false, // No unknown fields
+ ),
+ (
+ "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqqsgq2a25dxl5hrntdtn6zvydt7d66hyzsyhqs4wdynavys42xgl6sgx9c4g7me86a27t07mdtfry458rtjr0v92cnmswpsjscgt2vcse3sgpz3uapa".to_owned(),
+ InvoiceBuilder::new(Currency::Bitcoin)
+ .amount_milli_satoshis(2_500_000_000)
+ .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
+ .payment_secret(PaymentSecret([0x11; 32]))
+ .payment_hash(sha256::Hash::from_hex(
+ "0001020304050607080900010203040506070809000102030405060708090102"
+ ).unwrap())
+ .description("coffee beans".to_owned())
+ .build_raw()
+ .unwrap()
+ .sign(|_| {
+ RecoverableSignature::from_compact(
+ &hex::decode("5755469bf4b8e6b6ae7a1308d5f9bad5c82812e0855cd24fac242aa323fa820c5c551ede4faeabcb7fb6d5a464ad0e35c86f615589ee0e0c250c216a662198c1").unwrap(),
+ RecoveryId::from_i32(1).unwrap()
+ )
+ }).unwrap(),
+ true, // Different features than set in InvoiceBuilder
+ false, // No unknown fields
+ ),
+ (
+ "LNBC25M1PVJLUEZPP5QQQSYQCYQ5RQWZQFQQQSYQCYQ5RQWZQFQQQSYQCYQ5RQWZQFQYPQDQ5VDHKVEN9V5SXYETPDEESSP5ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYGS9Q5SQQQQQQQQQQQQQQQQSGQ2A25DXL5HRNTDTN6ZVYDT7D66HYZSYHQS4WDYNAVYS42XGL6SGX9C4G7ME86A27T07MDTFRY458RTJR0V92CNMSWPSJSCGT2VCSE3SGPZ3UAPA".to_owned(),
+ InvoiceBuilder::new(Currency::Bitcoin)
+ .amount_milli_satoshis(2_500_000_000)
+ .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
+ .payment_secret(PaymentSecret([0x11; 32]))
+ .payment_hash(sha256::Hash::from_hex(
+ "0001020304050607080900010203040506070809000102030405060708090102"
+ ).unwrap())
+ .description("coffee beans".to_owned())
+ .build_raw()
+ .unwrap()
+ .sign(|_| {
+ RecoverableSignature::from_compact(
+ &hex::decode("5755469bf4b8e6b6ae7a1308d5f9bad5c82812e0855cd24fac242aa323fa820c5c551ede4faeabcb7fb6d5a464ad0e35c86f615589ee0e0c250c216a662198c1").unwrap(),
+ RecoveryId::from_i32(1).unwrap()
+ )
+ }).unwrap(),
+ true, // Different features than set in InvoiceBuilder
+ false, // No unknown fields
+ ),
+ (
+ "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqqsgq2qrqqqfppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhpnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqspnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnp5qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnpkqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqz599y53s3ujmcfjp5xrdap68qxymkqphwsexhmhr8wdz5usdzkzrse33chw6dlp3jhuhge9ley7j2ayx36kawe7kmgg8sv5ugdyusdcqzn8z9x".to_owned(),
+ InvoiceBuilder::new(Currency::Bitcoin)
+ .amount_milli_satoshis(2_500_000_000)
+ .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
+ .payment_secret(PaymentSecret([0x11; 32]))
+ .payment_hash(sha256::Hash::from_hex(
+ "0001020304050607080900010203040506070809000102030405060708090102"
+ ).unwrap())
+ .description("coffee beans".to_owned())
+ .build_raw()
+ .unwrap()
+ .sign(|_| {
+ RecoverableSignature::from_compact(
+ &hex::decode("150a5252308f25bc2641a186de87470189bb003774326beee33b9a2a720d1584386631c5dda6fc3195f97464bfc93d2574868eadd767d6da1078329c4349c837").unwrap(),
+ RecoveryId::from_i32(0).unwrap()
+ )
+ }).unwrap(),
+ true, // Different features than set in InvoiceBuilder
+ true, // Some unknown fields
+ ),
]
}
-
#[test]
-fn serialize() {
- for (serialized, deserialized, _) in get_test_tuples() {
- assert_eq!(deserialized.to_string(), serialized);
- }
-}
-
-#[test]
-fn deserialize() {
- for (serialized, deserialized, maybe_error) in get_test_tuples() {
+fn invoice_deserialize() {
+ for (serialized, deserialized, ignore_feature_diff, ignore_unknown_fields) in get_test_tuples() {
+ eprintln!("Testing invoice {}...", serialized);
let parsed = serialized.parse::<SignedRawInvoice>().unwrap();
- assert_eq!(parsed, deserialized);
+ let (parsed_invoice, _, parsed_sig) = parsed.into_parts();
+ let (deserialized_invoice, _, deserialized_sig) = deserialized.into_parts();
- let validated = Invoice::from_signed(parsed);
+ assert_eq!(deserialized_sig, parsed_sig);
+ assert_eq!(deserialized_invoice.hrp, parsed_invoice.hrp);
+ assert_eq!(deserialized_invoice.data.timestamp, parsed_invoice.data.timestamp);
- if let Some(error) = maybe_error {
- assert_eq!(Err(error), validated);
- } else {
- assert!(validated.is_ok());
+ let mut deserialized_hunks: HashSet<_> = deserialized_invoice.data.tagged_fields.iter().collect();
+ let mut parsed_hunks: HashSet<_> = parsed_invoice.data.tagged_fields.iter().collect();
+ if ignore_feature_diff {
+ deserialized_hunks.retain(|h|
+ if let RawTaggedField::KnownSemantics(TaggedField::Features(_)) = h { false } else { true });
+ parsed_hunks.retain(|h|
+ if let RawTaggedField::KnownSemantics(TaggedField::Features(_)) = h { false } else { true });
+ }
+ if ignore_unknown_fields {
+ parsed_hunks.retain(|h|
+ if let RawTaggedField::UnknownSemantics(_) = h { false } else { true });
}
+ assert_eq!(deserialized_hunks, parsed_hunks);
+
+ Invoice::from_signed(serialized.parse::<SignedRawInvoice>().unwrap()).unwrap();
}
}
+
+#[test]
+fn test_bolt_invalid_invoices() {
+ // Tests the BOLT 11 invalid invoice test vectors
+ assert_eq!(Invoice::from_str(
+ "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqqsgqtqyx5vggfcsll4wu246hz02kp85x4katwsk9639we5n5yngc3yhqkm35jnjw4len8vrnqnf5ejh0mzj9n3vz2px97evektfm2l6wqccp3y7372"
+ ), Err(ParseOrSemanticError::SemanticError(SemanticError::InvalidFeatures)));
+ assert_eq!(Invoice::from_str(
+ "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrnt"
+ ), Err(ParseOrSemanticError::ParseError(ParseError::Bech32Error(bech32::Error::InvalidChecksum))));
+ assert_eq!(Invoice::from_str(
+ "pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny"
+ ), Err(ParseOrSemanticError::ParseError(ParseError::Bech32Error(bech32::Error::MissingSeparator))));
+ assert_eq!(Invoice::from_str(
+ "LNBC2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny"
+ ), Err(ParseOrSemanticError::ParseError(ParseError::Bech32Error(bech32::Error::MixedCase))));
+ assert_eq!(Invoice::from_str(
+ "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgqwgt7mcn5yqw3yx0w94pswkpq6j9uh6xfqqqtsk4tnarugeektd4hg5975x9am52rz4qskukxdmjemg92vvqz8nvmsye63r5ykel43pgz7zq0g2"
+ ), Err(ParseOrSemanticError::SemanticError(SemanticError::InvalidSignature)));
+ assert_eq!(Invoice::from_str(
+ "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6na6hlh"
+ ), Err(ParseOrSemanticError::ParseError(ParseError::TooShortDataPart)));
+ assert_eq!(Invoice::from_str(
+ "lnbc2500x1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgqrrzc4cvfue4zp3hggxp47ag7xnrlr8vgcmkjxk3j5jqethnumgkpqp23z9jclu3v0a7e0aruz366e9wqdykw6dxhdzcjjhldxq0w6wgqcnu43j"
+ ), Err(ParseOrSemanticError::ParseError(ParseError::UnknownSiPrefix)));
+ assert_eq!(Invoice::from_str(
+ "lnbc2500000001p1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgq0lzc236j96a95uv0m3umg28gclm5lqxtqqwk32uuk4k6673k6n5kfvx3d2h8s295fad45fdhmusm8sjudfhlf6dcsxmfvkeywmjdkxcp99202x"
+ ), Err(ParseOrSemanticError::SemanticError(SemanticError::ImpreciseAmount)));
+}
/// Note that the commitment number starts at (1 << 48) - 1 and counts backwards.
// TODO: return a Result so we can signal a validation error
fn release_commitment_secret(&self, idx: u64) -> [u8; 32];
+ /// Validate the counterparty's signatures on the holder commitment transaction and HTLCs.
+ ///
+ /// This is required in order for the signer to make sure that releasing a commitment
+ /// secret won't leave us without a broadcastable holder transaction.
+ /// Policy checks should be implemented in this function, including checking the amount
+ /// sent to us and checking the HTLCs.
+ fn validate_holder_commitment(&self, holder_tx: &HolderCommitmentTransaction) -> Result<(), ()>;
/// Gets the holder's channel public keys and basepoints
fn pubkeys(&self) -> &ChannelPublicKeys;
/// Gets an arbitrary identifier describing the set of keys which are provided back to you in
/// Create a signature for a counterparty's commitment transaction and associated HTLC transactions.
///
/// Note that if signing fails or is rejected, the channel will be force-closed.
+ ///
+ /// Policy checks should be implemented in this function, including checking the amount
+ /// sent to us and checking the HTLCs.
//
// TODO: Document the things someone using this interface should enforce before signing.
fn sign_counterparty_commitment(&self, commitment_tx: &CommitmentTransaction, secp_ctx: &Secp256k1<secp256k1::All>) -> Result<(Signature, Vec<Signature>), ()>;
+ /// Validate the counterparty's revocation.
+ ///
+ /// This is required in order for the signer to make sure that the state has moved
+ /// forward and it is safe to sign the next counterparty commitment.
+ fn validate_counterparty_revocation(&self, idx: u64, secret: &SecretKey) -> Result<(), ()>;
/// Create a signatures for a holder's commitment transaction and its claiming HTLC transactions.
/// This will only ever be called with a non-revoked commitment_tx. This will be called with the
chan_utils::build_commitment_secret(&self.commitment_seed, idx)
}
+ fn validate_holder_commitment(&self, _holder_tx: &HolderCommitmentTransaction) -> Result<(), ()> {
+ Ok(())
+ }
+
fn pubkeys(&self) -> &ChannelPublicKeys { &self.holder_channel_pubkeys }
fn channel_keys_id(&self) -> [u8; 32] { self.channel_keys_id }
Ok((commitment_sig, htlc_sigs))
}
+ fn validate_counterparty_revocation(&self, _idx: u64, _secret: &SecretKey) -> Result<(), ()> {
+ Ok(())
+ }
+
fn sign_holder_commitment_and_htlcs(&self, commitment_tx: &HolderCommitmentTransaction, secp_ctx: &Secp256k1<secp256k1::All>) -> Result<(Signature, Vec<Signature>), ()> {
let funding_pubkey = PublicKey::from_secret_key(secp_ctx, &self.funding_key);
let funding_redeemscript = make_funding_redeemscript(&funding_pubkey, &self.counterparty_pubkeys().funding_pubkey);
self.counterparty_funding_pubkey()
);
+ self.holder_signer.validate_holder_commitment(&holder_commitment_tx)
+ .map_err(|_| ChannelError::Close("Failed to validate our commitment".to_owned()))?;
+
// Now that we're past error-generating stuff, update our local state:
let funding_redeemscript = self.get_funding_redeemscript();
self.counterparty_funding_pubkey()
);
+ self.holder_signer.validate_holder_commitment(&holder_commitment_tx)
+ .map_err(|_| ChannelError::Close("Failed to validate our commitment".to_owned()))?;
+
let funding_redeemscript = self.get_funding_redeemscript();
let funding_txo = self.get_funding_txo().unwrap();
);
let next_per_commitment_point = self.holder_signer.get_per_commitment_point(self.cur_holder_commitment_transaction_number - 1, &self.secp_ctx);
+ self.holder_signer.validate_holder_commitment(&holder_commitment_tx)
+ .map_err(|_| (None, ChannelError::Close("Failed to validate our commitment".to_owned())))?;
let per_commitment_secret = self.holder_signer.release_commitment_secret(self.cur_holder_commitment_transaction_number + 1);
// Update state now that we've passed all the can-fail calls...
return Err(ChannelError::Close("Peer sent revoke_and_ack after we'd started exchanging closing_signeds".to_owned()));
}
+ let secret = secp_check!(SecretKey::from_slice(&msg.per_commitment_secret), "Peer provided an invalid per_commitment_secret".to_owned());
+
if let Some(counterparty_prev_commitment_point) = self.counterparty_prev_commitment_point {
- if PublicKey::from_secret_key(&self.secp_ctx, &secp_check!(SecretKey::from_slice(&msg.per_commitment_secret), "Peer provided an invalid per_commitment_secret".to_owned())) != counterparty_prev_commitment_point {
+ if PublicKey::from_secret_key(&self.secp_ctx, &secret) != counterparty_prev_commitment_point {
return Err(ChannelError::Close("Got a revoke commitment secret which didn't correspond to their current pubkey".to_owned()));
}
}
*self.next_remote_commitment_tx_fee_info_cached.lock().unwrap() = None;
}
+ self.holder_signer.validate_counterparty_revocation(
+ self.cur_counterparty_commitment_transaction_number + 1,
+ &secret
+ ).map_err(|_| ChannelError::Close("Failed to validate revocation from peer".to_owned()))?;
+
self.commitment_secrets.provide_secret(self.cur_counterparty_commitment_transaction_number + 1, msg.per_commitment_secret)
.map_err(|_| ChannelError::Close("Previous secrets did not match new one".to_owned()))?;
self.latest_monitor_update_id += 1;
use io;
use prelude::*;
use core::{cmp, fmt};
+use core::hash::{Hash, Hasher};
use core::marker::PhantomData;
use bitcoin::bech32;
}
}
}
+impl<T: sealed::Context> Hash for Features<T> {
+ fn hash<H: Hasher>(&self, hasher: &mut H) {
+ self.flags.hash(hasher);
+ }
+}
impl<T: sealed::Context> PartialEq for Features<T> {
fn eq(&self, o: &Self) -> bool {
self.flags.eq(&o.flags)
&self.flags
}
- pub(crate) fn requires_unknown_bits(&self) -> bool {
+ /// Returns true if this `Features` object contains unknown feature flags which are set as
+ /// "required".
+ pub fn requires_unknown_bits(&self) -> bool {
// Bitwise AND-ing with all even bits set except for known features will select required
// unknown features.
let byte_count = T::KNOWN_FEATURE_MASK.len();
let chan_lock = nodes[0].node.channel_state.lock().unwrap();
let local_chan = chan_lock.by_id.get(&chan.2).unwrap();
let chan_signer = local_chan.get_signer();
+ // Make the signer believe we validated another commitment, so we can release the secret
+ chan_signer.get_enforcement_state().last_holder_commitment -= 1;
+
let pubkeys = chan_signer.pubkeys();
(pubkeys.revocation_basepoint, pubkeys.htlc_basepoint,
chan_signer.release_commitment_secret(INITIAL_COMMITMENT_NUMBER),
// commitment transaction, we would have happily carried on and provided them the next
// commitment transaction based on one RAA forward. This would probably eventually have led to
// channel closure, but it would not have resulted in funds loss. Still, our
- // EnforcingSigner would have paniced as it doesn't like jumps into the future. Here, we
+ // EnforcingSigner would have panicked as it doesn't like jumps into the future. Here, we
// check simply that the channel is closed in response to such an RAA, but don't check whether
// we decide to punish our counterparty for revoking their funds (as we don't currently
// implement that).
let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1, InitFeatures::known(), InitFeatures::known()).2;
let mut guard = nodes[0].node.channel_state.lock().unwrap();
- let keys = &guard.by_id.get_mut(&channel_id).unwrap().get_signer();
+ let keys = guard.by_id.get_mut(&channel_id).unwrap().get_signer();
+
const INITIAL_COMMITMENT_NUMBER: u64 = (1 << 48) - 1;
+
+ // Make signer believe we got a counterparty signature, so that it allows the revocation
+ keys.get_enforcement_state().last_holder_commitment -= 1;
let per_commitment_secret = keys.release_commitment_secret(INITIAL_COMMITMENT_NUMBER);
+
// Must revoke without gaps
+ keys.get_enforcement_state().last_holder_commitment -= 1;
keys.release_commitment_secret(INITIAL_COMMITMENT_NUMBER - 1);
+
+ keys.get_enforcement_state().last_holder_commitment -= 1;
let next_per_commitment_point = PublicKey::from_secret_key(&Secp256k1::new(),
&SecretKey::from_slice(&keys.release_commitment_secret(INITIAL_COMMITMENT_NUMBER - 2)).unwrap());
pub funding_txid: Txid,
/// The specific output index funding this channel
pub funding_output_index: u16,
- /// The signature of the channel initiator (funder) on the funding transaction
+ /// The signature of the channel initiator (funder) on the initial commitment transaction
pub signature: Signature,
}
pub struct FundingSigned {
/// The channel ID
pub channel_id: [u8; 32],
- /// The signature of the channel acceptor (fundee) on the funding transaction
+ /// The signature of the channel acceptor (fundee) on the initial commitment transaction
pub signature: Signature,
}
/// Fees for routing via a given channel or a node
-#[derive(Eq, PartialEq, Copy, Clone, Debug)]
+#[derive(Eq, PartialEq, Copy, Clone, Debug, Hash)]
pub struct RoutingFees {
/// Flat routing fee in satoshis
pub base_msat: u32,
use core::ops::Deref;
/// A hop in a route
-#[derive(Clone, PartialEq)]
+#[derive(Clone, Hash, PartialEq, Eq)]
pub struct RouteHop {
/// The node_id of the node at this hop.
pub pubkey: PublicKey,
/// A route directs a payment from the sender (us) to the recipient. If the recipient supports MPP,
/// it can take multiple paths. Each path is composed of one or more hops through the network.
-#[derive(Clone, PartialEq)]
+#[derive(Clone, Hash, PartialEq, Eq)]
pub struct Route {
/// The list of routes taken for a single (potentially-)multi-part payment. The pubkey of the
/// last RouteHop in each path must be the same.
}
/// A list of hops along a payment path terminating with a channel to the recipient.
-#[derive(Eq, PartialEq, Debug, Clone)]
+#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct RouteHint(pub Vec<RouteHintHop>);
/// A channel descriptor for a hop along a payment path.
-#[derive(Eq, PartialEq, Debug, Clone)]
+#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct RouteHintHop {
/// The node_id of the non-target end of the route
pub src_node_id: PublicKey,
return Err(LightningError{err: "Cannot send a payment of 0 msat".to_owned(), action: ErrorAction::IgnoreError});
}
- let last_hops = last_hops.iter().filter_map(|hops| hops.0.last()).collect::<Vec<_>>();
- for last_hop in last_hops.iter() {
- if last_hop.src_node_id == *payee {
- return Err(LightningError{err: "Last hop cannot have a payee as a source.".to_owned(), action: ErrorAction::IgnoreError});
+ for route in last_hops.iter() {
+ for hop in &route.0 {
+ if hop.src_node_id == *payee {
+ return Err(LightningError{err: "Last hop cannot have a payee as a source.".to_owned(), action: ErrorAction::IgnoreError});
+ }
}
}
// If a caller provided us with last hops, add them to routing targets. Since this happens
// earlier than general path finding, they will be somewhat prioritized, although currently
// it matters only if the fees are exactly the same.
- for hop in last_hops.iter() {
+ for route in last_hops.iter().filter(|route| !route.0.is_empty()) {
+ let first_hop_in_route = &(route.0)[0];
let have_hop_src_in_graph =
- // Only add the last hop to our candidate set if either we have a direct channel or
- // they are in the regular network graph.
- first_hop_targets.get(&hop.src_node_id).is_some() ||
- network.get_nodes().get(&hop.src_node_id).is_some();
+ // Only add the hops in this route to our candidate set if either
+ // we have a direct channel to the first hop or the first hop is
+ // in the regular network graph.
+ first_hop_targets.get(&first_hop_in_route.src_node_id).is_some() ||
+ network.get_nodes().get(&first_hop_in_route.src_node_id).is_some();
if have_hop_src_in_graph {
- // BOLT 11 doesn't allow inclusion of features for the last hop hints, which
- // really sucks, cause we're gonna need that eventually.
- let last_hop_htlc_minimum_msat: u64 = match hop.htlc_minimum_msat {
- Some(htlc_minimum_msat) => htlc_minimum_msat,
- None => 0
- };
- let directional_info = DummyDirectionalChannelInfo {
- cltv_expiry_delta: hop.cltv_expiry_delta as u32,
- htlc_minimum_msat: last_hop_htlc_minimum_msat,
- htlc_maximum_msat: hop.htlc_maximum_msat,
- fees: hop.fees,
- };
- // We assume that the recipient only included route hints for routes which had
- // sufficient value to route `final_value_msat`. Note that in the case of "0-value"
- // invoices where the invoice does not specify value this may not be the case, but
- // better to include the hints than not.
- if add_entry!(hop.short_channel_id, hop.src_node_id, payee, directional_info, Some((final_value_msat + 999) / 1000), &empty_channel_features, 0, path_value_msat, 0) {
- // If this hop connects to a node with which we have a direct channel,
- // ignore the network graph and, if the last hop was added, add our
- // direct channel to the candidate set.
- //
- // Note that we *must* check if the last hop was added as `add_entry`
- // always assumes that the third argument is a node to which we have a
- // path.
- if let Some(&(ref first_hop, ref features, ref outbound_capacity_msat, _)) = first_hop_targets.get(&hop.src_node_id) {
- add_entry!(first_hop, *our_node_id , hop.src_node_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features, 0, path_value_msat, 0);
+ // We start building the path from reverse, i.e., from payee
+ // to the first RouteHintHop in the path.
+ let hop_iter = route.0.iter().rev();
+ let prev_hop_iter = core::iter::once(payee).chain(
+ route.0.iter().skip(1).rev().map(|hop| &hop.src_node_id));
+ let mut hop_used = true;
+ let mut aggregate_next_hops_fee_msat: u64 = 0;
+ let mut aggregate_next_hops_path_htlc_minimum_msat: u64 = 0;
+
+ for (idx, (hop, prev_hop_id)) in hop_iter.zip(prev_hop_iter).enumerate() {
+ // BOLT 11 doesn't allow inclusion of features for the last hop hints, which
+ // really sucks, cause we're gonna need that eventually.
+ let hop_htlc_minimum_msat: u64 = hop.htlc_minimum_msat.unwrap_or(0);
+
+ let directional_info = DummyDirectionalChannelInfo {
+ cltv_expiry_delta: hop.cltv_expiry_delta as u32,
+ htlc_minimum_msat: hop_htlc_minimum_msat,
+ htlc_maximum_msat: hop.htlc_maximum_msat,
+ fees: hop.fees,
+ };
+
+ let reqd_channel_cap = if let Some (val) = final_value_msat.checked_add(match idx {
+ 0 => 999,
+ _ => aggregate_next_hops_fee_msat.checked_add(999).unwrap_or(u64::max_value())
+ }) { Some( val / 1000 ) } else { break; }; // converting from msat or breaking if max ~ infinity
+
+
+ // We assume that the recipient only included route hints for routes which had
+ // sufficient value to route `final_value_msat`. Note that in the case of "0-value"
+ // invoices where the invoice does not specify value this may not be the case, but
+ // better to include the hints than not.
+ if !add_entry!(hop.short_channel_id, hop.src_node_id, prev_hop_id, directional_info, reqd_channel_cap, &empty_channel_features, aggregate_next_hops_fee_msat, path_value_msat, aggregate_next_hops_path_htlc_minimum_msat) {
+ // If this hop was not used then there is no use checking the preceding hops
+ // in the RouteHint. We can break by just searching for a direct channel between
+ // last checked hop and first_hop_targets
+ hop_used = false;
+ }
+
+ // Searching for a direct channel between last checked hop and first_hop_targets
+ if let Some(&(ref first_hop, ref features, ref outbound_capacity_msat, _)) = first_hop_targets.get(&prev_hop_id) {
+ add_entry!(first_hop, *our_node_id , prev_hop_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features, aggregate_next_hops_fee_msat, path_value_msat, aggregate_next_hops_path_htlc_minimum_msat);
+ }
+
+ if !hop_used {
+ break;
+ }
+
+ // In the next values of the iterator, the aggregate fees already reflects
+ // the sum of value sent from payer (final_value_msat) and routing fees
+ // for the last node in the RouteHint. We need to just add the fees to
+ // route through the current node so that the preceeding node (next iteration)
+ // can use it.
+ let hops_fee = compute_fees(aggregate_next_hops_fee_msat + final_value_msat, hop.fees)
+ .map_or(None, |inc| inc.checked_add(aggregate_next_hops_fee_msat));
+ aggregate_next_hops_fee_msat = if let Some(val) = hops_fee { val } else { break; };
+
+ let hop_htlc_minimum_msat_inc = if let Some(val) = compute_fees(aggregate_next_hops_path_htlc_minimum_msat, hop.fees) { val } else { break; };
+ let hops_path_htlc_minimum = aggregate_next_hops_path_htlc_minimum_msat
+ .checked_add(hop_htlc_minimum_msat_inc);
+ aggregate_next_hops_path_htlc_minimum_msat = if let Some(val) = hops_path_htlc_minimum { cmp::max(hop_htlc_minimum_msat, val) } else { break; };
+
+ if idx == route.0.len() - 1 {
+ // The last hop in this iterator is the first hop in
+ // overall RouteHint.
+ // If this hop connects to a node with which we have a direct channel,
+ // ignore the network graph and, if the last hop was added, add our
+ // direct channel to the candidate set.
+ //
+ // Note that we *must* check if the last hop was added as `add_entry`
+ // always assumes that the third argument is a node to which we have a
+ // path.
+ if let Some(&(ref first_hop, ref features, ref outbound_capacity_msat, _)) = first_hop_targets.get(&hop.src_node_id) {
+ add_entry!(first_hop, *our_node_id , hop.src_node_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features, aggregate_next_hops_fee_msat, path_value_msat, aggregate_next_hops_path_htlc_minimum_msat);
+ }
}
}
}
let logger = Arc::new(test_utils::TestLogger::new());
let chain_monitor = Arc::new(test_utils::TestChainSource::new(Network::Testnet));
let net_graph_msg_handler = NetGraphMsgHandler::new(genesis_block(Network::Testnet).header.block_hash(), None, Arc::clone(&logger));
- // Build network from our_id to node7:
+ // Build network from our_id to node6:
//
// -1(1)2- node0 -1(3)2-
// / \
// \ /
// -1(7)2- node5 -1(10)2-
//
+ // Channels 5, 8, 9 and 10 are private channels.
+ //
// chan5 1-to-2: enabled, 100 msat fee
// chan5 2-to-1: enabled, 0 fee
//
cltv_expiry_delta: (8 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
+ }
+ ]), RouteHint(vec![RouteHintHop {
+ src_node_id: nodes[4].clone(),
+ short_channel_id: 9,
+ fees: RoutingFees {
+ base_msat: 1001,
+ proportional_millionths: 0,
+ },
+ cltv_expiry_delta: (9 << 8) | 1,
+ htlc_minimum_msat: None,
+ htlc_maximum_msat: None,
}]), RouteHint(vec![RouteHintHop {
+ src_node_id: nodes[5].clone(),
+ short_channel_id: 10,
+ fees: zero_fees,
+ cltv_expiry_delta: (10 << 8) | 1,
+ htlc_minimum_msat: None,
+ htlc_maximum_msat: None,
+ }])]
+ }
+
+ fn last_hops_multi_private_channels(nodes: &Vec<PublicKey>) -> Vec<RouteHint> {
+ let zero_fees = RoutingFees {
+ base_msat: 0,
+ proportional_millionths: 0,
+ };
+ vec![RouteHint(vec![RouteHintHop {
+ src_node_id: nodes[2].clone(),
+ short_channel_id: 5,
+ fees: RoutingFees {
+ base_msat: 100,
+ proportional_millionths: 0,
+ },
+ cltv_expiry_delta: (5 << 8) | 1,
+ htlc_minimum_msat: None,
+ htlc_maximum_msat: None,
+ }, 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(vec![RouteHintHop {
src_node_id: nodes[4].clone(),
short_channel_id: 9,
fees: RoutingFees {
}
#[test]
- fn last_hops_test() {
+ fn partial_route_hint_test() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
// Simple test across 2, 3, 5, and 4 via a last_hop channel
+ // Tests the behaviour when the RouteHint contains a suboptimal hop.
+ // RouteHint may be partially used by the algo to build the best path.
// First check that last hop can't have its source as the payee.
let invalid_last_hop = RouteHint(vec![RouteHintHop {
htlc_maximum_msat: None,
}]);
- let mut invalid_last_hops = last_hops(&nodes);
+ let mut invalid_last_hops = last_hops_multi_private_channels(&nodes);
invalid_last_hops.push(invalid_last_hop);
{
if let Err(LightningError{err, action: ErrorAction::IgnoreError}) = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &invalid_last_hops.iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)) {
} else { panic!(); }
}
- let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &last_hops(&nodes).iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)).unwrap();
+ let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &last_hops_multi_private_channels(&nodes).iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)).unwrap();
+ assert_eq!(route.paths[0].len(), 5);
+
+ assert_eq!(route.paths[0][0].pubkey, nodes[1]);
+ assert_eq!(route.paths[0][0].short_channel_id, 2);
+ assert_eq!(route.paths[0][0].fee_msat, 100);
+ assert_eq!(route.paths[0][0].cltv_expiry_delta, (4 << 8) | 1);
+ assert_eq!(route.paths[0][0].node_features.le_flags(), &id_to_feature_flags(2));
+ assert_eq!(route.paths[0][0].channel_features.le_flags(), &id_to_feature_flags(2));
+
+ assert_eq!(route.paths[0][1].pubkey, nodes[2]);
+ assert_eq!(route.paths[0][1].short_channel_id, 4);
+ assert_eq!(route.paths[0][1].fee_msat, 0);
+ assert_eq!(route.paths[0][1].cltv_expiry_delta, (6 << 8) | 1);
+ assert_eq!(route.paths[0][1].node_features.le_flags(), &id_to_feature_flags(3));
+ assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags(4));
+
+ assert_eq!(route.paths[0][2].pubkey, nodes[4]);
+ assert_eq!(route.paths[0][2].short_channel_id, 6);
+ assert_eq!(route.paths[0][2].fee_msat, 0);
+ assert_eq!(route.paths[0][2].cltv_expiry_delta, (11 << 8) | 1);
+ assert_eq!(route.paths[0][2].node_features.le_flags(), &id_to_feature_flags(5));
+ assert_eq!(route.paths[0][2].channel_features.le_flags(), &id_to_feature_flags(6));
+
+ assert_eq!(route.paths[0][3].pubkey, nodes[3]);
+ assert_eq!(route.paths[0][3].short_channel_id, 11);
+ assert_eq!(route.paths[0][3].fee_msat, 0);
+ assert_eq!(route.paths[0][3].cltv_expiry_delta, (8 << 8) | 1);
+ // If we have a peer in the node map, we'll use their features here since we don't have
+ // a way of figuring out their features from the invoice:
+ assert_eq!(route.paths[0][3].node_features.le_flags(), &id_to_feature_flags(4));
+ assert_eq!(route.paths[0][3].channel_features.le_flags(), &id_to_feature_flags(11));
+
+ assert_eq!(route.paths[0][4].pubkey, nodes[6]);
+ assert_eq!(route.paths[0][4].short_channel_id, 8);
+ assert_eq!(route.paths[0][4].fee_msat, 100);
+ assert_eq!(route.paths[0][4].cltv_expiry_delta, 42);
+ assert_eq!(route.paths[0][4].node_features.le_flags(), &Vec::<u8>::new()); // We dont pass flags in from invoices yet
+ assert_eq!(route.paths[0][4].channel_features.le_flags(), &Vec::<u8>::new()); // We can't learn any flags from invoices, sadly
+ }
+
+ fn empty_last_hop(nodes: &Vec<PublicKey>) -> Vec<RouteHint> {
+ 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(vec![
+
+ ]), RouteHint(vec![RouteHintHop {
+ src_node_id: nodes[5].clone(),
+ short_channel_id: 10,
+ fees: zero_fees,
+ cltv_expiry_delta: (10 << 8) | 1,
+ htlc_minimum_msat: None,
+ htlc_maximum_msat: None,
+ }])]
+ }
+
+ #[test]
+ fn ignores_empty_last_hops_test() {
+ let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
+ let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
+
+ // Test handling of an empty RouteHint passed in Invoice.
+
+ let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &empty_last_hop(&nodes).iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)).unwrap();
assert_eq!(route.paths[0].len(), 5);
assert_eq!(route.paths[0][0].pubkey, nodes[1]);
assert_eq!(route.paths[0][4].channel_features.le_flags(), &Vec::<u8>::new()); // We can't learn any flags from invoices, sadly
}
+ fn multi_hint_last_hops(nodes: &Vec<PublicKey>) -> Vec<RouteHint> {
+ let zero_fees = RoutingFees {
+ base_msat: 0,
+ proportional_millionths: 0,
+ };
+ vec![RouteHint(vec![RouteHintHop {
+ src_node_id: nodes[2].clone(),
+ short_channel_id: 5,
+ fees: RoutingFees {
+ base_msat: 100,
+ proportional_millionths: 0,
+ },
+ cltv_expiry_delta: (5 << 8) | 1,
+ htlc_minimum_msat: None,
+ htlc_maximum_msat: None,
+ }, 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(vec![RouteHintHop {
+ src_node_id: nodes[5].clone(),
+ short_channel_id: 10,
+ fees: zero_fees,
+ cltv_expiry_delta: (10 << 8) | 1,
+ htlc_minimum_msat: None,
+ htlc_maximum_msat: None,
+ }])]
+ }
+
+ #[test]
+ fn multi_hint_last_hops_test() {
+ let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
+ let (_, our_id, privkeys, nodes) = get_nodes(&secp_ctx);
+ // Test through channels 2, 3, 5, 8.
+ // Test shows that multiple hop hints are considered.
+
+ // Disabling channels 6 & 7 by flags=2
+ update_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[2], UnsignedChannelUpdate {
+ chain_hash: genesis_block(Network::Testnet).header.block_hash(),
+ short_channel_id: 6,
+ timestamp: 2,
+ flags: 2, // to disable
+ cltv_expiry_delta: 0,
+ htlc_minimum_msat: 0,
+ htlc_maximum_msat: OptionalField::Absent,
+ fee_base_msat: 0,
+ fee_proportional_millionths: 0,
+ excess_data: Vec::new()
+ });
+ update_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[2], UnsignedChannelUpdate {
+ chain_hash: genesis_block(Network::Testnet).header.block_hash(),
+ short_channel_id: 7,
+ timestamp: 2,
+ flags: 2, // to disable
+ cltv_expiry_delta: 0,
+ htlc_minimum_msat: 0,
+ htlc_maximum_msat: OptionalField::Absent,
+ fee_base_msat: 0,
+ fee_proportional_millionths: 0,
+ excess_data: Vec::new()
+ });
+
+ let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &multi_hint_last_hops(&nodes).iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)).unwrap();
+ assert_eq!(route.paths[0].len(), 4);
+
+ assert_eq!(route.paths[0][0].pubkey, nodes[1]);
+ assert_eq!(route.paths[0][0].short_channel_id, 2);
+ assert_eq!(route.paths[0][0].fee_msat, 200);
+ assert_eq!(route.paths[0][0].cltv_expiry_delta, 1025);
+ assert_eq!(route.paths[0][0].node_features.le_flags(), &id_to_feature_flags(2));
+ assert_eq!(route.paths[0][0].channel_features.le_flags(), &id_to_feature_flags(2));
+
+ assert_eq!(route.paths[0][1].pubkey, nodes[2]);
+ assert_eq!(route.paths[0][1].short_channel_id, 4);
+ assert_eq!(route.paths[0][1].fee_msat, 100);
+ assert_eq!(route.paths[0][1].cltv_expiry_delta, 1281);
+ assert_eq!(route.paths[0][1].node_features.le_flags(), &id_to_feature_flags(3));
+ assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags(4));
+
+ assert_eq!(route.paths[0][2].pubkey, nodes[3]);
+ assert_eq!(route.paths[0][2].short_channel_id, 5);
+ assert_eq!(route.paths[0][2].fee_msat, 0);
+ assert_eq!(route.paths[0][2].cltv_expiry_delta, 2049);
+ assert_eq!(route.paths[0][2].node_features.le_flags(), &id_to_feature_flags(4));
+ assert_eq!(route.paths[0][2].channel_features.le_flags(), &Vec::<u8>::new());
+
+ assert_eq!(route.paths[0][3].pubkey, nodes[6]);
+ assert_eq!(route.paths[0][3].short_channel_id, 8);
+ assert_eq!(route.paths[0][3].fee_msat, 100);
+ assert_eq!(route.paths[0][3].cltv_expiry_delta, 42);
+ assert_eq!(route.paths[0][3].node_features.le_flags(), &Vec::<u8>::new()); // We dont pass flags in from invoices yet
+ assert_eq!(route.paths[0][3].channel_features.le_flags(), &Vec::<u8>::new()); // We can't learn any flags from invoices, sadly
+ }
+
+ fn last_hops_with_public_channel(nodes: &Vec<PublicKey>) -> Vec<RouteHint> {
+ let zero_fees = RoutingFees {
+ base_msat: 0,
+ proportional_millionths: 0,
+ };
+ vec![RouteHint(vec![RouteHintHop {
+ src_node_id: nodes[4].clone(),
+ short_channel_id: 11,
+ fees: zero_fees,
+ cltv_expiry_delta: (11 << 8) | 1,
+ htlc_minimum_msat: None,
+ htlc_maximum_msat: None,
+ }, 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(vec![RouteHintHop {
+ src_node_id: nodes[4].clone(),
+ short_channel_id: 9,
+ fees: RoutingFees {
+ base_msat: 1001,
+ proportional_millionths: 0,
+ },
+ cltv_expiry_delta: (9 << 8) | 1,
+ htlc_minimum_msat: None,
+ htlc_maximum_msat: None,
+ }]), RouteHint(vec![RouteHintHop {
+ src_node_id: nodes[5].clone(),
+ short_channel_id: 10,
+ fees: zero_fees,
+ cltv_expiry_delta: (10 << 8) | 1,
+ htlc_minimum_msat: None,
+ htlc_maximum_msat: None,
+ }])]
+ }
+
+ #[test]
+ fn last_hops_with_public_channel_test() {
+ let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
+ let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
+ // This test shows that public routes can be present in the invoice
+ // which would be handled in the same manner.
+
+ let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &last_hops_with_public_channel(&nodes).iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)).unwrap();
+ assert_eq!(route.paths[0].len(), 5);
+
+ assert_eq!(route.paths[0][0].pubkey, nodes[1]);
+ assert_eq!(route.paths[0][0].short_channel_id, 2);
+ assert_eq!(route.paths[0][0].fee_msat, 100);
+ assert_eq!(route.paths[0][0].cltv_expiry_delta, (4 << 8) | 1);
+ assert_eq!(route.paths[0][0].node_features.le_flags(), &id_to_feature_flags(2));
+ assert_eq!(route.paths[0][0].channel_features.le_flags(), &id_to_feature_flags(2));
+
+ assert_eq!(route.paths[0][1].pubkey, nodes[2]);
+ assert_eq!(route.paths[0][1].short_channel_id, 4);
+ assert_eq!(route.paths[0][1].fee_msat, 0);
+ assert_eq!(route.paths[0][1].cltv_expiry_delta, (6 << 8) | 1);
+ assert_eq!(route.paths[0][1].node_features.le_flags(), &id_to_feature_flags(3));
+ assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags(4));
+
+ assert_eq!(route.paths[0][2].pubkey, nodes[4]);
+ assert_eq!(route.paths[0][2].short_channel_id, 6);
+ assert_eq!(route.paths[0][2].fee_msat, 0);
+ assert_eq!(route.paths[0][2].cltv_expiry_delta, (11 << 8) | 1);
+ assert_eq!(route.paths[0][2].node_features.le_flags(), &id_to_feature_flags(5));
+ assert_eq!(route.paths[0][2].channel_features.le_flags(), &id_to_feature_flags(6));
+
+ assert_eq!(route.paths[0][3].pubkey, nodes[3]);
+ assert_eq!(route.paths[0][3].short_channel_id, 11);
+ assert_eq!(route.paths[0][3].fee_msat, 0);
+ assert_eq!(route.paths[0][3].cltv_expiry_delta, (8 << 8) | 1);
+ // If we have a peer in the node map, we'll use their features here since we don't have
+ // a way of figuring out their features from the invoice:
+ assert_eq!(route.paths[0][3].node_features.le_flags(), &id_to_feature_flags(4));
+ assert_eq!(route.paths[0][3].channel_features.le_flags(), &Vec::<u8>::new());
+
+ assert_eq!(route.paths[0][4].pubkey, nodes[6]);
+ assert_eq!(route.paths[0][4].short_channel_id, 8);
+ assert_eq!(route.paths[0][4].fee_msat, 100);
+ assert_eq!(route.paths[0][4].cltv_expiry_delta, 42);
+ assert_eq!(route.paths[0][4].node_features.le_flags(), &Vec::<u8>::new()); // We dont pass flags in from invoices yet
+ assert_eq!(route.paths[0][4].channel_features.le_flags(), &Vec::<u8>::new()); // We can't learn any flags from invoices, sadly
+ }
+
#[test]
fn our_chans_last_hop_connect_test() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
use ln::{chan_utils, msgs};
use chain::keysinterface::{Sign, InMemorySigner, BaseSign};
-use io;
use prelude::*;
use core::cmp;
use sync::{Mutex, Arc};
+#[cfg(test)] use sync::MutexGuard;
use bitcoin::blockdata::transaction::{Transaction, SigHashType};
use bitcoin::util::bip143;
use bitcoin::secp256k1;
use bitcoin::secp256k1::key::{SecretKey, PublicKey};
use bitcoin::secp256k1::{Secp256k1, Signature};
-use util::ser::{Writeable, Writer, Readable};
+use util::ser::{Writeable, Writer};
use io::Error;
-use ln::msgs::DecodeError;
/// Initial value for revoked commitment downward counter
pub const INITIAL_REVOKED_COMMITMENT_NUMBER: u64 = 1 << 48;
/// - When signing, the holder transaction has not been revoked
/// - When revoking, the holder transaction has not been signed
/// - The holder commitment number is monotonic and without gaps
+/// - The revoked holder commitment number is monotonic and without gaps
+/// - There is at least one unrevoked holder transaction at all times
/// - The counterparty commitment number is monotonic and without gaps
/// - The pre-derived keys and pre-built transaction in CommitmentTransaction were correctly built
///
/// Eventually we will probably want to expose a variant of this which would essentially
/// be what you'd want to run on a hardware wallet.
///
+/// Note that counterparty signatures on the holder transaction are not checked, but it should
+/// be in a complete implementation.
+///
/// Note that before we do so we should ensure its serialization format has backwards- and
/// forwards-compatibility prefix/suffixes!
#[derive(Clone)]
pub struct EnforcingSigner {
pub inner: InMemorySigner,
- /// The last counterparty commitment number we signed, backwards counting
- pub last_commitment_number: Arc<Mutex<Option<u64>>>,
- /// The last holder commitment number we revoked, backwards counting
- pub revoked_commitment: Arc<Mutex<u64>>,
+ /// Channel state used for policy enforcement
+ pub state: Arc<Mutex<EnforcementState>>,
pub disable_revocation_policy_check: bool,
}
impl EnforcingSigner {
/// Construct an EnforcingSigner
pub fn new(inner: InMemorySigner) -> Self {
+ let state = Arc::new(Mutex::new(EnforcementState::new()));
Self {
inner,
- last_commitment_number: Arc::new(Mutex::new(None)),
- revoked_commitment: Arc::new(Mutex::new(INITIAL_REVOKED_COMMITMENT_NUMBER)),
+ state,
disable_revocation_policy_check: false
}
}
/// Construct an EnforcingSigner with externally managed storage
///
/// Since there are multiple copies of this struct for each channel, some coordination is needed
- /// so that all copies are aware of revocations. A pointer to this state is provided here, usually
- /// by an implementation of KeysInterface.
- pub fn new_with_revoked(inner: InMemorySigner, revoked_commitment: Arc<Mutex<u64>>, disable_revocation_policy_check: bool) -> Self {
+ /// so that all copies are aware of enforcement state. A pointer to this state is provided
+ /// here, usually by an implementation of KeysInterface.
+ pub fn new_with_revoked(inner: InMemorySigner, state: Arc<Mutex<EnforcementState>>, disable_revocation_policy_check: bool) -> Self {
Self {
inner,
- last_commitment_number: Arc::new(Mutex::new(None)),
- revoked_commitment,
+ state,
disable_revocation_policy_check
}
}
+
+ #[cfg(test)]
+ pub fn get_enforcement_state(&self) -> MutexGuard<EnforcementState> {
+ self.state.lock().unwrap()
+ }
}
impl BaseSign for EnforcingSigner {
fn release_commitment_secret(&self, idx: u64) -> [u8; 32] {
{
- let mut revoked = self.revoked_commitment.lock().unwrap();
- assert!(idx == *revoked || idx == *revoked - 1, "can only revoke the current or next unrevoked commitment - trying {}, revoked {}", idx, *revoked);
- *revoked = idx;
+ let mut state = self.state.lock().unwrap();
+ assert!(idx == state.last_holder_revoked_commitment || idx == state.last_holder_revoked_commitment - 1, "can only revoke the current or next unrevoked commitment - trying {}, last revoked {}", idx, state.last_holder_revoked_commitment);
+ assert!(idx > state.last_holder_commitment, "cannot revoke the last holder commitment - attempted to revoke {} last commitment {}", idx, state.last_holder_commitment);
+ state.last_holder_revoked_commitment = idx;
}
self.inner.release_commitment_secret(idx)
}
+ fn validate_holder_commitment(&self, holder_tx: &HolderCommitmentTransaction) -> Result<(), ()> {
+ let mut state = self.state.lock().unwrap();
+ let idx = holder_tx.commitment_number();
+ assert!(idx == state.last_holder_commitment || idx == state.last_holder_commitment - 1, "expecting to validate the current or next holder commitment - trying {}, current {}", idx, state.last_holder_commitment);
+ state.last_holder_commitment = idx;
+ Ok(())
+ }
+
fn pubkeys(&self) -> &ChannelPublicKeys { self.inner.pubkeys() }
fn channel_keys_id(&self) -> [u8; 32] { self.inner.channel_keys_id() }
self.verify_counterparty_commitment_tx(commitment_tx, secp_ctx);
{
- let mut last_commitment_number_guard = self.last_commitment_number.lock().unwrap();
+ let mut state = self.state.lock().unwrap();
let actual_commitment_number = commitment_tx.commitment_number();
- let last_commitment_number = last_commitment_number_guard.unwrap_or(actual_commitment_number);
+ let last_commitment_number = state.last_counterparty_commitment;
// These commitment numbers are backwards counting. We expect either the same as the previously encountered,
// or the next one.
assert!(last_commitment_number == actual_commitment_number || last_commitment_number - 1 == actual_commitment_number, "{} doesn't come after {}", actual_commitment_number, last_commitment_number);
- *last_commitment_number_guard = Some(cmp::min(last_commitment_number, actual_commitment_number))
+ // Ensure that the counterparty doesn't get more than two broadcastable commitments -
+ // the last and the one we are trying to sign
+ assert!(actual_commitment_number >= state.last_counterparty_revoked_commitment - 2, "cannot sign a commitment if second to last wasn't revoked - signing {} revoked {}", actual_commitment_number, state.last_counterparty_revoked_commitment);
+ state.last_counterparty_commitment = cmp::min(last_commitment_number, actual_commitment_number)
}
Ok(self.inner.sign_counterparty_commitment(commitment_tx, secp_ctx).unwrap())
}
+ fn validate_counterparty_revocation(&self, idx: u64, _secret: &SecretKey) -> Result<(), ()> {
+ let mut state = self.state.lock().unwrap();
+ assert!(idx == state.last_counterparty_revoked_commitment || idx == state.last_counterparty_revoked_commitment - 1, "expecting to validate the current or next counterparty revocation - trying {}, current {}", idx, state.last_counterparty_revoked_commitment);
+ state.last_counterparty_revoked_commitment = idx;
+ Ok(())
+ }
+
fn sign_holder_commitment_and_htlcs(&self, commitment_tx: &HolderCommitmentTransaction, secp_ctx: &Secp256k1<secp256k1::All>) -> Result<(Signature, Vec<Signature>), ()> {
let trusted_tx = self.verify_holder_commitment_tx(commitment_tx, secp_ctx);
let commitment_txid = trusted_tx.txid();
let holder_csv = self.inner.counterparty_selected_contest_delay();
- let revoked = self.revoked_commitment.lock().unwrap();
+ let state = self.state.lock().unwrap();
let commitment_number = trusted_tx.commitment_number();
- if *revoked - 1 != commitment_number && *revoked - 2 != commitment_number {
+ if state.last_holder_revoked_commitment - 1 != commitment_number && state.last_holder_revoked_commitment - 2 != commitment_number {
if !self.disable_revocation_policy_check {
panic!("can only sign the next two unrevoked commitment numbers, revoked={} vs requested={} for {}",
- *revoked, commitment_number, self.inner.commitment_seed[0])
+ state.last_holder_revoked_commitment, commitment_number, self.inner.commitment_seed[0])
}
}
impl Writeable for EnforcingSigner {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), Error> {
+ // EnforcingSigner has two fields - `inner` ([`InMemorySigner`]) and `state`
+ // ([`EnforcementState`]). `inner` is serialized here and deserialized by
+ // [`KeysInterface::read_chan_signer`]. `state` is managed by [`KeysInterface`]
+ // and will be serialized as needed by the implementation of that trait.
self.inner.write(writer)?;
- let last = *self.last_commitment_number.lock().unwrap();
- last.write(writer)?;
Ok(())
}
}
-impl Readable for EnforcingSigner {
- fn read<R: io::Read>(reader: &mut R) -> Result<Self, DecodeError> {
- let inner = Readable::read(reader)?;
- let last_commitment_number = Readable::read(reader)?;
- Ok(EnforcingSigner {
- inner,
- last_commitment_number: Arc::new(Mutex::new(last_commitment_number)),
- revoked_commitment: Arc::new(Mutex::new(INITIAL_REVOKED_COMMITMENT_NUMBER)),
- disable_revocation_policy_check: false,
- })
- }
-}
-
impl EnforcingSigner {
fn verify_counterparty_commitment_tx<'a, T: secp256k1::Signing + secp256k1::Verification>(&self, commitment_tx: &'a CommitmentTransaction, secp_ctx: &Secp256k1<T>) -> TrustedCommitmentTransaction<'a> {
commitment_tx.verify(&self.inner.get_channel_parameters().as_counterparty_broadcastable(),
.expect("derived different per-tx keys or built transaction")
}
}
+
+/// The state used by [`EnforcingSigner`] in order to enforce policy checks
+///
+/// This structure is maintained by KeysInterface since we may have multiple copies of
+/// the signer and they must coordinate their state.
+#[derive(Clone)]
+pub struct EnforcementState {
+ /// The last counterparty commitment number we signed, backwards counting
+ pub last_counterparty_commitment: u64,
+ /// The last counterparty commitment they revoked, backwards counting
+ pub last_counterparty_revoked_commitment: u64,
+ /// The last holder commitment number we revoked, backwards counting
+ pub last_holder_revoked_commitment: u64,
+ /// The last validated holder commitment number, backwards counting
+ pub last_holder_commitment: u64,
+}
+
+impl EnforcementState {
+ /// Enforcement state for a new channel
+ pub fn new() -> Self {
+ EnforcementState {
+ last_counterparty_commitment: INITIAL_REVOKED_COMMITMENT_NUMBER,
+ last_counterparty_revoked_commitment: INITIAL_REVOKED_COMMITMENT_NUMBER,
+ last_holder_revoked_commitment: INITIAL_REVOKED_COMMITMENT_NUMBER,
+ last_holder_commitment: INITIAL_REVOKED_COMMITMENT_NUMBER,
+ }
+ }
+}
use ln::msgs;
use ln::msgs::OptionalField;
use ln::script::ShutdownScript;
-use util::enforcing_trait_impls::{EnforcingSigner, INITIAL_REVOKED_COMMITMENT_NUMBER};
+use util::enforcing_trait_impls::{EnforcingSigner, EnforcementState};
use util::events;
use util::logger::{Logger, Level, Record};
use util::ser::{Readable, ReadableArgs, Writer, Writeable};
fn get_channel_signer(&self, _inbound: bool, _channel_value_satoshis: u64) -> EnforcingSigner { unreachable!(); }
fn get_secure_random_bytes(&self) -> [u8; 32] { [0; 32] }
- fn read_chan_signer(&self, reader: &[u8]) -> Result<Self::Signer, msgs::DecodeError> {
- EnforcingSigner::read(&mut io::Cursor::new(reader))
+ fn read_chan_signer(&self, mut reader: &[u8]) -> Result<Self::Signer, msgs::DecodeError> {
+ let inner: InMemorySigner = Readable::read(&mut reader)?;
+ let state = Arc::new(Mutex::new(EnforcementState::new()));
+
+ Ok(EnforcingSigner::new_with_revoked(
+ inner,
+ state,
+ false
+ ))
}
fn sign_invoice(&self, _invoice_preimage: Vec<u8>) -> Result<RecoverableSignature, ()> { unreachable!(); }
}
pub override_session_priv: Mutex<Option<[u8; 32]>>,
pub override_channel_id_priv: Mutex<Option<[u8; 32]>>,
pub disable_revocation_policy_check: bool,
- revoked_commitments: Mutex<HashMap<[u8;32], Arc<Mutex<u64>>>>,
+ enforcement_states: Mutex<HashMap<[u8;32], Arc<Mutex<EnforcementState>>>>,
expectations: Mutex<Option<VecDeque<OnGetShutdownScriptpubkey>>>,
}
fn get_channel_signer(&self, inbound: bool, channel_value_satoshis: u64) -> EnforcingSigner {
let keys = self.backing.get_channel_signer(inbound, channel_value_satoshis);
- let revoked_commitment = self.make_revoked_commitment_cell(keys.commitment_seed);
- EnforcingSigner::new_with_revoked(keys, revoked_commitment, self.disable_revocation_policy_check)
+ let state = self.make_enforcement_state_cell(keys.commitment_seed);
+ EnforcingSigner::new_with_revoked(keys, state, self.disable_revocation_policy_check)
}
fn get_secure_random_bytes(&self) -> [u8; 32] {
let mut reader = io::Cursor::new(buffer);
let inner: InMemorySigner = Readable::read(&mut reader)?;
- let revoked_commitment = self.make_revoked_commitment_cell(inner.commitment_seed);
-
- let last_commitment_number = Readable::read(&mut reader)?;
+ let state = self.make_enforcement_state_cell(inner.commitment_seed);
- Ok(EnforcingSigner {
+ Ok(EnforcingSigner::new_with_revoked(
inner,
- last_commitment_number: Arc::new(Mutex::new(last_commitment_number)),
- revoked_commitment,
- disable_revocation_policy_check: self.disable_revocation_policy_check,
- })
+ state,
+ self.disable_revocation_policy_check
+ ))
}
fn sign_invoice(&self, invoice_preimage: Vec<u8>) -> Result<RecoverableSignature, ()> {
override_session_priv: Mutex::new(None),
override_channel_id_priv: Mutex::new(None),
disable_revocation_policy_check: false,
- revoked_commitments: Mutex::new(HashMap::new()),
+ enforcement_states: Mutex::new(HashMap::new()),
expectations: Mutex::new(None),
}
}
pub fn derive_channel_keys(&self, channel_value_satoshis: u64, id: &[u8; 32]) -> EnforcingSigner {
let keys = self.backing.derive_channel_keys(channel_value_satoshis, id);
- let revoked_commitment = self.make_revoked_commitment_cell(keys.commitment_seed);
- EnforcingSigner::new_with_revoked(keys, revoked_commitment, self.disable_revocation_policy_check)
+ let state = self.make_enforcement_state_cell(keys.commitment_seed);
+ EnforcingSigner::new_with_revoked(keys, state, self.disable_revocation_policy_check)
}
- fn make_revoked_commitment_cell(&self, commitment_seed: [u8; 32]) -> Arc<Mutex<u64>> {
- let mut revoked_commitments = self.revoked_commitments.lock().unwrap();
- if !revoked_commitments.contains_key(&commitment_seed) {
- revoked_commitments.insert(commitment_seed, Arc::new(Mutex::new(INITIAL_REVOKED_COMMITMENT_NUMBER)));
+ fn make_enforcement_state_cell(&self, commitment_seed: [u8; 32]) -> Arc<Mutex<EnforcementState>> {
+ let mut states = self.enforcement_states.lock().unwrap();
+ if !states.contains_key(&commitment_seed) {
+ let state = EnforcementState::new();
+ states.insert(commitment_seed, Arc::new(Mutex::new(state)));
}
- let cell = revoked_commitments.get(&commitment_seed).unwrap();
+ let cell = states.get(&commitment_seed).unwrap();
Arc::clone(cell)
}
}