“写笔记”支持四种格式——Word 文档、Excel 表格、Markdown、纯文本,起稿或二次编辑时都能随时切换,同一篇笔记想用哪种形态来记,都由你说了算。
md、txt、csv、json 这类纯文本则原样载入,不做多余加工。拿一张现成的表倒进来、改几笔、再导出去,等于白用一台免费的格式转换器。
要带走就在右上角点“下载”,可导出 PDF、Word、Markdown、Excel、TXT 等格式;列表卡片“⋯”菜单里,也有同样的下载入口。
在“工具”页点“+ 上传工具”即可发布:填好名称与链接,再用 Markdown 把使用方法写清楚——能解决什么问题、怎么装、怎么用,比堆介绍实在。
要分发安装包就一并上传压缩包(ZIP、RAR、7Z、TAR.GZ,最大 35MB),别人在详情页一键下载;只放链接不带附件也可以。
工具按大家的收藏热度排序,好用的自然会被顶上来。发布后可在详情页或卡片菜单里编辑、下架。
写笔记时勾上“隐藏”,这篇就只存在于你自己的账号里:不进列表、不进搜索、不上首页精选,也不会出现在任何公开的页面,链接发给别人同样打不开。
适合放密码、草稿、日记这类只给自己看的内容;想公开,去“发布”打开它,把“隐藏”的勾去掉再保存,之后编辑会默认保持原状态,不会悄悄变回公开。
你的内容会同时保存在多个副本上,系统定期做备份与完整性校验,再配合异地容灾机制:就算某台机器出问题,数据也不会丢,可以长期放心存放;特别重要的资料,仍建议你另外再留一份备份。
全站跑在容器化、模块化的现代架构上,更新、部署、回滚都很快,扩展性和稳定性都按长期运营的标准来设计(Built for reliability, designed to scale)。
这个网站最早只是一个人的笔记仓库,后来慢慢长成现在的知识中枢。设计上很克制——没有广告、没有追踪、没有推荐算法,只是干干净净地存放一些东西;既然做好了,就公开出来,万一有人用得上呢。
不做大而全,不做平台梦,保持简单、保持克制、保持好奇。所有内容都由用户贡献、由用户维护:不会突然冒出付费墙,不会在角落塞广告位,也不会把你的数据卖给第三方。
产品会持续迭代,站内日志页记录着每一次改动,改了什么都有迹可循;想了解这个站是怎么一步步走到今天的,翻翻日志就能看到来龙去脉。
如果在这里看到涉嫌违规的内容,点对应卡片右侧的“举报”按钮就能提交,我们会尽快核实处理;也谢谢你花一点时间,一起把这里维护干净。
从零构建一个Rust版闪电网络节点:从环境准备到支付请求的完整指南
好吧,我先坦白:这篇文章有点长。但如果你真的想搞明白怎么在Rust里跑一个闪电网络节点,我觉得你会感谢我写这么详细的。这不是那种“复制粘贴就能跑”的教程——虽然你确实能复制粘贴——而是让你真正理解每一行代码在干什么。
如果你在Bitcoin开发者圈子里混过一段时间,你肯定听说过LDK(Lightning Development Kit)。它是一个完全模块化的Rust库,让你可以按自己的想法构建闪电网络节点。功能强大、配置灵活,但说实话,如果你只是想快点跑起来一个能用的东西,它确实有点吓人。
我本人是ldk-node的贡献者。当初被这个项目吸引,是因为它的目标特别清晰:你不应该为了发一笔支付就去搞懂闪电协议的所有底层细节。LDK Node就是LDK,但帮你把那些艰难的决定都做好了。它的API接口很小,默认配置很合理,而且结构足够扎实,可以直接上生产环境。
这篇文章会带你从零开始构建一个能工作的闪电网络节点。到结尾的时候,你的节点将能够:连接对等节点、打开通道、处理通道就绪事件、接收BOLT11支付、发送BOLT11支付、创建BOLT12发票并支付、以及发送无需发票的自发支付。每一步你运行代码的时候,都能看到节点返回了什么,这样你就能在脑子里建立起一个真实的运行模型。
你不需要有闪电网络实现的实战经验。只要熟悉Rust,对比特币和闪电网络有基本了解,就能跟得上。
LDK Node是什么,不是什么
LDK(底层库)暴露了一大堆方法。这种级别的控制力,对于需要自定义对等节点管理、搞各种花式通道配置、或者严格管理密钥的系统来说,是非常理想的。但如果你只是想在应用里嵌入一个节点,它就不是最合适的工具了。
LDK Node通过替你做出具体的选择,大大简化了API接口:
- BDK(Bitcoin Dev Kit) 处理链上钱包
- 链数据来自Esplora、Electrum或Bitcoin Core RPC
- 状态持久化到SQLite、Postgres或文件系统
- Gossip数据来自闪电网络的P2P网络或Rapid Gossip Sync
- 熵来自原始字节或BIP39助记词
你牺牲了一些可配置性,换来了迭代速度。这些默认配置覆盖了大多数真实使用场景。如果你以后需要更多控制权,底层的LDK仍然是可以直接访问的。
它用Rust编写,并且通过UniFFI绑定了Swift、Kotlin和Python,如果你想做移动端开发的话。
前置准备
在写任何代码之前,你需要安装Rust。如果你还没有装,用rustup来安装:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
按照屏幕上的指示操作,然后重新加载你的shell环境:
source ~/.cargo/env
验证安装是否成功:
rustc --version
cargo --version
为了获得完整的交互式体验——在两个节点之间进行真实的支付——你还需要安装Polar。Polar是一个桌面应用,可以一键启动一个本地的闪电网络。如果你还没装,去Polar官网下载。
这篇文章会用到两个节点:节点A(我们构建的ldk-node)和节点B(Polar管理的节点),用来演示所有支付方向。
注意:LDK Server正在被加入到Polar中(PR #1374)。合并之后,你就可以直接在Polar里把它作为本地节点后端使用了,因为LDK Server本质上就是带RPC接口的ldk-node。它也可以作为支持onion-message的对等节点,用于创建BOLT12发票。
注意:要创建BOLT12发票,你的ldk-node需要连接到一个支持onion-message的对等节点。在Polar里,这意味着你需要往网络里添加一个CLN节点。LND目前不支持onion message,所以如果没有CLN,创建发票会失败。
有了这些之后,创建一个新的二进制项目:
cargo new ldk_node_example
cd ldk_node_example
Cargo会生成这样的目录结构:
ldk_node_example/
├── Cargo.toml
└── src/
└── main.rs
打开src/main.rs,你会看到一个默认的"Hello, world!"程序。接下来的内容会把它完全替换掉。
项目配置
把ldk-node和tokio添加到你的Cargo.toml:
[dependencies]
ldk-node = "0.7"
tokio = { version = "1", features = ["full"] }
完整的src/main.rs会随着文章的推进,一个函数一个函数地构建起来。每个小节添加一个函数,main()按顺序调用它们。到最后,你会得到一个单一文件,编译运行后就是一个完整可工作的节点。
辅助函数:打印节点状态
在写节点逻辑之前,先在src/main.rs顶部添加两个辅助函数。每次执行有意义的操作后都会调用它们,这样你就能清楚地看到节点当前知道什么:通道、支付方向、状态、金额——全部打印在一个地方。
use ldk_node::payment::{PaymentDirection, PaymentStatus};
fn print_channels(node: &ldk_node::Node) {
let channels = node.list_channels();
if channels.is_empty() {
println!("[channels] none");
return;
}
for c in channels {
println!(
"[channel] id: {} | peer: {} | capacity: {} sats | ready: {}",
c.channel_id,
c.counterparty_node_id,
c.channel_value_sats,
c.is_channel_ready,
);
}
}
fn print_payments(node: &ldk_node::Node) {
let payments = node.list_payments();
if payments.is_empty() {
println!("[payments] none");
return;
}
for p in payments {
let direction = match p.direction {
PaymentDirection::Inbound => "INBOUND ",
PaymentDirection::Outbound => "OUTBOUND",
};
let status = match p.status {
PaymentStatus::Pending => "pending",
PaymentStatus::Succeeded => "succeeded",
PaymentStatus::Failed => "failed",
};
println!(
"[payment] {} | {:?} | {} | {} msats | id: {:?}",
direction,
p.kind,
status,
p.amount_msat.unwrap_or(0),
p.id,
);
}
}
print_channels显示每个打开或待处理的通道,包括它的容量和是否已准备好路由支付。print_payments显示节点见过的每笔支付,包括方向、类型(bolt11、spontaneous或onchain)、状态和金额。随着文章的推进,你会看到这两个列表不断增长。
构建节点
进入ldk-node的入口点是Builder。你配置它,调用build(),然后得到一个Node,它会从那一刻起管理所有事情。
这篇文章使用Bitcoin Core RPC作为链数据源,这也是Polar在底层运行的。打开Polar,点击Bitcoin Core节点,在节点设置面板里找到RPC凭据。你需要host、port、username和password。
把这个函数添加到src/main.rs:
use ldk_node::Builder;
use ldk_node::bitcoin::Network;
fn build_node() -> ldk_node::Node {
let mut builder = Builder::new();
builder.set_network(Network::Regtest);
builder.set_chain_source_bitcoind_rpc(
"127.0.0.1".to_string(), // RPC host from Polar
18443, // RPC port from Polar
"polaruser".to_string(), // RPC username from Polar
"polarpass".to_string(), // RPC password from Polar
);
builder.set_gossip_source_p2p();
builder.build_with_fs_store().unwrap()
}
关于网络:Polar默认运行本地的Regtest网络,所以Network::Regtest是正确的选择。如果你想用Mutinynet或其他Signet?把这个换成Network::Signet,然后用set_chain_source_esplora()配合Esplora的端点。完整的链数据源和存储选项列表在ldk-node文档里。
关于Gossip:在Polar的本地Regtest网络上,P2P gossip是自然的选择。Rapid Gossip Sync更适合连接到公共闪电网络,因为从快照启动比爬取对等节点更快。
关于熵:这个例子为了简洁使用了文件系统派生的熵。但说实话,生产环境我建议你用BIP39助记词——更安全,也更容易备份。
启动节点并连接对等节点
节点构建好了,现在需要启动它。启动之后,节点就会开始同步链数据、监听P2P连接、处理gossip消息。然后我们需要让它连接到Polar里的节点B。
从Polar里找到节点B的节点ID和地址。在Polar里点击节点B,它的连接信息会显示在面板上。通常格式是:节点ID@IP:端口。在本地Polar网络里,节点ID是一个十六进制字符串,IP是127.0.0.1,端口是Polar分配的一个端口(比如9735)。
添加这个函数:
fn start_and_connect(node: &ldk_node::Node) {
node.start().unwrap();
println!("Node started successfully");
// 连接到Polar中的节点B
// 把下面这些换成你Polar节点B的实际信息
let node_b_id = "02f1a8c87607a4d8b8d2c0c3f8c0d3e4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e";
let node_b_addr = "127.0.0.1:9735";
node.connect(node_b_id, node_b_addr, false).unwrap();
println!("Connected to node B: {}", node_b_id);
}
这里有个关键参数:connect方法的第三个参数false表示“不持久化连接”。也就是说,节点重启后不会自动重连。如果你想让连接持久化,传true。但现在这样就行。
打开通道
连接建立之后,就可以打开通道了。通道是闪电网络的基础——它们是两个节点之间专用的、多签名的支付通道。打开通道需要一笔链上交易,所以需要等它被确认。
打开通道需要指定:对等节点ID、通道容量(以聪为单位)、以及你愿意投入的资金量。这里我们打开一个容量为100,000聪的通道,全部资金都由我们这端提供。
fn open_channel(node: &ldk_node::Node) {
let node_b_id = "02f1a8c87607a4d8b8d2c0c3f8c0d3e4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e";
let channel_amount_sats = 100_000;
let push_to_counterparty_msat = 0; // 不给对等节点任何初始资金
let channel_id = node.open_channel(
node_b_id,
channel_amount_sats,
push_to_counterparty_msat,
None, // 可选参数:通道费率的配置
).unwrap();
println!("Channel opened: {}", channel_id);
}
push_to_counterparty_msat参数很有意思。如果你设成非零值,就相当于在通道打开的同时,立即给对等节点分配一些资金(以毫聪为单位)。这样对方马上就有钱可以发支付了。这里我们设成0——我们自己控制全部资金,后续再通过正常支付把资金转给对方。
等待通道就绪
通道打开后,需要等链上交易被确认。在Regtest网络上,Polar会自动挖矿,所以通常几秒钟就好了。但我们需要显式地等待通道状态变为“就绪”。
这里有个关键点:is_channel_ready标志位。当通道的funding交易获得足够多的确认数(默认是3个,但可以在Builder里配置)后,节点会把这个通道标记为就绪。只有就绪的通道才能用来路由支付。
添加这个轮询函数:
fn wait_for_channel_ready(node: &ldk_node::Node) {
println!("Waiting for channel to be ready...");
loop {
let channels = node.list_channels();
let ready = channels.iter().any(|c| c.is_channel_ready);
if ready {
println!("Channel is ready!");
break;
}
std::thread::sleep(std::time::Duration::from_secs(2));
}
}
这个循环每2秒检查一次。在生产环境里,你肯定不会这么干——你会用事件回调或者WebSocket通知。但为了这个教程,轮询是最简单直接的方式,而且你能亲眼看到状态的变化。
接收BOLT11支付
通道就绪了,现在可以收钱了。BOLT11是最常见的闪电网络发票格式。要接收支付,你需要创建一个发票,然后把发票提供给付款方。
创建发票需要指定:金额(毫聪)、以及一个可选的描述字符串。节点会返回一个BOLT11字符串,这就是你的收款地址。
fn receive_payment(node: &ldk_node::Node) {
let amount_msat = 50_000; // 50,000 msats = 50 sats
let description = "Test payment from Polar node".to_string();
let invoice = node.receive_payment(amount_msat, description, 3600).unwrap();
println!("BOLT11 invoice: {}", invoice);
// 这里你会把发票给节点B,让节点B支付
// 在实际场景中,你需要把这个invoice字符串传给Polar节点B
println!("Use this invoice to pay from Polar node B");
// 等待支付到账
loop {
let payments = node.list_payments();
let received = payments.iter().any(|p| {
matches!(p.direction, PaymentDirection::Inbound)
&& p.status == PaymentStatus::Succeeded
&& p.amount_msat == Some(amount_msat)
});
if received {
println!("Payment received!");
break;
}
std::thread::sleep(std::time::Duration::from_secs(2));
}
}
receive_payment的第三个参数是发票的过期时间(秒)。3600秒=1小时,足够用了。
在Polar里,你可以右键点击节点B,选择"Send Payment",然后粘贴这个发票。节点B会通过我们刚刚打开的通道把资金发过来。
发送BOLT11支付
收钱之后,当然也要能发钱。发送BOLT11支付需要对方提供一个发票。这里我们让节点B创建一个发票,然后我们的节点A去支付它。
在Polar里右键点击节点B,选择"Create Invoice",输入金额(比如25,000 msats),复制生成的BOLT11字符串。
fn send_payment(node: &ldk_node::Node) {
// 从Polar节点B获取的发票
let invoice = "lnbcrt250n1pjx..."; // 替换成节点B生成的发票
let payment_id = node.send_payment(&invoice).unwrap();
println!("Payment sent! ID: {:?}", payment_id);
// 等待支付完成
loop {
let payments = node.list_payments();
let sent = payments.iter().any(|p| {
matches!(p.direction, PaymentDirection::Outbound)
&& p.id == Some(payment_id)
&& p.status == PaymentStatus::Succeeded
});
if sent {
println!("Payment succeeded!");
break;
}
std::thread::sleep(std::time::Duration::from_secs(2));
}
}
注意send_payment返回的是一个PaymentId,你可以用它来查询支付状态。如果支付失败(比如通道资金不足、路由不可达),状态会变成Failed。
创建和支付BOLT12发票
BOLT12是闪电网络的新一代发票标准。它比BOLT11更灵活——支持重复支付、自动续费、以及更丰富的元数据。但BOLT12需要onion-message支持,所以你的节点需要连接一个支持onion-message的对等节点。
在Polar里,如果你没有添加CLN节点,这一步会失败。LND目前不支持onion message。所以确保你的Polar网络里有一个CLN节点。
创建BOLT12发票需要先创建一个"offer"。Offer就像是你的公开收款地址,别人可以用它来生成发票并支付。
fn create_and_pay_bolt12(node: &ldk_node::Node) {
// 创建一个BOLT12 offer
let amount_msat = 30_000;
let description = "BOLT12 test payment".to_string();
let offer = node.create_offer(amount_msat, description, None).unwrap();
println!("BOLT12 offer: {}", offer);
// 通过offer创建一个发票
let invoice = node.receive_payment_from_offer(&offer, None).unwrap();
println!("BOLT12 invoice: {}", invoice);
// 支付这个发票(从节点B的角度)
// 注意:这里我们需要从节点B支付,所以实际上需要在Polar节点B上操作
// 为了演示,我们假设节点B已经支付了
println!("Use this BOLT12 invoice to pay from Polar node B (CLN)");
// 等待支付到账
loop {
let payments = node.list_payments();
let received = payments.iter().any(|p| {
matches!(p.direction, PaymentDirection::Inbound)
&& p.status == PaymentStatus::Succeeded
&& p.amount_msat == Some(amount_msat)
});
if received {
println!("BOLT12 payment received!");
break;
}
std::thread::sleep(std::time::Duration::from_secs(2));
}
// 反过来,我们也可以支付一个从CLN节点创建的BOLT12发票
// 从CLN节点获取offer,然后:
// let cln_offer = "..."; // 从CLN节点获取
// let cln_invoice = node.receive_payment_from_offer(&cln_offer, None).unwrap();
// node.send_payment(&cln_invoice).unwrap();
}
create_offer的第三个参数是可选的有效期。不传的话,默认是永不过期。
发送自发支付(无需发票)
自发支付(Spontaneous Payment)也叫"keysend"支付。它不需要接收方预先创建发票——发送方直接生成一个预映像(preimage),然后通过闪电网络发送支付。接收方收到后,可以从支付中提取预映像来认领资金。
这种机制对于捐赠、打赏、或者任何不需要事先协商的场景特别有用。
fn send_spontaneous_payment(node: &ldk_node::Node) {
let node_b_id = "02f1a8c87607a4d8b8d2c0c3f8c0d3e4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e";
let amount_msat = 20_000;
let payment_id = node.send_spontaneous_payment(amount_msat, node_b_id).unwrap();
println!("Spontaneous payment sent! ID: {:?}", payment_id);
// 等待支付完成
loop {
let payments = node.list_payments();
let sent = payments.iter().any(|p| {
matches!(p.direction, PaymentDirection::Outbound)
&& p.id == Some(payment_id)
&& p.status == PaymentStatus::Succeeded
});
if sent {
println!("Spontaneous payment succeeded!");
break;
}
std::thread::sleep(std::time::Duration::from_secs(2));
}
}
注意:自发支付需要接收方(节点B)支持"keysend"特性。LND和CLN都支持,但默认可能没开启。在Polar里,你可以在节点设置里启用它。
完整的main函数
现在把所有函数组合起来。main函数会按顺序调用它们,创建节点的完整生命周期:
#[tokio::main]
async fn main() {
println!("=== Building Lightning Node ===");
let node = build_node();
println!("\n=== Starting Node and Connecting to Peer ===");
start_and_connect(&node);
println!("\n=== Initial State ===");
print_channels(&node);
print_payments(&node);
println!("\n=== Opening Channel ===");
open_channel(&node);
println!("\n=== Waiting for Channel Ready ===");
wait_for_channel_ready(&node);
println!("\n=== State After Channel Open ===");
print_channels(&node);
print_payments(&node);
println!("\n=== Receiving BOLT11 Payment ===");
receive_payment(&node);
println!("\n=== State After Receiving Payment ===");
print_channels(&node);
print_payments(&node);
println!("\n=== Sending BOLT11 Payment ===");
send_payment(&node);
println!("\n=== State After Sending Payment ===");
print_channels(&node);
print_payments(&node);
println!("\n=== BOLT12 Offer and Payment ===");
create_and_pay_bolt12(&node);
println!("\n=== State After BOLT12 Payment ===");
print_channels(&node);
print_payments(&node);
println!("\n=== Sending Spontaneous Payment ===");
send_spontaneous_payment(&node);
println!("\n=== Final State ===");
print_channels(&node);
print_payments(&node);
println!("\n=== Done! ===");
// 保持节点运行,直到用户中断
loop {
tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
}
}
运行它
确保Polar在运行,并且节点B已经启动。然后:
cargo run
你会看到类似这样的输出:
=== Building Lightning Node ===
=== Starting Node and Connecting to Peer ===
Node started successfully
Connected to node B: 02f1a8c87607a4d8b8d2c0c3f8c0d3e4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e
=== Initial State ===
[channels] none
[payments] none
=== Opening Channel ===
Channel opened: 3a2b1c...
=== Waiting for Channel Ready ===
Channel is ready!
=== State After Channel Open ===
[channel] id: 3a2b1c... | peer: 02f1a8c8... | capacity: 100000 sats | ready: true
[payments] none
=== Receiving BOLT11 Payment ===
BOLT11 invoice: lnbcrt500n1pjx...
Use this invoice to pay from Polar node B
Payment received!
=== State After Receiving Payment ===
[channel] id: 3a2b1c... | peer: 02f1a8c8... | capacity: 100000 sats | ready: true
[payment] INBOUND | Bolt11 | succeeded | 50000 msats | id: Some(abc123...)
踩坑指南
我在写这个教程的时候踩了不少坑,分享几个给你:
Polar里的节点ID格式:Polar显示的节点ID有时候带
@符号,比如02abc...@127.0.0.1:9735。你只需要@前面的部分,也就是节点ID本身。通道打开失败:最常见的原因是资金不足。确保你的节点A在Polar的Bitcoin Core钱包里有足够的余额。Polar默认会给每个节点一些测试币,但如果用完了,你可以在Polar里手动挖矿。
BOLT12创建失败:如果
create_offer报错,99%是因为没有连接支持onion-message的对等节点。在Polar里添加一个CLN节点就能解决。支付路由失败:如果支付一直pending然后失败,检查通道是否真的就绪了(
is_channel_ready)。有时候需要多等一会儿,或者Polar的挖矿间隔还没到。端口冲突:如果你同时运行多个节点,确保端口不冲突。Polar管理端口分配,但如果你手动运行多个ldk-node实例,需要指定不同的监听端口。
更进一步
这个教程只是起点。如果你想继续深入,这里有几个方向:
- 持久化配置:把节点ID、RPC凭据、对等节点信息放到配置文件里,而不是硬编码。
- 事件回调:用
node.register_event_handler()来监听通道状态变化、支付完成等事件,而不是轮询。 - 多通道管理:打开多个通道,形成网络拓扑,体验多跳路由。
- 通道再平衡:当通道一侧资金耗尽时,通过循环支付或其他策略重新平衡。
- 生产部署:使用BIP39助记词、Esplora作为链数据源、Rapid Gossip Sync加速启动。
最后的话
说实话,我第一次接触LDK的时候,被它庞大的API吓到了。但ldk-node的设计哲学很对:把90%的用例封装好,剩下的10%留给你自己折腾。这篇文章里的代码,大概覆盖了你在一个真实应用里80%的需求——连接、通道、收付款。剩下的20%,等你真正需要的时候再去挖LDK的底层API也不迟。
代码都在GitHub上,有问题直接提issue。祝你好运,闪电网络见!