点时间锁定合约(PTLC)的原理与实现
问题背景
在 HTLC Forwarding 中,全程使用同一个 $s$
,这样一旦控制链路上的多个节点,就可以知道一个交易中经过的许多节点,甚至找到支付的起点和终点。所有有人就想,能不能有办法让每一个点对点交易都使用不同的秘密值。解决方法其一是 PTLC。
HTLC 路径支付回顾
众所周知,HTLC 可以用于多跳的路径支付。这种方式称为 HTLC Forwarding。
假设要在路径 Alice ↔ Bob ↔ Carol 上完成 Alice 到 Carol 的支付。
过程:
-
Carol 随机创建原像 s,并且计算其哈希 H。将 H 其作为发票交付给 Alice。现在只有 Carol 知道 s,而 Alice 和 Carol 都知道 H.
-
Alice 通过算法探测出可行路径 Alice ↔ Bob ↔ Carol
-
Alice 在 Alice 和 Bob 的通道上添加 HTLC 并存入币,此合约规定:如果 Bob 提供 s,则可以得到锁定的币。否则,Alice 将在超时后取回币(比如约定 t=2 时超时)。现在只有 Carol 知道 s,而三个人都知道 H.
-
Bob 于是在 Bob 和 Carol 的通道上添加 HTLC 并存入币,规定:如果 Carol 提供 s,则可以得到锁定的币。否则,Bob 取回币(约定 t=1 超时)
-
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。
-
首先 Bob 随机生成自己的秘密值 s 作为私钥,并且乘以基点得到
$sG$
即公钥。将公钥开到发票里发给 Alice。 -
Alice 创建一个 PTLC,Bob 必须提供秘密值
$s$
解锁得到资金,否则超时后钱退还给 Alice。
这很好理解。
一个中间人的情况
假设要在路径 Alice ↔ Bob ↔ Carol 上完成 Alice 到 Carol 的支付(时间条件略,同 HTLC)。
-
同样的,Carol 生成
$s$
并公开$sG$
给 Alice。 -
Alice 通过算法探测出可行路径 Alice ↔ Bob ↔ Carol。创建三个秘密值
$a,b,c$
,并且,将$b$
发送给 Bob,将$c,a+b+c$
发送给 Carol。-
注意由于这是洋葱消息,Bob 将知道
$b, c, a+b+c$
三个值。进而可以推算出$a$
的值。因此 Bob 实际上知道除了$s$
外的所有值。 -
而 Carol 只知道
$a+b, c, s$
的值。(极端情况和 Bob 串通,则知道所有值)
-
-
Alice 给 Bob 创建 PTLC。Bob 的解锁条件为
$a+s$
。不知道$s$
因此无法解锁。 -
Bob 给 Carol 创建 PTLC。Carol 的解锁条件为
$a+b+s$
。不知道$a$
和$s$
因此无法解锁。(极端情况下 B、C 串通,则因为不知道$s$
还是无法解锁。) -
Carol 要解锁资金,它公开
$a+b+s$
的值。 -
Bob 用减法得到
$a+s$
的值,公开后,解锁了资金。 -
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 的协议示范。