Skip to content

Commit

Permalink
test: HTLC handling
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 committed Aug 21, 2024
1 parent 32f4c4d commit 1dff07b
Show file tree
Hide file tree
Showing 6 changed files with 623 additions and 4 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ python-install:
python-lint:
cd tests && poetry run ruff check

python-lint-fix:
cd tests && poetry run ruff check --fix

python-format:
cd tests && poetry run ruff format

Expand Down
13 changes: 10 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::handler::Handler;
use crate::settler::Settler;
use anyhow::Result;
use cln_plugin::{Builder, RpcMethodBuilder};
use log::{debug, error, info};
use log::{debug, error, info, warn};
use std::fs;
use std::path::Path;
use tokio_util::sync::CancellationToken;
Expand Down Expand Up @@ -82,7 +82,7 @@ async fn main() -> Result<()> {
}
};

let mpp_timeout = match plugin.option(&OPTION_MPP_TIMEOUT) {
let mut mpp_timeout = match plugin.option(&OPTION_MPP_TIMEOUT) {
Ok(timeout) => {
if timeout < 0 {
plugin.disable("MPP timeout has to be positive").await?;
Expand Down Expand Up @@ -146,6 +146,13 @@ async fn main() -> Result<()> {
}
};

let is_regtest = config.network == "regtest";

if is_regtest {
mpp_timeout = 10;
warn!("Using MPP timeout of {} seconds on regtest", mpp_timeout);
}

let invoice_helper = database::helpers::invoice_helper::InvoiceHelperDatabase::new(db);
let mut settler = Settler::new(invoice_helper.clone(), mpp_timeout);

Expand All @@ -163,7 +170,7 @@ async fn main() -> Result<()> {
let grpc_server = grpc::server::Server::new(
&grpc_host,
grpc_port,
config.network == "regtest",
is_regtest,
cancellation_token.clone(),
std::env::current_dir()?.join(utils::built_info::PKG_NAME),
invoice_helper,
Expand Down
152 changes: 152 additions & 0 deletions tests/hold/test_htlcs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import json
import time

import bolt11
import pytest
from bolt11 import MilliSatoshi
from bolt11.models.tags import TagChar

from hold.protos.hold_pb2 import (
Invoice,
InvoiceRequest,
InvoiceResponse,
InvoiceState,
ListRequest,
SettleRequest,
)
from hold.protos.hold_pb2_grpc import HoldStub
from hold.utils import (
LndPay,
hold_client,
lightning,
lnd,
new_preimage,
new_preimage_bytes,
)


def assert_failed_payment(
cl: HoldStub, payment_hash: bytes, dec: bolt11.Bolt11
) -> None:
invoice = lightning("signinvoice", bolt11.encode(dec))["bolt11"]

pay = LndPay(1, invoice)
pay.start()
pay.join()

assert pay.res["status"] == "FAILED"
assert pay.res["failure_reason"] == "FAILURE_REASON_INCORRECT_PAYMENT_DETAILS"

list_invoice: Invoice = cl.List(
ListRequest(
payment_hash=payment_hash,
)
).invoices[0]
assert list_invoice.state == InvoiceState.UNPAID
assert len(list_invoice.htlcs) == 1
assert list_invoice.htlcs[0].state == InvoiceState.CANCELLED

# Poor man's way to check if there is a pending HTLC for that hash
assert payment_hash.hex() not in json.dumps(lightning("listpeerchannels"))


class TestHtlcs:
@pytest.fixture(scope="class", autouse=True)
def cl(self) -> HoldStub:
(channel, client) = hold_client()

yield client

channel.close()

def test_ignore_non_hold_invoice(self) -> None:
invoice = lightning("invoice", "1000", new_preimage()[0], "invoice-test")[
"bolt11"
]

pay = LndPay(1, invoice)
pay.start()
pay.join()

assert pay.res["status"] == "SUCCEEDED"

def test_ignore_forward(self) -> None:
hold_node_id = lightning("getinfo")["id"]
outgoing_channel = next(
c
for c in lnd("listchannels")["channels"]
if c["remote_pubkey"] == hold_node_id
)["chan_id"]

invoice = lnd("addinvoice", "1000", node=2)["payment_request"]

pay = LndPay(1, invoice, outgoing_chan_id=outgoing_channel)
pay.start()
pay.join()

assert pay.res["status"] == "SUCCEEDED"

def test_invalid_payment_secret(self, cl: HoldStub) -> None:
(_, payment_hash) = new_preimage_bytes()
invoice: InvoiceResponse = cl.Invoice(
InvoiceRequest(payment_hash=payment_hash, amount_msat=21_000)
)

dec = bolt11.decode(invoice.bolt11)
dec.tags.get(TagChar.payment_secret).data = new_preimage()[0]

assert_failed_payment(cl, payment_hash, dec)

def test_invalid_final_cltv_expiry(self, cl: HoldStub) -> None:
(_, payment_hash) = new_preimage_bytes()
min_final_cltv_expiry = 80
invoice: InvoiceResponse = cl.Invoice(
InvoiceRequest(
payment_hash=payment_hash,
amount_msat=21_000,
min_final_cltv_expiry=min_final_cltv_expiry,
)
)

dec = bolt11.decode(invoice.bolt11)
dec.tags.get(TagChar.min_final_cltv_expiry).data = min_final_cltv_expiry - 21

assert_failed_payment(cl, payment_hash, dec)

def test_acceptable_overpayment(self, cl: HoldStub) -> None:
(preimage, payment_hash) = new_preimage_bytes()
amount = 21_000
invoice: InvoiceResponse = cl.Invoice(
InvoiceRequest(
payment_hash=payment_hash,
amount_msat=amount,
)
)

dec = bolt11.decode(invoice.bolt11)
dec.amount_msat = MilliSatoshi(amount * 2)

invoice_signed = lightning("signinvoice", bolt11.encode(dec))["bolt11"]
pay = LndPay(1, invoice_signed)
pay.start()

time.sleep(1)
cl.Settle(SettleRequest(payment_preimage=preimage))

pay.join()
assert pay.res["status"] == "SUCCEEDED"

def test_unacceptable_overpayment(self, cl: HoldStub) -> None:
(preimage, payment_hash) = new_preimage_bytes()
amount = 21_000
invoice: InvoiceResponse = cl.Invoice(
InvoiceRequest(
payment_hash=payment_hash,
amount_msat=amount,
)
)

dec = bolt11.decode(invoice.bolt11)
dec.amount_msat = MilliSatoshi((amount * 2) + 1)

assert_failed_payment(cl, payment_hash, dec)
87 changes: 87 additions & 0 deletions tests/hold/test_mpp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import json
import time

import bolt11
import pytest
from bolt11 import MilliSatoshi

from hold.protos.hold_pb2 import (
Invoice,
InvoiceRequest,
InvoiceResponse,
InvoiceState,
ListRequest,
SettleRequest,
)
from hold.protos.hold_pb2_grpc import HoldStub
from hold.utils import LndPay, hold_client, lightning, new_preimage_bytes


class TestMpp:
@pytest.fixture(scope="class", autouse=True)
def cl(self) -> HoldStub:
(channel, client) = hold_client()

yield client

channel.close()

@pytest.mark.parametrize("parts", [2, 4, 5, 10])
def test_mpp_payment(self, cl: HoldStub, parts: int) -> None:
(preimage, payment_hash) = new_preimage_bytes()
amount = 20_000
invoice: InvoiceResponse = cl.Invoice(
InvoiceRequest(payment_hash=payment_hash, amount_msat=amount)
)

shard_size = int(amount / parts / 1_000)
pay = LndPay(1, invoice.bolt11, max_shard_size=shard_size)
pay.start()

# 10 parts can take a little longer than a second
time.sleep(2)
info: Invoice = cl.List(ListRequest(payment_hash=payment_hash)).invoices[0]
assert info.state == InvoiceState.ACCEPTED
assert len(info.htlcs) == parts
assert all(htlc.state == InvoiceState.ACCEPTED for htlc in info.htlcs)
assert all(htlc.msat == int(amount / parts) for htlc in info.htlcs)

cl.Settle(SettleRequest(payment_preimage=preimage))

pay.join()
assert pay.res["status"] == "SUCCEEDED"

def test_mpp_timeout(self, cl: HoldStub) -> None:
(_, payment_hash) = new_preimage_bytes()
amount = 20_000
invoice: InvoiceResponse = cl.Invoice(
InvoiceRequest(payment_hash=payment_hash, amount_msat=amount)
)

dec = bolt11.decode(invoice.bolt11)
dec.amount_msat = MilliSatoshi(dec.amount_msat - 1_000)

pay = LndPay(
1, lightning("signinvoice", bolt11.encode(dec))["bolt11"], timeout=1
)
pay.start()
pay.join()

assert pay.res["status"] == "FAILED"
assert pay.res["failure_reason"] == "FAILURE_REASON_TIMEOUT"
assert len(pay.res["htlcs"]) == 1

htlc = pay.res["htlcs"][0]
assert htlc["failure"]["code"] == "MPP_TIMEOUT"

list_invoice: Invoice = cl.List(
ListRequest(
payment_hash=payment_hash,
)
).invoices[0]
assert list_invoice.state == InvoiceState.UNPAID
assert len(list_invoice.htlcs) == 1
assert list_invoice.htlcs[0].state == InvoiceState.CANCELLED

# Poor man's way to check if there is a pending HTLC for that hash
assert payment_hash.hex() not in json.dumps(lightning("listpeerchannels"))
Loading

0 comments on commit 1dff07b

Please sign in to comment.