最近开发了一个项目 thor,它是一个 lightning address server,基于 Rust + NWC 开发。
thor 有两个主要优点。一是 lightning address 可以直接绑定到自己的域名,而不是依赖于第三方服务。二是基于 NWC,无需自行运行闪电网络节点,即可实现自托管的 lightning address。
我的 lightning address yfaming@yfaming.com
,就是通过 thor 实现的。
什么是 lightning address?
Lightning address 是闪电网络衍生的一个广受欢迎的功能,它看起来就像一个邮箱地址,比如 yfaming@yfaming.com
。
大部分钱包都支持 lightning address,在钱包里输入收款方 lightning address,就可以向他支付比特币了。Nostr 协议支持用户设置 lightning address,zap(打赏)时就是通过它实现的。
市面上有许多 lightning address 服务提供商,比如 getalby, coinos 等等。但和 yfaming@getalby.com
和 yfaming@coinos.io
这样的地址相比,使用自己域名的 yfaming@yfaming.com
显然更专业,也更有个性。
lightning address 由 LNURL 里的两个规范定义:
简单地说,当我们向 yfaming@yfaming.com
付款时,钱包会发送两个 HTTP 请求。第一个请求是 GET https://yfaming.com/.well-known/lnurlp/yfaming
,它的响应如下:
{
"callback": "https://yfaming.com/lnurlp/yfaming",
"maxSendable": 100000000000,
"minSendable": 1000,
"metadata": "[[\"text/identifier\",\"yfaming@yfaming.com\"],[\"text/plain\",\"sats for yfaming@yfaming.com\"],[\"text/plain\",\"powered by https://github.com/yfaming/thor\"]]",
"tag": "payRequest"
}
然后我们输入付款金额,钱包向 callback
url 发送第二个请求,GET https://yfaming.com/lnurlp/yfaming?amount=21
,获取 invoice。
{
"pr": "lnbc210p...", // invoice
"routes": []
}
后续流程就和支付普通的 invoice 一样。
总结一下,lightning address server 需要提供两个 HTTP 接口,它的核心功能是根据用户提供的 amount
生成 invoice。
如何生成 invoice 呢?我们需要运行自己的闪电网络节点,比如 LND, Core Lightning,然后调用它们的接口创建 invoice。但有了 NWC,我们可以调用它的 make_invoice
接口生成 invoice,无需自己运行节点。
什么是 NWC?
NWC 是闪电网络与 Nostr 协议结合而形成的协议。
Nostr (Notes and Other Stuff Transmitted By Relays) 是一个去中心化的社交网络协议。它不依赖中心化平台,用户将消息发布到 relay(中继),由客户端从这些 relay 获取内容。用户通过公钥标识身份,通过私钥对消息进行签名。任何人都可以运行 relay 或开发客户端,因此 nostr 是一个开放的、去中心化的生态。
Nostr 通过 NIPS规定协议的细节。在 Nostr 中用户的行为被抽象为 event,其字段如下:
{
"id": <32-bytes lowercase hex-encoded sha256 of the serialized event data>,
"pubkey": <32-bytes lowercase hex-encoded public key of the event creator>,
"created_at": <unix timestamp in seconds>,
"kind": <integer between 0 and 65535>,
"tags": [
[<arbitrary string>...],
// ...
],
"content": <arbitrary string>,
"sig": <64-bytes lowercase hex of the signature of the sha256 hash of the serialized event data, which is the same as the "id" field>
}
event 以 id
为标识,包括发布者的公钥 pubkey
,签名 sig
,以及内容 content
等字段。event 内容的语义及结构,由 kind
决定。
Nostr 客户端与 relay 之间通过 websocket 进行通信。客户端可以发布 event,并订阅指定过滤条件的 event 等等。
event 的定义是非常通用的,可以适应不同的功能。通过定义不同的 kind
及相应 content
的结构与语义,Nostr 实现了社交网络有关的大部分功能,比如 profile,关注,发帖,评论,点赞,私信,打赏等等。基于 Nostr 的应用,包括 Damus, Primal, Coracle 等等。值得一提的是,Nostr 用户可以在 profile 中设置 lightning address,并通过它进行打赏。
NWC (Nostr Wallet Connect) 则将闪电网络钱包的基础功能通过 Nostr 协议进行了标准化。它定义了 get_balance
, make_invoice
, pay_invoice
, lookup_invoice
, list_transactions
等接口。NWC 旨在解决市场上闪电网络钱包接口不统一、应用接入成本过高的问题。在 NWC 之前,应用如果要支持闪电网络,需要每个钱包维护一套代码,繁琐且重复。有了 NWC 之后,钱包服务实现 NWC 接口,而应用只需按照 NWC 协议接入即可。它大幅降低了应用接入闪电网络的成本,从而推动了闪电网络的普及。
NWC 协议由 NIP-47 规定。
thor
到这里,思路就清晰了:
lightning address server 需要提供两个 HTTP 接口,并能够创建 invoice。NWC 恰好解决了创建 invoice 的问题。把两者结合起来,就有了 thor。
thor 使用 axum 作为 Web 框架,并使用 rust-nostr/nostr 提供的 nwc crate 与 NWC 协议交互。实现时我主要参考了 LUD-16、LUD-06 和 NIP-47 规范,确保符合标准。
如何部署 thor
前几天在 stacker.news 和 X 上发了关于 thor 的帖子之后,有人问如何部署 thor,最低硬件规格等问题。
作为一个 Rust 项目,thor 的资源需求非常低。运行时内存占用不超 10MB,CPU 占用更是可忽略不计。
我在 DigitalOcean 上选择了最便宜的 VPS 部署,512 MB memory, 1 CPU, 10 GB SSD,费用 $4/月。另外添加了一块 30 GB 的磁盘,额外 $3/月。总费用 $7/月。如果不添加额外的磁盘,月费用就只要 $4。
如果只部署,不需要在 VPS 上编译,直接上传编译好的二进制文件即可,省心且避免小内存机器的麻烦,这样的规格绰绰有余。
部署时,建议使用 systemd service 管理 thor 进程,并且强烈建议使用 nginx 作为反向代理。互联网环境是非常险恶的,使用久经考验的 nginx 作为前置防护,才能最大程度保证安全。systemd 和 nginx 的资料非常多,大家按照文档和自己的需求进行配置即可。
下图是 nginx 访问日志的一部分,可以看到,随时都有恶意访问,例如扫描和漏洞探测请求。
低配 VPS 构建 Rust 项目的经验
但如果要在 VPS 上编译 Rust 项目,就需要做一些调整,因为 Rust 编译时很吃内存和临时存储。
DigitalOcean VPS 默认没有 swap,512MB 的内存不足以编译 thor,rustc 会因 OOM 而退出。我添加了一个 2GB 的 swap 文件:
fallocate -l 2G /data/swap
# 设置权限(必须 600,否则 swapon 拒绝)
chmod 600 /data/swap
mkswap /data/swap
swapon /data/swap
# /etc/fstab 里也要添加一行,重启后也能继续生效
# /data/swap none swap sw 0 0
vim /etc/fstab
设置了 swap 之后再编译,正常完成。
另外,VPS 的 /tmp
挂载的是一个 tmpfs
,只有 229MB,也太小了。在 cargo install fd-find
时,编译 jemalloc-sys
crate 报错 No space left on device
,就是因为 /tmp
空间不足。
df -lh
与 /tmp
有关的结果:
Filesystem Size Used Avail Use% Mounted on
tmpfs 229M 187M 42M 82% /tmp
tmpfs
是一个基于内存的文件系统,实际数据存储在 RAM 或者 swap 中。/tmp
是由 Systemd 管理的,unit 是 tmp.mount
,配置文件是 /usr/lib/systemd/system/tmp.mount
。
打开后发现里面有个配置项 Options
,指定了 size=50%%
,意思是指定 /tmp
的大小是内存的 50%。我将 size
改为 size=1G
,然后重启,再次安装 fd-find
时一切正常。
总之,在低配 VPS 上编译 Rust 项目,需要开启 swap(至少 2GB),调整 /tmp
大小(建议 1GB 以上)。也可在别的机器上编译,VPS 仅用作部署。
如果你也想在自己的域名上拥有 lightning address,可以试试 thor 。