rgb-cln/external/lnprototest/tests/test_bolt7-20-query_channel...

416 lines
14 KiB
Python

#! /usr/bin/env python3
# Tests for gossip_timestamp_filter
from lnprototest import (
Connect,
Block,
ExpectMsg,
Msg,
RawMsg,
Funding,
Event,
Side,
MustNotMsg,
OneOf,
Runner,
bitfield,
TryAll,
Sequence,
regtest_hash,
CheckEq,
EventError,
namespace,
Wait,
)
from helpers import tx_spendable, utxo
from typing import Optional
import unittest
import time
import io
import zlib
import crc32c
from pyln.spec.bolt7 import channel_update_timestamps
from pyln.proto.message import Message
# Note for gossip_channel_range: we are *allowed* to return a superset
# of what they ask, so if someone does that this test must be modified
# to accept it (add an option_IMPL_gossip_query_superset?).
#
# Meanwhile, we assume an exact reply.
def encode_timestamps(t1: int = 0, t2: int = 0) -> str:
# BOLT #7:
# For a single `channel_update`, timestamps are encoded as:
#
# 1. subtype: `channel_update_timestamps`
# 2. data:
# * [`u32`:`timestamp_node_id_1`]
# * [`u32`:`timestamp_node_id_2`]
#
# Where:
# * `timestamp_node_id_1` is the timestamp of the `channel_update` for
# `node_id_1`, or 0 if there was no `channel_update` from that node.
# * `timestamp_node_id_2` is the timestamp of the `channel_update` for
# `node_id_2`, or 0 if there was no `channel_update` from that node.
v, _ = channel_update_timestamps.val_from_str(
"{{timestamp_node_id_1={},timestamp_node_id_2={}}}".format(t1, t2)
)
buf = io.BytesIO()
channel_update_timestamps.write(buf, v, {})
return buf.getvalue().hex()
def decode_timestamps(runner: "Runner", event: Event, field: str) -> str:
# Get timestamps from last reply_channel_range msg
timestamps = runner.get_stash(event, "ExpectMsg")[-1][1]["tlvs"]["timestamps_tlv"]
# BOLT #7:
# Encoding types:
# * `0`: uncompressed array of `short_channel_id` types, in ascending
# order.
# * `1`: array of `short_channel_id` types, in ascending order, compressed
# with zlib deflate<sup>[1](#reference-1)</sup>
if timestamps["encoding_type"] == 0:
b = bytes.fromhex(timestamps["encoded_timestamps"])
elif timestamps["encoding_type"] == 1:
b = zlib.decompress(bytes.fromhex(timestamps["encoded_timestamps"]))
else:
raise EventError(event, "Unknown encoding type: {}".format(timestamps))
return b.hex()
def decode_scids(runner: "Runner", event: Event, field: str) -> str:
# Nothing to decode if dummy runner.
if runner._is_dummy():
return ""
# Get encoded_short_ids from last msg.
encoded = bytes.fromhex(
runner.get_stash(event, "ExpectMsg")[-1][1]["encoded_short_ids"]
)
# BOLT #7:
# Encoding types:
# * `0`: uncompressed array of `short_channel_id` types, in ascending
# order.
# * `1`: array of `short_channel_id` types, in ascending order, compressed
# with zlib deflate<sup>[1](#reference-1)</sup>
if encoded[0] == 0:
b = encoded[1:]
elif encoded[0] == 1:
b = zlib.decompress(encoded[1:])
else:
raise EventError(
event, "Unknown encoding type {}: {}".format(encoded[0], encoded.hex())
)
scidtype = namespace().get_fundamentaltype("short_channel_id")
arr = []
buf = io.BytesIO(b)
while True:
scid = scidtype.read(buf, {})
if scid is None:
break
arr.append(scid)
return ",".join([scidtype.val_to_str(a, {}) for a in arr])
def calc_checksum(update: Message) -> int:
# BOLT #7: The checksum of a `channel_update` is the CRC32C checksum as
# specified in [RFC3720](https://tools.ietf.org/html/rfc3720#appendix-B.4)
# of this `channel_update` without its `signature` and `timestamp` fields.
bufio = io.BytesIO()
update.write(bufio)
buf = bufio.getvalue()
# BOLT #7:
# 1. type: 258 (`channel_update`)
# 2. data:
# * [`signature`:`signature`]
# * [`chain_hash`:`chain_hash`]
# * [`short_channel_id`:`short_channel_id`]
# * [`u32`:`timestamp`]
# * [`byte`:`message_flags`]
# Note: 2 bytes for `type` field
return crc32c.crc32c(buf[2 + 64 : 2 + 64 + 32 + 8] + buf[2 + 64 + 32 + 8 + 4 :])
def update_checksums(update1: Optional[Message], update2: Optional[Message]) -> str:
# BOLT #7:
# For a single `channel_update`, checksums are encoded as:
#
# 1. subtype: `channel_update_checksums`
# 2. data:
# * [`u32`:`checksum_node_id_1`]
# * [`u32`:`checksum_node_id_2`]
#
# Where:
# * `checksum_node_id_1` is the checksum of the `channel_update` for
# `node_id_1`, or 0 if there was no `channel_update` from that node.
# * `checksum_node_id_2` is the checksum of the `channel_update` for
# `node_id_2`, or 0 if there was no `channel_update` from that node.
if update1:
csum1 = calc_checksum(update1)
else:
csum1 = 0
if update2:
csum2 = calc_checksum(update2)
else:
csum2 = 0
return "{{checksum_node_id_1={},checksum_node_id_2={}}}".format(csum1, csum2)
def test_query_channel_range(runner: Runner) -> None:
if runner.has_option("option_gossip_queries") is None:
unittest.SkipTest("Needs option_gossip_queries")
funding1, funding1_tx = Funding.from_utxo(
*utxo(0),
local_node_privkey="02",
local_funding_privkey="10",
remote_node_privkey="03",
remote_funding_privkey="20"
)
funding2, funding2_tx = Funding.from_utxo(
*utxo(1),
local_node_privkey="04",
local_funding_privkey="30",
remote_node_privkey="05",
remote_funding_privkey="40"
)
timestamp_103x1x0_LOCAL = int(time.time())
timestamp_109x1x0_LOCAL = timestamp_103x1x0_LOCAL - 1
timestamp_109x1x0_REMOTE = timestamp_109x1x0_LOCAL - 1
ts_103x1x0 = encode_timestamps(*funding1.node_id_sort(timestamp_103x1x0_LOCAL, 0))
ts_109x1x0 = encode_timestamps(
*funding2.node_id_sort(timestamp_109x1x0_LOCAL, timestamp_109x1x0_REMOTE)
)
update_103x1x0_LOCAL = funding1.channel_update(
side=Side.local,
short_channel_id="103x1x0",
disable=False,
cltv_expiry_delta=144,
htlc_minimum_msat=0,
fee_base_msat=1000,
fee_proportional_millionths=10,
timestamp=timestamp_103x1x0_LOCAL,
htlc_maximum_msat=None,
)
update_109x1x0_LOCAL = funding2.channel_update(
side=Side.local,
short_channel_id="109x1x0",
disable=False,
cltv_expiry_delta=144,
htlc_minimum_msat=0,
fee_base_msat=1000,
fee_proportional_millionths=10,
timestamp=timestamp_109x1x0_LOCAL,
htlc_maximum_msat=None,
)
update_109x1x0_REMOTE = funding2.channel_update(
side=Side.remote,
short_channel_id="109x1x0",
disable=False,
cltv_expiry_delta=144,
htlc_minimum_msat=0,
fee_base_msat=1000,
fee_proportional_millionths=10,
timestamp=timestamp_109x1x0_REMOTE,
htlc_maximum_msat=None,
)
csums_103x1x0 = update_checksums(*funding1.node_id_sort(update_103x1x0_LOCAL, None))
csums_109x1x0 = update_checksums(
*funding2.node_id_sort(update_109x1x0_LOCAL, update_109x1x0_REMOTE)
)
test = [
Block(blockheight=102, txs=[tx_spendable]),
# Channel 103x1x0 (between 002 and 003)
Block(blockheight=103, number=6, txs=[funding1_tx]),
# Channel 109x1x0 (between 004 and 005)
Block(blockheight=109, number=6, txs=[funding2_tx]),
Connect(connprivkey="03"),
ExpectMsg("init"),
Msg("init", globalfeatures="", features=""),
RawMsg(funding1.channel_announcement("103x1x0", "")),
RawMsg(update_103x1x0_LOCAL),
RawMsg(funding2.channel_announcement("109x1x0", "")),
RawMsg(update_109x1x0_LOCAL),
RawMsg(update_109x1x0_REMOTE),
# c-lightning gets a race condition if we dont wait for
# these updates to be added to the gossip store
# FIXME: convert to explicit signal
Wait(1),
# New peer connects, with gossip_query option.
Connect(connprivkey="05"),
ExpectMsg("init"),
# BOLT #9:
# | 6/7 | `gossip_queries` | More sophisticated gossip control
Msg("init", globalfeatures="", features=bitfield(7)),
TryAll(
# No queries? Must not get anything.
[
MustNotMsg("channel_announcement"),
MustNotMsg("channel_update"),
MustNotMsg("node_announcement"),
],
# This should elicit an empty response
[
Msg(
"query_channel_range",
chain_hash=regtest_hash,
first_blocknum=0,
number_of_blocks=103,
),
ExpectMsg(
"reply_channel_range",
chain_hash=regtest_hash,
first_blocknum=0,
number_of_blocks=103,
),
CheckEq(decode_scids, ""),
],
# This should get the first one, not the second.
[
Msg(
"query_channel_range",
chain_hash=regtest_hash,
first_blocknum=103,
number_of_blocks=1,
),
ExpectMsg(
"reply_channel_range",
chain_hash=regtest_hash,
first_blocknum=103,
number_of_blocks=1,
),
CheckEq(decode_scids, "103x1x0"),
],
# This should get the second one, not the first.
[
Msg(
"query_channel_range",
chain_hash=regtest_hash,
first_blocknum=109,
number_of_blocks=4294967295,
),
OneOf(
ExpectMsg(
"reply_channel_range",
chain_hash=regtest_hash,
first_blocknum=109,
number_of_blocks=4294967186,
),
# Could truncate number_of_blocks.
ExpectMsg(
"reply_channel_range",
chain_hash=regtest_hash,
first_blocknum=109,
number_of_blocks=1,
),
),
CheckEq(decode_scids, "109x1x0"),
],
# This should get both.
[
Msg(
"query_channel_range",
chain_hash=regtest_hash,
first_blocknum=103,
number_of_blocks=7,
),
ExpectMsg(
"reply_channel_range",
chain_hash=regtest_hash,
first_blocknum=103,
number_of_blocks=7,
),
CheckEq(decode_scids, "103x1x0,109x1x0"),
],
# This should get appended timestamp fields with option_gossip_queries_ex
Sequence(
enable=runner.has_option("option_gossip_queries_ex") is not None,
events=[
Msg(
"query_channel_range",
chain_hash=regtest_hash,
first_blocknum=103,
number_of_blocks=7,
tlvs="{query_option={query_option_flags=1}}",
),
ExpectMsg(
"reply_channel_range",
chain_hash=regtest_hash,
first_blocknum=103,
number_of_blocks=7,
),
CheckEq(decode_timestamps, ts_103x1x0 + ts_109x1x0),
CheckEq(decode_scids, "103x1x0,109x1x0"),
],
),
# This should get appended checksum fields with option_gossip_queries_ex
Sequence(
enable=runner.has_option("option_gossip_queries_ex") is not None,
events=[
Msg(
"query_channel_range",
chain_hash=regtest_hash,
first_blocknum=103,
number_of_blocks=7,
tlvs="{query_option={query_option_flags=2}}",
),
ExpectMsg(
"reply_channel_range",
chain_hash=regtest_hash,
first_blocknum=103,
number_of_blocks=7,
tlvs="{checksums_tlv={checksums=["
+ csums_103x1x0
+ ","
+ csums_109x1x0
+ "]}}",
),
CheckEq(decode_scids, "103x1x0,109x1x0"),
],
),
# This should append timestamps and checksums with option_gossip_queries_ex
Sequence(
enable=runner.has_option("option_gossip_queries_ex") is not None,
events=[
Msg(
"query_channel_range",
chain_hash=regtest_hash,
first_blocknum=103,
number_of_blocks=7,
tlvs="{query_option={query_option_flags=3}}",
),
ExpectMsg(
"reply_channel_range",
chain_hash=regtest_hash,
first_blocknum=103,
number_of_blocks=7,
tlvs="{checksums_tlv={checksums=["
+ csums_103x1x0
+ ","
+ csums_109x1x0
+ "]}}",
),
CheckEq(decode_timestamps, ts_103x1x0 + ts_109x1x0),
CheckEq(decode_scids, "103x1x0,109x1x0"),
],
),
),
]
runner.run(test)