点时间锁定合约(PTLC)的原理与实现

问题背景

在 HTLC Forwarding 中,全程使用同一个 $s$,这样一旦控制链路上的多个节点,就可以知道一个交易中经过的许多节点,甚至找到支付的起点和终点。所有有人就想,能不能有办法让每一个点对点交易都使用不同的秘密值。解决方法其一是 PTLC。

HTLC 路径支付回顾

众所周知,HTLC 可以用于多跳的路径支付。这种方式称为 HTLC Forwarding。

假设要在路径 Alice ↔ Bob ↔ Carol 上完成 Alice 到 Carol 的支付。

过程:

  1. Carol 随机创建原像 s,并且计算其哈希 H。将 H 其作为发票交付给 Alice。现在只有 Carol 知道 s,而 Alice 和 Carol 都知道 H.

  2. Alice 通过算法探测出可行路径 Alice ↔ Bob ↔ Carol

  3. Alice 在 Alice 和 Bob 的通道上添加 HTLC 并存入币,此合约规定:如果 Bob 提供 s,则可以得到锁定的币。否则,Alice 将在超时后取回币(比如约定 t=2 时超时)。现在只有 Carol 知道 s,而三个人都知道 H.

  4. Bob 于是在 Bob 和 Carol 的通道上添加 HTLC 并存入币,规定:如果 Carol 提供 s,则可以得到锁定的币。否则,Bob 取回币(约定 t=1 超时)

  5. Carol 当然知道 s,因此 Carol 提供 s,换到了锁定的币。Bob 利用 s 解锁其与 Alice 的合约上的币。

于是,币从 Alice 转给了 Bob,并从 Bob 转给了 Carol. 这个过程中不存在卷款跑路但不提供 s 的情况,因为必须向区块链揭示 s 才能提款,这样保证了 s 可以被公布。

椭圆曲线回顾

简单回顾一下非对称加密的原理。我们可以生成复杂的秘密值 $s$,乘以特殊生成点 $G$ 可以得到公钥 $sG$。只知道 $G$$sG$ 是难以计算出 $s$ 的(因为这涉及到在椭圆曲线上进行离散对数运算,此处不展开)。

另外椭圆上的点的数乘与加法运算支持分配律,即 $(x+y)G = xG + yG$

PTLC 支付原理

最简单的情况

先考虑点对点的情况,Alice ↔ Bob 的路径上 Alice 转给 Bob。

  1. 首先 Bob 随机生成自己的秘密值 s 作为私钥,并且乘以基点得到 $sG$ 即公钥。将公钥开到发票里发给 Alice。

  2. Alice 创建一个 PTLC,Bob 必须提供秘密值 $s$ 解锁得到资金,否则超时后钱退还给 Alice。

这很好理解。

一个中间人的情况

假设要在路径 Alice ↔ Bob ↔ Carol 上完成 Alice 到 Carol 的支付(时间条件略,同 HTLC)。

  1. 同样的,Carol 生成 $s$ 并公开 $sG$ 给 Alice。

  2. Alice 通过算法探测出可行路径 Alice ↔ Bob ↔ Carol。创建三个秘密值 $a,b,c$,并且,将 $b$ 发送给 Bob,将 $c,a+b+c$ 发送给 Carol。

    1. 注意由于这是洋葱消息,Bob 将知道 $b, c, a+b+c$ 三个值。进而可以推算出 $a$ 的值。因此 Bob 实际上知道除了 $s$ 外的所有值。

    2. 而 Carol 只知道 $a+b, c, s$ 的值。(极端情况和 Bob 串通,则知道所有值)

  3. Alice 给 Bob 创建 PTLC。Bob 的解锁条件为 $a+s$。不知道 $s$ 因此无法解锁。

  4. Bob 给 Carol 创建 PTLC。Carol 的解锁条件为 $a+b+s$。不知道 $a$$s$ 因此无法解锁。(极端情况下 B、C 串通,则因为不知道 $s$ 还是无法解锁。)

  5. Carol 要解锁资金,它公开 $a+b+s$ 的值。

  6. Bob 用减法得到 $a+s$ 的值,公开后,解锁了资金。

  7. Alice 用剑法得到 $s$ 的值,公开后,证明自己已经给 Carol 完成付款。

自此完成了交易。即便 Bob 和 Carol 串通,也不影响协议的安全性。

PTLC 支付实现

基础库的使用示例

我们用到以下依赖

1[dependencies]
2k256 = "0.11.6"
3rand = "0.8.5"

基本运算示例如下:

 1use k256::{
 2    ecdsa::{SigningKey, VerifyingKey},
 3    elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint},
 4    ProjectivePoint,
 5};
 6use rand::{thread_rng, Rng};
 7
 8fn main() {
 9    let mut rng = thread_rng();
10    let s1 = rng.gen::<[u8; 32]>();
11    let s2 = rng.gen::<[u8; 32]>();
12
13    let s1_g = generate_public_key(&s1);
14    let s2_g = generate_public_key(&s2);
15
16    let combined_key = add_public_keys(&s1_g, &s2_g);
17
18    println!("s1: {:x?}", s1);
19    println!("s2: {:x?}", s2);
20    println!("s1_g: {:?}", s1_g);
21    println!("s2_g: {:?}", s2_g);
22    println!("(s1 + s2)G: {:?}", combined_key);
23}
24
25fn generate_public_key(nonce: &[u8; 32]) -> ProjectivePoint {
26    let signing_key = SigningKey::from_bytes(nonce).expect("Invalid nonce");
27    let verifying_key = VerifyingKey::from(&signing_key);
28    let point = verifying_key.to_encoded_point(true);
29
30    ProjectivePoint::from_encoded_point(&point).unwrap()
31}
32
33fn add_public_keys(point1: &ProjectivePoint, point2: &ProjectivePoint) -> ProjectivePoint {
34    *point1 + *point2
35}

使用 Rust 实现 PTLC

下面我们编写一个程序模拟支付链路上的 PTLC 的执行。

 1use k256::{
 2    ecdsa::{SigningKey, VerifyingKey},
 3    elliptic_curve::{
 4        bigint::{ArrayEncoding, UInt},
 5        ops::Reduce,
 6        sec1::{FromEncodedPoint, ToEncodedPoint},
 7    },
 8    ProjectivePoint, Scalar, U256,
 9};
10use rand::{thread_rng, Rng};
11
12// 生成公钥
13fn pt_from_sk(signing_key: &SigningKey) -> ProjectivePoint {
14    let verifying_key = VerifyingKey::from(signing_key);
15    let point = verifying_key.to_encoded_point(true);
16    ProjectivePoint::from_encoded_point(&point).unwrap()
17}
18
19fn pk_from_sk(sk: &Scalar) -> ProjectivePoint {
20    let bytes = sk.to_bytes();
21    let sign_key = SigningKey::from_bytes(&bytes.as_slice()).unwrap();
22    pt_from_sk(&sign_key)
23}
24
25// 支付通道
26struct PTLC {
27    unlock_condition: Scalar,
28}
29
30fn sk_to_scalar(signing_key: &SigningKey) -> Scalar {
31    let bytes = signing_key.to_bytes();
32    <Scalar as Reduce<U256>>::from_uint_reduced(UInt::from_be_byte_array(bytes))
33}
34
35fn create_sk_scalar() -> Scalar {
36    let mut rng = thread_rng();
37    let sk = SigningKey::random(&mut rng);
38    sk_to_scalar(&sk)
39}
40
41fn create_ec_keypair() -> (Scalar, ProjectivePoint) {
42    let mut rng = thread_rng();
43    let sk = SigningKey::random(&mut rng);
44    let secret_key = sk_to_scalar(&sk);
45    let public_key = pt_from_sk(&sk);
46    (secret_key, public_key)
47}
48
49struct Participant {}
50
51impl Participant {
52    fn new() -> Self {
53        Participant {}
54    }
55
56    fn create_ptlc(&self, unlock_condition: Scalar) -> PTLC {
57        PTLC { unlock_condition }
58    }
59
60    fn unlock_funds(&self, ptlc: &PTLC, secret_value: Scalar) -> bool {
61        ptlc.unlock_condition == secret_value
62    }
63}
64
65fn main() {
66    // 创建 Alice、Bob 和 Carol
67    let alice = Participant::new();
68    let bob = Participant::new();
69    let carol = Participant::new();
70
71    // Carol 选择秘密值,并公开 sG 给 Alice
72    let (s, s_g) = create_ec_keypair();
73
74    // Alice 创建支付通道给 Bob,条件是 a + s
75    let a = create_sk_scalar();
76    let b = create_sk_scalar();
77    let c = create_sk_scalar();
78
79    let ptlc_to_bob = alice.create_ptlc(a + s);
80    // Bob 创建支付通道给 Carol
81    let ptlc_to_carol = bob.create_ptlc(a + b + s);
82
83    // Carol 解锁资金
84    let secret_value = a + b + s;
85    assert!(carol.unlock_funds(&ptlc_to_carol, secret_value));
86    println!("Carol successfully unlocked the funds.");
87
88    // Bob 解锁资金
89    let secret_value = secret_value - b;
90    assert!(bob.unlock_funds(&ptlc_to_bob, secret_value));
91    println!("Bob successfully unlocked the funds.");
92
93    // Alice 证明自己已经给 Carol 完成付款。
94    let secret_value = secret_value - a;
95    assert!(pk_from_sk(&secret_value) == s_g);
96    println!("Alice successfully proved she paid Carol.");
97}

(仅供学习参考)

在比特币实现 PTLC

有了上面的实现参考,我们可以考虑怎么用 Bitcoin Script 实现这个逻辑。

  • 锁定时间依然用 OP_CHECKLOCKTIMEVERIFY 来完成。

  • 在比特币引入了 Schnorr 签名后,实现聚合密钥的解锁也成为可能。通过 OP_CHECKSIGVERIFY 或者 OP_CHECKSIGADD 可以验证签名。实际上 PTLC 的签名和普通的单密码值生成的签名并没有什么差别,用相同的代码就可以涵盖。

从 HTLC 的脚本可以改写得出 PTLC 的脚本:

    OP_IF
        <sig> <pk> OP_CHECKSIGVERIFY OP_DUP OP_HASH160 <seller pubkey hash>            
    OP_ELSE
        <num> [TIMEOUTOP] OP_DROP OP_DUP OP_HASH160 <buyer pubkey hash>
    OP_ENDIF
    OP_EQUALVERIFY
    OP_CHECKSIG

在 LN 实现 PTLC

开源节点 Eclair 提供了基于 PTLC 的协议示范。

参考

  1. 什么是 “点时间锁合约(PTLC)”?

  2. 13. Payment Channels and Lightning Network - YouTube

  3. On Bitcoin’s Schnorr signature algorithm and Taproot script and witness sizes | by Dr. Johannes Hofmann | Coinmonks | Medium