From 5ccb247cb27f14cb3706b20cda99c97c262f0feb Mon Sep 17 00:00:00 2001 From: yukang Date: Wed, 16 Oct 2024 16:56:41 +0800 Subject: [PATCH] add invoice get rpc and check expire --- src/fiber/channel.rs | 13 ++++ src/fiber/network.rs | 6 ++ src/fiber/types.rs | 2 + src/invoice/invoice_impl.rs | 7 ++ src/invoice/tests/invoice_impl.rs | 15 ++++ src/rpc/invoice.rs | 72 +++++++++++++++++-- .../e2e/router-pay/12-node1-send-payment.bru | 1 + .../22-node3-gen-expiring-invoice.bru | 65 +++++++++++++++++ .../23-node1-send-payment-will-fail.bru | 48 +++++++++++++ 9 files changed, 224 insertions(+), 5 deletions(-) create mode 100644 tests/bruno/e2e/router-pay/22-node3-gen-expiring-invoice.bru create mode 100644 tests/bruno/e2e/router-pay/23-node1-send-payment-will-fail.bru diff --git a/src/fiber/channel.rs b/src/fiber/channel.rs index e64a9341..41c6771f 100644 --- a/src/fiber/channel.rs +++ b/src/fiber/channel.rs @@ -729,6 +729,19 @@ where let tlcs = state.get_tlcs_for_settle_down(); for tlc_info in tlcs { let tlc = tlc_info.tlc.clone(); + if let Some(invoice) = self.store.get_invoice(&tlc.payment_hash) { + if invoice.is_expired() { + let command = RemoveTlcCommand { + id: tlc.get_id(), + reason: RemoveTlcReason::RemoveTlcFail(TlcErrPacket::new(TlcErr::new( + TlcErrorCode::InvoiceExpired, + ))), + }; + let result = self.handle_remove_tlc_command(state, command); + info!("try to settle down tlc: {:?} result: {:?}", &tlc, &result); + } + } + let preimage = if let Some(preimage) = tlc.payment_preimage { preimage } else if let Some(preimage) = self.store.get_invoice_preimage(&tlc.payment_hash) { diff --git a/src/fiber/network.rs b/src/fiber/network.rs index c51fe09d..6cb18370 100644 --- a/src/fiber/network.rs +++ b/src/fiber/network.rs @@ -280,6 +280,12 @@ impl SendPaymentData { .transpose() .map_err(|_| "invoice is invalid".to_string())?; + if let Some(invoice) = invoice.clone() { + if invoice.is_expired() { + return Err("invoice is expired".to_string()); + } + } + fn validate_field( field: Option, invoice_field: Option, diff --git a/src/fiber/types.rs b/src/fiber/types.rs index 8dd5ffad..096cec2a 100644 --- a/src/fiber/types.rs +++ b/src/fiber/types.rs @@ -1420,6 +1420,7 @@ pub enum TlcErrorCode { IncorrectCltvExpiry = UPDATE | 13, ExpiryTooSoon = UPDATE | 14, IncorrectOrUnknownPaymentDetails = PERM | 15, + InvoiceExpired = PERM | 16, FinalIncorrectCltvExpiry = 18, FinalIncorrectHtlcAmount = 19, ChannelDisabled = UPDATE | 20, @@ -1451,6 +1452,7 @@ impl TlcErrorCode { TlcErrorCode::IncorrectOrUnknownPaymentDetails | TlcErrorCode::FinalIncorrectCltvExpiry | TlcErrorCode::FinalIncorrectHtlcAmount + | TlcErrorCode::InvoiceExpired | TlcErrorCode::MppTimeout => true, _ => false, } diff --git a/src/invoice/invoice_impl.rs b/src/invoice/invoice_impl.rs index b0f0d359..135c7115 100644 --- a/src/invoice/invoice_impl.rs +++ b/src/invoice/invoice_impl.rs @@ -222,6 +222,13 @@ impl CkbInvoice { &self.data.payment_hash } + pub fn is_expired(&self) -> bool { + self.expiry_time().map_or(false, |expiry| { + self.data.timestamp + expiry.as_millis() + < std::time::UNIX_EPOCH.elapsed().unwrap().as_millis() + }) + } + /// Check that the invoice is signed correctly and that key recovery works pub fn check_signature(&self) -> Result<(), InvoiceError> { if self.signature.is_none() { diff --git a/src/invoice/tests/invoice_impl.rs b/src/invoice/tests/invoice_impl.rs index 300018d3..2e4fac1c 100644 --- a/src/invoice/tests/invoice_impl.rs +++ b/src/invoice/tests/invoice_impl.rs @@ -437,3 +437,18 @@ fn test_invoice_udt_script() { let decoded = serde_json::from_str::(&res.unwrap()).unwrap(); assert_eq!(decoded, invoice); } + +#[test] +fn test_invoice_check_expired() { + let private_key = gen_rand_private_key(); + let invoice = InvoiceBuilder::new(Currency::Fibb) + .amount(Some(1280)) + .payment_hash(rand_sha256_hash()) + .expiry_time(Duration::from_secs(1)) + .build_with_sign(|hash| Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key)) + .unwrap(); + + assert_eq!(invoice.is_expired(), false); + std::thread::sleep(Duration::from_secs(2)); + assert_eq!(invoice.is_expired(), true); +} diff --git a/src/rpc/invoice.rs b/src/rpc/invoice.rs index 3ed4b34b..cdf64636 100644 --- a/src/rpc/invoice.rs +++ b/src/rpc/invoice.rs @@ -1,5 +1,6 @@ use std::time::Duration; +use crate::fiber::graph::{NetworkGraphStateStore, PaymentSessionStatus}; use crate::fiber::hash_algorithm::HashAlgorithm; use crate::fiber::serde_utils::{U128Hex, U64Hex}; use crate::fiber::types::Hash256; @@ -32,7 +33,7 @@ pub(crate) struct NewInvoiceParams { } #[derive(Clone, Serialize, Deserialize)] -pub(crate) struct NewInvoiceResult { +pub(crate) struct InvoiceResult { invoice_address: String, invoice: CkbInvoice, } @@ -47,19 +48,45 @@ pub(crate) struct ParseInvoiceResult { invoice: CkbInvoice, } +#[derive(Serialize, Deserialize, Debug)] +pub struct GetInvoiceParams { + payment_hash: Hash256, +} + +#[derive(Clone, Serialize, Deserialize)] +enum InvoiceStatus { + Unpaid, + Inflight, + Paid, + Expired, +} + +#[derive(Clone, Serialize, Deserialize)] +pub(crate) struct GetInvoiceResult { + invoice_address: String, + invoice: CkbInvoice, + status: InvoiceStatus, +} + #[rpc(server)] trait InvoiceRpc { #[method(name = "new_invoice")] async fn new_invoice( &self, params: NewInvoiceParams, - ) -> Result; + ) -> Result; #[method(name = "parse_invoice")] async fn parse_invoice( &self, params: ParseInvoiceParams, ) -> Result; + + #[method(name = "get_invoice")] + async fn get_invoice( + &self, + payment_hash: GetInvoiceParams, + ) -> Result; } pub(crate) struct InvoiceRpcServerImpl { @@ -76,12 +103,12 @@ impl InvoiceRpcServerImpl { #[async_trait] impl InvoiceRpcServer for InvoiceRpcServerImpl where - S: InvoiceStore + Send + Sync + 'static, + S: InvoiceStore + NetworkGraphStateStore + Send + Sync + 'static, { async fn new_invoice( &self, params: NewInvoiceParams, - ) -> Result { + ) -> Result { let mut invoice_builder = InvoiceBuilder::new(params.currency) .amount(Some(params.amount)) .payment_preimage(params.payment_preimage); @@ -116,7 +143,7 @@ where .store .insert_invoice(invoice.clone(), Some(params.payment_preimage)) { - Ok(_) => Ok(NewInvoiceResult { + Ok(_) => Ok(InvoiceResult { invoice_address: invoice.to_string(), invoice, }), @@ -150,4 +177,39 @@ where )), } } + + async fn get_invoice( + &self, + params: GetInvoiceParams, + ) -> Result { + let payment_hash = params.payment_hash; + match self.store.get_invoice(&payment_hash) { + Some(invoice) => { + let invoice_status = if invoice.is_expired() { + InvoiceStatus::Expired + } else { + InvoiceStatus::Unpaid + }; + let payment_session = self.store.get_payment_session(payment_hash); + let status = match payment_session { + Some(session) => match session.status { + PaymentSessionStatus::Inflight => InvoiceStatus::Inflight, + PaymentSessionStatus::Success => InvoiceStatus::Paid, + _ => invoice_status, + }, + None => invoice_status, + }; + Ok(GetInvoiceResult { + invoice_address: invoice.to_string(), + invoice, + status, + }) + } + None => Err(ErrorObjectOwned::owned( + CALL_EXECUTION_FAILED_CODE, + "invoice not found".to_string(), + Some(payment_hash), + )), + } + } } diff --git a/tests/bruno/e2e/router-pay/12-node1-send-payment.bru b/tests/bruno/e2e/router-pay/12-node1-send-payment.bru index fd587bd9..4dc89dc3 100644 --- a/tests/bruno/e2e/router-pay/12-node1-send-payment.bru +++ b/tests/bruno/e2e/router-pay/12-node1-send-payment.bru @@ -37,4 +37,5 @@ assert { script:post-response { // Sleep for sometime to make sure current operation finishes before next request starts. await new Promise(r => setTimeout(r, 100)); + console.log("12 step result: ", res.body); } diff --git a/tests/bruno/e2e/router-pay/22-node3-gen-expiring-invoice.bru b/tests/bruno/e2e/router-pay/22-node3-gen-expiring-invoice.bru new file mode 100644 index 00000000..c33c0e1e --- /dev/null +++ b/tests/bruno/e2e/router-pay/22-node3-gen-expiring-invoice.bru @@ -0,0 +1,65 @@ +meta { + name: generate a invoice which will expiring in short time + type: http + seq: 22 +} + +post { + url: {{NODE3_RPC_URL}} + body: json + auth: none +} + +headers { + Content-Type: application/json + Accept: application/json +} + +body:json { + { + "id": "42", + "jsonrpc": "2.0", + "method": "new_invoice", + "params": [ + { + "amount": "0x613", + "currency": "Fibb", + "description": "test invoice generated by node3", + "expiry": "0x2", + "final_cltv": "0x28", + "payment_preimage": "{{payment_preimage}}" + } + ] + } +} + +assert { + res.body.error: isUndefined + res.body.result: isDefined +} + +script:pre-request { + // generate random preimage + function generateRandomPreimage() { + let hash = '0x'; + for (let i = 0; i < 64; i++) { + hash += Math.floor(Math.random() * 16).toString(16); + } + return hash; + } + const payment_preimage = generateRandomPreimage(); + bru.setVar("payment_preimage", payment_preimage); + let hash_algorithm = bru.getEnvVar("HASH_ALGORITHM"); + if (hash_algorithm !== null) { + let body = req.getBody(); + body.params[0].hash_algorithm = hash_algorithm; + req.setBody(body); + } +} + +script:post-response { + // Sleep for sometime to make sure current operation finishes before next request starts. + await new Promise(r => setTimeout(r, 3000)); + console.log("generated result: ", res.body.result); + bru.setVar("encoded_invoice", res.body.result.invoice_address); +} diff --git a/tests/bruno/e2e/router-pay/23-node1-send-payment-will-fail.bru b/tests/bruno/e2e/router-pay/23-node1-send-payment-will-fail.bru new file mode 100644 index 00000000..08969000 --- /dev/null +++ b/tests/bruno/e2e/router-pay/23-node1-send-payment-will-fail.bru @@ -0,0 +1,48 @@ +meta { + name: Node1 send payment with router + type: http + seq: 23 +} + +post { + url: {{NODE1_RPC_URL}} + body: json + auth: none +} + +headers { + Content-Type: application/json + Accept: application/json +} + +body:json { + { + "id": "42", + "jsonrpc": "2.0", + "method": "send_payment", + "params": [ + { + "invoice": "{{encoded_invoice}}" + } + ] + } +} + +assert { + res.body.error: isDefined +} + + +script:pre-request { + // sleep for a while + await new Promise(r => setTimeout(r, 1000)); +} + + +script:post-response { + // Sleep for sometime to make sure current operation finishes before next request starts. + await new Promise(r => setTimeout(r, 100)); + if (!(res.body.error.message.includes("invoice is expired"))) { + throw new Error("Assertion failed: error message is not right"); + } +}