声明: 本文纯做技术分析, 不代表任何加密货币投资购买建议
多方安全计算 mpc 背景:
多方安全技术 越来越多的被应用在加密货币钱包/交易所 。 其主要应用在3个方面:
- 多方共同生成一个钱包的私钥, 并对这个私钥进行分片。使得不同分片支持门限恢复操作。如将一个完整的密钥分片分成5份,需要两份即可恢复出完整密钥。 5 记做分片数量, 2记做门限数量。
- 多方协同签名, 使用多方的密钥分片对交易进行签名。具体来说, 每一个密钥分片对交易进行签名 并 拼凑起来的 签名 等价于 完整私钥对交易进行签名。
- 多方协同密钥重置, 密钥参与方经过协商, 将原本的私钥分片打散然后重新分配, 最终的完整私钥和公钥不变, 但是每个参与方持有的私钥分片发生改变, 整个协商过程中完整私钥都不会出现。
这里的多方(一般被称作party)是比较广义的, 可能有多重意义, 比如一个用户登录一台设备, 这台设备可以被称作一个 party。 比如加密币平台的服务器为客户端party生成一个临时的 进程, 这个进程与客户端的party一起协商出一把私钥, 这个进程也可以称作一个 party。 在密码学上面, 只要参与多方安全计算的实体都统称为 party。 生成的密钥分片也会被保存到多个地方, 通常是两到三个: 客户端, 服务端, 用户方(用户自己记在脑袋或者小本本炒好都可以)
通过上面的两个应用可以实现下面两个非常吸引人的性质:
- 分布式完成密钥生成(密钥重置), 永远不会出现完整的 密钥, 无论是内存, 硬盘, 还是cpu, 服务器的任何硬件永远都不会出现完整的密钥
- 签名过程, 无论是内存, 硬盘, 还是cpu, 服务器的任何硬件永远都不会出现使用完整的密钥对交易进行签名 但实现效果和使用一把完整密钥进行签名的效果一致
这两个性质就非常诱人, 即使钱包/交易所里面有内鬼, 内鬼也无法知道钱包的完整私钥, 因为私钥压根从来就没有展示出来过。配合上零日志记录+端到端加密, 可以实现神仙来了都无法撼动的资产安全效果。
这篇博客主要讲解 币安 tss lib(一种GG18 协议的实现)中的分布式密钥生成keygen部分。
需要你拥有 ECC椭圆曲线加密/签名, shamir密钥分发, Fedlman VSS密钥验证, Pailliar同态加密算法的前置知识,如果没有可以移步我的其他文章:
- 加密货币安全基石:直观理解ECC椭圆曲线加密算法
- 加密货币安全基石:从ECC到ECDSA椭圆曲线签名
- 加密货币安全基石: 从拉格朗日插值到shamir共享密钥算法
- 加密货币安全基石: 从Shamir到Feldman VSS算法, 以币安tss-lib库为例
- 加密货币安全基石: 详解 Paillier 同态加密并使用币安tss-lib库
Pedersen's DKG
1991 年 Pedersen 提出的 Threshold Cryptosystem without a Trusted Party 是GG18协议的核心,其讲解了如何去中心化,分布式地生成密钥分片。GG18 协议 在此基础上增加了零知识证明, Feldman VSS , 非小质数校验等冗余安全性完善,这些有些是属于密钥生成部分,有些是用于签名部分。 可以说,理解了 Pedersen's DKG 就大致理解了 GG18协议的 keygen部分
假设有4个用户Alice, Bob, Chalize, David 想要协商出一把共享密钥, 每个用户保存一个共享密钥的分片, 3个分片可以恢复出完整密钥。可以通过下几个步骤实现:
Alice 选定自己的私钥10, Bob选定自己的私钥20, Chalize选定自己的私钥30, David选定自己的私钥40
共享密钥 = 10+20+30+40 = 100. 问题的关键就是如何做各方不泄漏自己的私钥情况下将自己的私钥分(10,20,30,40)享出去,从而可以恢复出最终的共享密钥 100.
可以这么做, Alice 选定一条二次曲线 <math xmlns="http://www.w3.org/1998/Math/MathML"> f ( x ) = x 2 + x + 10 f(x)= x^2+x+10 </math>f(x)=x2+x+10 计算:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> f ( 1 ) = 12 ; A l i c e 自己保留 f ( 2 ) = 16 ; 分享给 B o b f ( 3 ) = 22 ; 分享给 C h a r l i z e f ( 4 ) = 30 ; 分享给 D a v i d f(1)= 12; Alice 自己保留\\ f(2)= 16; 分享给Bob\\ f(3)= 22; 分享给Charlize\\ f(4)= 30; 分享给David </math>f(1)=12;Alice自己保留f(2)=16;分享给Bobf(3)=22;分享给Charlizef(4)=30;分享给David
同理, Bob 选定一条二次曲线 <math xmlns="http://www.w3.org/1998/Math/MathML"> f ( x ) = 2 x 2 + 3 x + 20 f(x)= 2x^2+3x+20 </math>f(x)=2x2+3x+20 计算:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> f ( 1 ) = 25 ; 分享给 A l i c e f ( 2 ) = 34 ; B o b 自己保留 f ( 3 ) = 47 ; 分享给 C h a r l i z e f ( 4 ) = 64 ; 分享给 D a v i d f(1)= 25; 分享给 Alice \\ f(2)= 34; Bob自己保留\\ f(3)= 47; 分享给Charlize\\ f(4)= 64; 分享给David </math>f(1)=25;分享给Alicef(2)=34;Bob自己保留f(3)=47;分享给Charlizef(4)=64;分享给David
同理, Charlize 选定一条二次曲线 <math xmlns="http://www.w3.org/1998/Math/MathML"> f ( x ) = − x 2 + x + 30 f(x)= -x^2+x+30 </math>f(x)=−x2+x+30 计算:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> f ( 1 ) = 30 ; 分享给 A l i c e f ( 2 ) = 28 ; 分享给 B o b f ( 3 ) = 24 ; C h a r l i z e 自己保留 f ( 4 ) = 18 ; 分享给 D a v i d f(1)= 30; 分享给 Alice \\ f(2)= 28; 分享给Bob \\ f(3)= 24; Charlize自己保留 \\ f(4)= 18; 分享给David </math>f(1)=30;分享给Alicef(2)=28;分享给Bobf(3)=24;Charlize自己保留f(4)=18;分享给David
同理, David 选定一条二次曲线 <math xmlns="http://www.w3.org/1998/Math/MathML"> f ( x ) = − x 2 + x + 40 f(x)= -x^2+x+40 </math>f(x)=−x2+x+40 计算:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> f ( 1 ) = 40 ; 分享给 A l i c e f ( 2 ) = 38 ; 分享给 B o b f ( 3 ) = 34 ; 分享给 C h a r l i z e f ( 4 ) = 28 ; D a v i d 自己保留 f(1)= 40; 分享给 Alice \\ f(2)= 38; 分享给Bob \\ f(3)= 34; 分享给Charlize \\ f(4)= 28; David自己保留 </math>f(1)=40;分享给Alicef(2)=38;分享给Bobf(3)=34;分享给Charlizef(4)=28;David自己保留
随后四个人各自计算自己的密钥分片
Alice private key = 12 + 25 +30 +40 = 107
Bob private key = 16 + 34+28+38 = 116
Charlize private key = 22+47+24+34 = 127
David share key = 30 + 64 + 18 +28 = 140
至此密钥分片的计算已经完成, 最终的私钥通过 (1,107), (2,116), (3,127), (4,140) 使用拉格朗日插值计算得到二次曲线 f(x) = x^2 + 6x 100, x= 0 时 100就是完整的密钥。
仔细观察 f(x) = x^2 + 6x 100 刚好等于四个人选择的曲线相加,这也是密钥生成的正确性的来源 四个人通过点对点传输的方式将自己的曲线间接的暴露给四个人。 具体来说, Bob 收到了 另外三个人的曲线在 x=2 位置的值, 再将自己曲线x=2与另外三个值相加就是所有人曲线相加在 x=2 的值。同理Alice 得到了四个人曲线相加在x=1的值,charlize 得到了四个人曲线在 x=3的值, David 得到了 四个人曲线在 x =4的值。四个点中任选三个点在就可以得出4个人相加得到的曲线 进而得到完整密钥100,这就是为什么threshold=3。
上面所说的就是 Pedersen's DKG 的完整过程了。 这个过程只涉及了如何分布式, 去中心化生成密钥, 但是不涉及私钥派生出公钥, 不涉及如何进行签名。
GG18的keygen部分核心是在 Pedersen 基础上还增加了公钥派生和椭圆曲线乘法计算, 额外增加了些安全冗余校验, 将其扩展出了签名和验签的功能。
Binance tss lib 中关于分布式密钥生成(keygen)的部分
单测入口
首先来看单测部分, 来到 tss-lib/ecdsa/keygen /local_party_test.go 中的 TestE2EConcurrentAndSaveFixtures 函数, 我们从单测入手, 宏观看看如何进行分布式密钥生成。相关的注释我都写得极其详细,大家可以简要的过一遍,不懂的后面我详细解释。
go
func TestE2EConcurrentAndSaveFixtures(t *testing.T) {
setUp("info")
// tss.SetCurve(elliptic.P256())
threshold := testThreshold // 2, 5
// 导入一些提前生成的本地文件, 加速单测, 主要是安全大素数safe prime的生成
fixtures, pIDs, err := LoadKeygenTestFixtures(testParticipants)
if err != nil {
common.Logger.Info("No test fixtures were found, so the safe primes will be generated from scratch. This may take a while...")
pIDs = tss.GenerateTestPartyIDs(testParticipants)
}
// 让 party 彼此知道互相的存在
p2pCtx := tss.NewPeerContext(pIDs)
parties := make([]*LocalParty, 0, len(pIDs))
// 密钥生成参与方之间的消息信道
errCh := make(chan *tss.Error, len(pIDs)) // 接收错误的 channel
outCh := make(chan tss.Message, len(pIDs)) // 接收正常消息 tss message 的 channel
endCh := make(chan *LocalPartySaveData, len(pIDs)) // 接收完成计算的 channel
// party 更新器
updater := test.SharedPartyUpdater
startGR := runtime.NumGoroutine()
// 初始化密钥生成参与方, 也就是 local party
for i := 0; i < len(pIDs); i++ {
var P *LocalParty
params := tss.NewParameters(tss.S256(), p2pCtx, pIDs[i], len(pIDs), threshold)
// mod proof 安全冗余操作 signing 用到 keygen 本身用不到
params.SetNoProofMod()
// fac proof 安全冗余操作 signing 用到 keygen 本身用不到
params.SetNoProofFac()
if i < len(fixtures) {
P = NewLocalParty(params, outCh, endCh, fixtures[i].LocalPreParams).(*LocalParty)
} else {
P = NewLocalParty(params, outCh, endCh).(*LocalParty)
}
parties = append(parties, P)
go func(P *LocalParty) {
if err := P.Start(); err != nil {
errCh <- err
}
}(P)
}
// 标记完成计算的 local party 数量
var ended int32
keygen:
for {
fmt.Printf("ACTIVE GOROUTINES: %d\n", runtime.NumGoroutine())
select {
case err := <-errCh: // local party 上报 error
common.Logger.Errorf("Error: %s", err)
assert.FailNow(t, err.Error())
break keygen
case msg := <-outCh:
dest := msg.GetTo()
if dest == nil { // 一个 local party 向 其他 所有local party 发消息
for _, P := range parties {
if P.PartyID().Index == msg.GetFrom().Index {
continue
}
go updater(P, msg, errCh)
}
} else { // 一个 local party 向另一个 local party 发消息
if dest[0].Index == msg.GetFrom().Index {
t.Fatalf("party %d tried to send a message to itself (%d)", dest[0].Index, msg.GetFrom().Index)
return
}
go updater(parties[dest[0].Index], msg, errCh)
}
case save := <-endCh:
// 找出 最终计算结果 savedata 对应的是第几个 local party
index, err := save.OriginalIndex()
assert.NoErrorf(t, err, "should not be an error getting a party's index from save data")
// 将计算结果写入本地
tryWriteTestFixtureFile(t, index, *save)
atomic.AddInt32(&ended, 1)// 完成计算结果的 local party 数量 +1
if atomic.LoadInt32(&ended) == int32(len(pIDs)) {
t.Logf("Done. Received save data from %d participants", ended)
// u 为原始的私钥
u := new(big.Int)
// 对每一个 party 算出来的分片数据进行合法性校验
for j, Pj := range parties {
pShares := make(vss.Shares, 0)
// 收集 Pj 在 round 2 协议中收到的分片(share)
for _, P := range parties {
vssMsgs := P.temp.kgRound2Message1s
share := vssMsgs[j].Content().(*KGRound2Message1).Share
shareStruct := &vss.Share{
Threshold: threshold,
ID: P.PartyID().KeyInt(),
Share: new(big.Int).SetBytes(share),
}
pShares = append(pShares, shareStruct)
}
// 通过拉格朗日插值恢复出曲线的常数项, 也就是 Pj 的原始私钥 uj
uj, err := pShares[:threshold+1].ReConstruct(tss.S256())
assert.NoError(t, err, "vss.ReConstruct should not throw error")
// uj 应该等于 Pj.temp.ui, 其中 ui 为 round1 中的随机生成的原始私钥, 应该和 通过拉格朗日插值恢复出来的 uj 相等
assert.Equal(t, uj, Pj.temp.ui)
// uG为原始私钥*G
uG := crypto.ScalarBaseMult(tss.EC(), uj)
// Pj.temo.vs[0] 为 round1 中随机生成的原始私钥*G 应该和 uG 相等
assert.True(t, uG.Equals(Pj.temp.vs[0]), "ensure u*G[j] == V_0")
// Pj.data.Xi 为 round3 阶段算出来的私钥, 也是最终保存的私钥(savedata中的 Xi), 相当于 Alice 最终得到的分片 107
xj := Pj.data.Xi
//gXj Pj 的最终私钥*G 也就是 Pj 的公钥, 也就是savedata 中的BigXj
gXj := crypto.ScalarBaseMult(tss.EC(), xj)
BigXj := Pj.data.BigXj[j]
// savedata中的 BigXj[j] 应该和 Xi*G 相等
assert.True(t, BigXj.Equals(gXj), "ensure BigX_j == g^x_j")
// // 污染一个分片, 然后恢复出私钥 uj,再乘*G得到假的公钥 应该不等于 Pj.temp.vs[0]
{
badShares := pShares[:threshold]
badShares[len(badShares)-1].Share.Set(big.NewInt(0))
uj, err := pShares[:threshold].ReConstruct(tss.S256())
assert.NoError(t, err)
assert.NotEqual(t, parties[j].temp.ui, uj)
BigXjX, BigXjY := tss.EC().ScalarBaseMult(uj.Bytes())
assert.NotEqual(t, BigXjX, Pj.temp.vs[0].X())
assert.NotEqual(t, BigXjY, Pj.temp.vs[0].Y())
}
u = new(big.Int).Add(u, uj) // 每一个 Pj 的 私钥相加
}
// 从 最终计算结果 savedata 中取出最终的公钥
pkX, pkY := save.ECDSAPub.X(), save.ECDSAPub.Y()
pk := ecdsa.PublicKey{
Curve: tss.EC(),
X: pkX,
Y: pkY,
}
sk := ecdsa.PrivateKey{
PublicKey: pk,
D: u,
}
// 确保公钥位于 离散椭圆曲线上
assert.True(t, sk.IsOnCurve(pkX, pkY), "public key must be on curve")
// 使用私钥签名, 公钥验签名验证
assert.NotZero(t, u, "u should not be zero")
ourPkX, ourPkY := tss.EC().ScalarBaseMult(u.Bytes())
assert.Equal(t, pkX, ourPkX, "pkX should match expected pk derived from u")
assert.Equal(t, pkY, ourPkY, "pkY should match expected pk derived from u")
t.Log("Public key tests done.")
// 确认每一个 party 都计算得到同一把公钥
for _, Pj := range parties {
assert.Equal(t, pkX, Pj.data.ECDSAPub.X())
assert.Equal(t, pkY, Pj.data.ECDSAPub.Y())
}
t.Log("Public key distribution test done.")
// ecdsa 签名与验签
data := make([]byte, 32)
for i := range data {
data[i] = byte(i)
}
r, s, err := ecdsa.Sign(rand.Reader, &sk, data)
assert.NoError(t, err, "sign should not throw an error")
ok := ecdsa.Verify(&pk, data, r, s)
assert.True(t, ok, "signature should be ok")
t.Log("ECDSA signing test done.")
t.Logf("Start goroutines: %d, End goroutines: %d", startGR, runtime.NumGoroutine())
break keygen
}
}
}
}
下面详细解释 单测函数干了什么
定义密钥生成参与方与签名阈值
参与方也就是最终密钥的分片个数, 阈值就是一定要超过这个数才能完成签名。 比如单测函数中分片个数为5, 阈值为2, 也就是完整的私钥被拆分成5片, 需要至少3个人才能完成签名
配置文件导入提前计算好的安全冗余校验, 与前置数据
LoadKeygenTestFixtures 会从 tss/_ecdsa_fixtures 中导入一些提前准备好的前置数据用于加速本地单测 我们可以来看一下这些数据里面有什么以 keygen_data_0.json 为例, 由于数据量太大, 我只展示和keygen有关的部分, 其他和keygen相关性不大的部分我将会用数字123表示(如果是int数组我将会只使用一个元素123表示)
json
{
"PaillierSK": {
"N": 26862170591381186117144639121800907711621441110694985906073099493104224258631997616337459884349048315436649598594766212786190249139720542986841637789367089751895746802368064104115662988051298443105665522549043623368088781757399812306242052676963161647378421463432813771675598887217547787422261194939872523185392600641669797286300834348740665304662829760721139573070204170902129262797162145018079946053388917283347495995703735479819366865064178966988962612678607190805087224162314010583832802161588455461100682306289046720947974174001828045869589748392310605782826097558345479795972515955139600004112610785604729710757,
"LambdaN": 13431085295690593058572319560900453855810720555347492953036549746552112129315998808168729942174524157718324799297383106393095124569860271493420818894683544875947873401184032052057831494025649221552832761274521811684044390878699906153121026338481580823689210731716406885837799443608773893711130597469936261592532213858878816794879138507493230952759071143256763914863135847264553077488577664633510002801989144150002815082601970607292530318876745886925922476991203656094267047307176836180759972736598187277189369375666238571075693265319527847455818556610107935217778613614515276483294115793052848151350340343144475494998,
"PhiN": 26862170591381186117144639121800907711621441110694985906073099493104224258631997616337459884349048315436649598594766212786190249139720542986841637789367089751895746802368064104115662988051298443105665522549043623368088781757399812306242052676963161647378421463432813771675598887217547787422261194939872523185064427717757633589758277014986461905518142286513527829726271694529106154977155329267020005603978288300005630165203941214585060637753491773851844953982407312188534094614353672361519945473196374554378738751332477142151386530639055694911637113220215870435557227229030552966588231586105696302700680686288950989996,
"P": 156199992157527515679277851563515941446129352347011319825196067672572313106672920497393870514107469203466250029881858883814872913259387909227911821518374527804663005734804760515923302853633171490096548012329974306537954808287455990519023653082720419807513617030697689651815046731746603871834526414575616974279,
"Q": 171972931754636180863279482190687457698558121860600423518736408700450794713333895253666069935303159779875615800617935381419433314051299283909205837177825350811890123813155577706389553834758909416625395542626595272258632835075316360438928982089374315539755253298617237177569237637287299829577403684740161746483
},
"NTildei": 123,
"H1i": 123,
"H2i": 123,
"Alpha": 123,
"Beta": 123,
"P": 123,
"Q": 123,
"Xi": 11916527433647828918977250606530964748554479545005979893012447833077873661340,
"ShareID": 59857031556462284717113645237935722663924232558699039874171440941840562677323,
"Ks": [
59857031556462284717113645237935722663924232558699039874171440941840562677323,
59857031556462284717113645237935722663924232558699039874171440941840562677324,
59857031556462284717113645237935722663924232558699039874171440941840562677325,
59857031556462284717113645237935722663924232558699039874171440941840562677326,
59857031556462284717113645237935722663924232558699039874171440941840562677327
],
"NTildej": [
123
],
"H1j": [
123
],
"BigXj": [
{
"Curve": "secp256k1",
"Coords": [
95225479287625109140551300097635441933915975782583911515343531112654602880814,
113745830257261593369068705146261698861441809650110061237310141136031506190085
]
},
{
"Curve": "secp256k1",
"Coords": [
19909020077923456087962021369246692987785610885502332606764981730113023110067,
60076350170225224442893367050676875983156697199114782416705437692213004111433
]
},
{
"Curve": "secp256k1",
"Coords": [
15656029217860558075932288367874977299995954233140419375302609508233656030817,
88293512119423239639079954683198441748713533855873639211876694257553830935691
]
},
{
"Curve": "secp256k1",
"Coords": [
15825259379483050804368543653451724857970141958098760943464945060863314262898,
46510254063758718632499733093297318465018983961512441577134679077369278627011
]
},
{
"Curve": "secp256k1",
"Coords": [
101163968142129288084264305494084191253074413300747651525777392366080313581620,
19458713537429380315587854195885123660811710862685360770347430223563133437479
]
}
],
"PaillierPKs": [
{
"N": 26862170591381186117144639121800907711621441110694985906073099493104224258631997616337459884349048315436649598594766212786190249139720542986841637789367089751895746802368064104115662988051298443105665522549043623368088781757399812306242052676963161647378421463432813771675598887217547787422261194939872523185392600641669797286300834348740665304662829760721139573070204170902129262797162145018079946053388917283347495995703735479819366865064178966988962612678607190805087224162314010583832802161588455461100682306289046720947974174001828045869589748392310605782826097558345479795972515955139600004112610785604729710757
},
{
"N": 28569426937909813160816852590974326182398707183206563780157489308279811863376093908221211903705518704565348072663191903836343635499091979154072341420741676813730020871016039693403607409462919125031372066954550208350129974140220983698064393340951930706962427015297577648437601064168848334164842111410896962654571826800302294766234904003147622246551178854009373086133349568572584906962173774282191211244583738166117722131851467394725949126097483624199330170392292115956857647929895014719727669500452359666570376448590229755339126098108084513655351630004806845329610086536348250655270492083872210115099541350980087869489
},
{
"N": 24206147216197161168800749713794253097360175090858672931928135053300720098263302199858364218289609440982336278990382306871237304598903324389321581163067390799950591531027240968685694116269131503639449889176152844762069948482523881916749982047987022468266212702666839762407435492828573898843940379718086699114362935636941751781265771147161683942488081675636897258681038605775448214108367751993197065197897191643383564344845162403884453232776839031251175853763144050201714908798915379664014184087913029794762586324582687266708240565299184055542301695610690632283322864399949456272972805575542427101734659832898527078677
},
{
"N": 27422133357851370316963785322815189604726575748114057717984837411771756070272482926958898758576215271907291562151935508777240048370919087691109363558754627052939183040039501310348824807217194423462067796268979252972390229592512803802105741520833681021737552492269574490364955499455488503619050939812934483556240372784852668293634144857453177818024665828049715609921864852313661181061967825839048394234894185931968992541576874445544364635775263264674967563604397356712492758200667296917972566268326712277912968541425534456091226445588857731271210711997226828598037017820056231841183710665446107873358077925757871906777
},
{
"N": 21505960474634451313164479453847246698949068816168543450757887402781638444470085463014709362627652554915905319404707097558936051290374460876928738652082570278593089424429424860613076608894979923762290356343173648507348492292368062802168911752824853129719568062188174453668131066706292448200533705323966142811976260936406546600112652090553738417255733994944221554428167638466246670287061019896463881779810197390238307556892485807795138448959345532929528137209046373349550262355661974463926686395148775662060236988349400478971416621513539908477667503550115870803074998306032371456267566517610267867391193312424397935929
}
],
"ECDSAPub": {
"Curve": "secp256k1",
"Coords": [
76266489189895419469020567248501927603989841769205411177925179985114092514949,
17959638069442050620236663888410692330316152082152911789514411031446499229348
]
}
}
我们只看和keygen有关的部分, 其他部分虽然是keygen过程中生成的, 但是用于signing签名部分, 一般是一些安全冗余的安全质数safe prime生成, 详细的我会在后面新开的文章中讲解, 这里不赘述了
- Xi, 一个参与方最终生成的密钥, 需要参与方妥善报错。
- ShareId, 私钥对应离散椭圆曲线上的横坐标, 这个横坐标对于其他参与方是公开的
- Ks, 所有参与方的私钥对应椭圆曲线上的横坐标, 对于所有参与方来说是公开. 注意这里需要维持所有参与方 Ks 内元素的顺序一致性, 因为参与方之间通信需要记住彼此的索引, 这个索引就是 ShareId 在 Ks 中的索引
- PaillierSK, 当前参与方的Paillier同态加密用到的私钥, 在keyegn的round3阶段(后面会讲什么是round3阶段)参与方广播自己公钥的时候会使用这把私钥进行签名, 其他参与方在round4阶段会对这个公钥进行验证, 相当于将 同态加密的公私钥对与 分片的公钥相绑定
- PaillierPKs, 当前参与方的Paillier同态加密用到的公钥, 用处在第4点讲过了, 这里不再赘述
- ECDSAPub, 最终完整的公钥, 也就是所有参与方一起协同生成的公钥, 用于派生出加密货币的收款地址, 对所有参与方可见且应该相同。 Cureve 字段表示使用哪一条离散椭圆曲线, Coords 对应公钥所在离散椭圆曲线上的横纵坐标
由于 Parillier 同态加密的安全性依赖于大整数的因数分解, 在生成安全素数safe prime的过程需要耗费笔记长的时间, 所以这里提前导入了准备好的数据
LoadKeygenTestFixtures 最终导出两个数据类型: LocalPartySaveData 和 tss.SortedPartyIDs, savedata 很好理解就是json 文件读取出来的数据 tss.SortedPartyIDs 就是创建一个抽象的 参与方 party, 并且将他们按照 Ks中的顺序进行排序, 排好序确定后索引以后通信才不会乱。 这个抽象的参与方Party就是PartyID 里面包含了 索引index 和 MessageWrapper_PartyID.
go
PartyID struct {
*MessageWrapper_PartyID
Index int `json:"index"`
}
其中MessageWrapper_PartyID包含了3个字段, Id 和 Moniker 都没有实际意义只是一个名称而已, Key 就是所有参与方私钥对应的离散椭圆曲线的点横坐标数组
pb.go
type MessageWrapper_PartyID struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Moniker string `protobuf:"bytes,2,opt,name=moniker,proto3" json:"moniker,omitempty"`
Key []byte `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"`
}
参与方彼此熟悉
下面这行代码就是一个中心化的熟悉彼此的context, 在后面创建localparty的时候会用到
go
p2pCtx := tss.NewPeerContext(pIDs)
创建密钥生成参与方 LocalParty 并启动
下面这几行代码就是创建tsslib 参与方的实体 LocalParty 并启动运行
go
parties := make([]*LocalParty, 0, len(pIDs))
errCh := make(chan *tss.Error, len(pIDs))
outCh := make(chan tss.Message, len(pIDs))
endCh := make(chan *LocalPartySaveData, len(pIDs))
updater := test.SharedPartyUpdater
startGR := runtime.NumGoroutine()
// init the parties
for i := 0; i < len(pIDs); i++ {
var P *LocalParty
params := tss.NewParameters(tss.S256(), p2pCtx, pIDs[i], len(pIDs), threshold)
// do not use in untrusted setting
params.SetNoProofMod()
// do not use in untrusted setting
params.SetNoProofFac()
if i < len(fixtures) {
P = NewLocalParty(params, outCh, endCh, fixtures[i].LocalPreParams).(*LocalParty)
} else {
P = NewLocalParty(params, outCh, endCh).(*LocalParty)
}
parties = append(parties, P)
go func(P *LocalParty) {
if err := P.Start(); err != nil {
errCh <- err
}
}(P)
}
LocalParty 是理解 tss 协议中一个非常重要的数据类型, 可以看作是最终进行多方安全计算的实体, 前面创建的 p2pCtx, pids 这些都是创建 LocalParty 的辅助条件, 我们可以看看 LocalParty 里面都有啥
go
LocalParty struct {
*tss.BaseParty
params *tss.Parameters
temp localTempData
data LocalPartySaveData
// outbound messaging
ut chan<- tss.Message
end chan<- *LocalPartySaveData
secret *big.Int
}
BaseParty 里面包含了随机种子和 party 运行的控制器(控制进入哪一个round阶段), 和协议本身并无太大关系 , 属于工程上的实现, 相信大家自己也能看懂
Parameters 包含了一些参数设置 比如 指定使用哪一条离散椭圆曲线, 指定参与方, 指定peerContext(上面说的p2pCtx就是peerContext), 指定阈值 threshold , 指定生成随机数的并发数, 生成安全素数safeprime 的超时时间, 是否使用 mod proof 和 fac proof (这两个proof不影响生成密钥的结果), 随机数生成器
LocalParty 消息通信
LocalParty 之间消息通信的路由规则分为两类, broadcast 和 p2p。 broadcast 是一个 LocalParty 广播同样的消息到其他所有 LocalParty, 广播的消息对于所有参与方来说是透明的, 一致的。 p2p 是一个 LocalParty 对另外一个 LocalParty 进行点对点通信, 每次通信的消息是不同的, 需要防止被其他参与方探测。
css
keygen:
for {
...
case msg := <-outCh:
dest := msg.GetTo()
if dest == nil { // broadcast!
for i, P := range parties {
if P.PartyID().Index == msg.GetFrom().Index {
continue
}
go updater(i, msg, errCh)
}
} else { // point-to-point!
if dest[0].Index == msg.GetFrom().Index {
t.Fatalf("party %d tried to send a message to itself (%d)", dest[0].Index, msg.GetFrom().Index)
return
}
go updater(dest[0].Index, msg, errCh)
}
...
}
计算完成后验证结果
这一部分没什么好讲的, 大体的注释我都在上面写好了, 可能比较难以理解的是下面这段代码 看完后面的 round1, round2 讲解后相信自然就看得懂了
go
keygen:
for {
fmt.Printf("ACTIVE GOROUTINES: %d\n", runtime.NumGoroutine())
select {
case err := <-errCh: // local party 上报 error
common.Logger.Errorf("Error: %s", err)
assert.FailNow(t, err.Error())
break keygen
case msg := <-outCh:
dest := msg.GetTo()
if dest == nil { // 一个 local party 向 其他 所有local party 发消息
for _, P := range parties {
if P.PartyID().Index == msg.GetFrom().Index {
continue
}
go updater(P, msg, errCh)
}
} else { // 一个 local party 向另一个 local party 发消息
if dest[0].Index == msg.GetFrom().Index {
t.Fatalf("party %d tried to send a message to itself (%d)", dest[0].Index, msg.GetFrom().Index)
return
}
go updater(parties[dest[0].Index], msg, errCh)
}
case save := <-endCh:
// 找出 最终计算结果 savedata 对应的是第几个 local party
index, err := save.OriginalIndex()
assert.NoErrorf(t, err, "should not be an error getting a party's index from save data")
// 将计算结果写入本地
tryWriteTestFixtureFile(t, index, *save)
atomic.AddInt32(&ended, 1)// 完成计算结果的 local party 数量 +1
if atomic.LoadInt32(&ended) == int32(len(pIDs)) {
t.Logf("Done. Received save data from %d participants", ended)
// u 为
u := new(big.Int)
// 对每一个 party 算出来的分片数据进行合法性校验
for j, Pj := range parties {
pShares := make(vss.Shares, 0)
// 收集 Pj 在 round 2 协议中收到的分片(share)
for _, P := range parties {
vssMsgs := P.temp.kgRound2Message1s
share := vssMsgs[j].Content().(*KGRound2Message1).Share
shareStruct := &vss.Share{
Threshold: threshold,
ID: P.PartyID().KeyInt(),
Share: new(big.Int).SetBytes(share),
}
pShares = append(pShares, shareStruct)
}
// 通过拉格朗日插值恢复出曲线的常数项, 也就是 Pj 的原始私钥 uj
uj, err := pShares[:threshold+1].ReConstruct(tss.S256())
assert.NoError(t, err, "vss.ReConstruct should not throw error")
// uj 应该等于 Pj.temp.ui, 其中 ui 为 round1 中的随机生成的原始私钥, 应该和 通过拉格朗日插值恢复出来的 uj 相等
assert.Equal(t, uj, Pj.temp.ui)
// uG为原始私钥*G
uG := crypto.ScalarBaseMult(tss.EC(), uj)
// Pj.temo.vs[0] 为 round1 中随机生成的原始私钥*G 应该和 uG 相等
assert.True(t, uG.Equals(Pj.temp.vs[0]), "ensure u*G[j] == V_0")
// Pj.data.Xi 为 round3 阶段算出来的私钥, 也是最终保存的私钥(savedata中的 Xi), 相当于 Alice 最终得到的分片 107
xj := Pj.data.Xi
//gXj Pj 的最终私钥*G 也就是 Pj 的公钥, 也就是savedata 中的BigXj
gXj := crypto.ScalarBaseMult(tss.EC(), xj)
BigXj := Pj.data.BigXj[j]
// savedata中的 BigXj[j] 应该和 Xi*G 相等
assert.True(t, BigXj.Equals(gXj), "ensure BigX_j == g^x_j")
// // 污染一个分片, 然后恢复出私钥 uj,再乘*G得到假的公钥 应该不等于 Pj.temp.vs[0]
{
badShares := pShares[:threshold]
badShares[len(badShares)-1].Share.Set(big.NewInt(0))
uj, err := pShares[:threshold].ReConstruct(tss.S256())
assert.NoError(t, err)
assert.NotEqual(t, parties[j].temp.ui, uj)
BigXjX, BigXjY := tss.EC().ScalarBaseMult(uj.Bytes())
assert.NotEqual(t, BigXjX, Pj.temp.vs[0].X())
assert.NotEqual(t, BigXjY, Pj.temp.vs[0].Y())
}
u = new(big.Int).Add(u, uj) // 每一个 Pj 的 私钥相加
}
// 从 最终计算结果 savedata 中取出最终的公钥
pkX, pkY := save.ECDSAPub.X(), save.ECDSAPub.Y()
pk := ecdsa.PublicKey{
Curve: tss.EC(),
X: pkX,
Y: pkY,
}
sk := ecdsa.PrivateKey{
PublicKey: pk,
D: u,
}
// 确保公钥位于 离散椭圆曲线上
assert.True(t, sk.IsOnCurve(pkX, pkY), "public key must be on curve")
// 使用私钥签名, 公钥验签名验证
assert.NotZero(t, u, "u should not be zero")
ourPkX, ourPkY := tss.EC().ScalarBaseMult(u.Bytes())
assert.Equal(t, pkX, ourPkX, "pkX should match expected pk derived from u")
assert.Equal(t, pkY, ourPkY, "pkY should match expected pk derived from u")
t.Log("Public key tests done.")
// 确认每一个 party 都计算得到同一把公钥
for _, Pj := range parties {
assert.Equal(t, pkX, Pj.data.ECDSAPub.X())
assert.Equal(t, pkY, Pj.data.ECDSAPub.Y())
}
t.Log("Public key distribution test done.")
// ecdsa 签名与验签
data := make([]byte, 32)
for i := range data {
data[i] = byte(i)
}
r, s, err := ecdsa.Sign(rand.Reader, &sk, data)
assert.NoError(t, err, "sign should not throw an error")
ok := ecdsa.Verify(&pk, data, r, s)
assert.True(t, ok, "signature should be ok")
t.Log("ECDSA signing test done.")
t.Logf("Start goroutines: %d, End goroutines: %d", startGR, runtime.NumGoroutine())
break keygen
}
}
}
}
运行阶段 Round1
go
func (round *round1) Start() *tss.Error {
// 标记运行阶段
if round.started {
return round.WrapError(errors.New("round already started"))
}
round.number = 1
round.started = true
round.resetOK()
// 取出partyid 和对应的索引
Pi := round.PartyID()
i := Pi.Index
// 1. ui为当前party的私钥
ui := common.GetRandomPositiveInt(round.PartialKeyRand(), round.EC().Params().N)
// 将私钥临时存储起来, 单测中要用到这个变量, 线上环境建议把这行代码注释掉
round.temp.ui = ui
// 取出每个party私钥对应的完整索引, 好比Aclice是1, Bob是
ids := round.Parties().IDs().Keys()
// 使用 feldman vss 创建 vs 和 shares
// shares 是多项式系数*G
// vs 是多项式曲线上的点
vs, shares, err := vss.Create(round.EC(), round.Threshold(), ui, ids, round.Rand())
if err != nil {
return round.WrapError(err, Pi)
}
round.save.Ks = ids
// 立刻从内存清空原始的私钥ui
ui = zero
_ = ui
// 将 vs 展开成一个大整数
pGFlat, err := crypto.FlattenECPoints(vs)
if err != nil {
return round.WrapError(err, Pi)
}
// pGFlat 的 commit
cmt := cmts.NewHashCommitment(round.Rand(), pGFlat...)
// 随机生成 localPreParms, 并且验证 localPreParms 的合法性
var preParams *LocalPreParams
if round.save.LocalPreParams.Validate() && !round.save.LocalPreParams.ValidateWithProof() {
return round.WrapError(
errors.New("`optionalPreParams` failed to validate; it might have been generated with an older version of tss-lib"))
} else if round.save.LocalPreParams.ValidateWithProof() {
preParams = &round.save.LocalPreParams
} else {
{
ctx, cancel := context.WithTimeout(context.Background(), round.SafePrimeGenTimeout())
defer cancel()
preParams, err = GeneratePreParamsWithContextAndRandom(ctx, round.Rand(), round.Concurrency())
if err != nil {
return round.WrapError(errors.New("pre-params generation failed"), Pi)
}
}
}
round.save.LocalPreParams = *preParams
round.save.NTildej[i] = preParams.NTildei
round.save.H1j[i], round.save.H2j[i] = preParams.H1i, preParams.H2i
// 对 localPreParms 使用 dlnproof
h1i, h2i, alpha, beta, p, q, NTildei := preParams.H1i,
preParams.H2i,
preParams.Alpha,
preParams.Beta,
preParams.P,
preParams.Q,
preParams.NTildei
dlnProof1 := dlnproof.NewDLNProof(h1i, h2i, alpha, p, q, NTildei, round.Rand())
dlnProof2 := dlnproof.NewDLNProof(h2i, h1i, beta, p, q, NTildei, round.Rand())
// for this P: SAVE
// - shareID
// and keep in temporary storage:
// - VSS Vs
// - our set of Shamir shares
round.temp.ssidNonce = new(big.Int).SetUint64(0)
// 将上面生成的Lins变量保存下来
round.save.ShareID = ids[i]
round.temp.vs = vs // 保存拉格朗日多项式的commitment
ssid, err := round.getSSID()
if err != nil {
return round.WrapError(errors.New("failed to generate ssid"))
}
round.temp.ssid = ssid
round.temp.shares = shares // shares
round.save.PaillierSK = preParams.PaillierSK
round.save.PaillierPKs[i] = &preParams.PaillierSK.PublicKey
round.temp.deCommitPolyG = cmt.D // 保存
// 广播 round1 的消息出去
{
msg, err := NewKGRound1Message(
round.PartyID(), cmt.C, &preParams.PaillierSK.PublicKey, preParams.NTildei, preParams.H1i, preParams.H2i, dlnProof1, dlnProof2)
if err != nil {
return round.WrapError(err, Pi)
}
round.temp.kgRound1Messages[i] = msg
round.out <- msg
}
return nil
}
round1 主要干了以下几个事情
-
生成原始私钥ui
-
根据原始私钥和threshold使用 feldman vss 生成 threshold+1 次方的曲线, 然后返回曲线的系数*G(G为椭圆曲线基点) 和曲线上的点, 也就是 shares 和 vs
-
如果没有提前生成localPreParms,则现场生成localPreParms, 说明一下这里的localPreParms其实并不参与密钥生成, 也就是有没有这一步和密钥生成的结果无关, 但是在签名的时候需要用到这几个参数, 签名需要Parillier同态加密, 而Paillier的安全性依赖于大整数的质因数分解, 所以这里以及后面的各种proof其实是对这几个大质数的合法性进行验证
-
将各种数据保存起来等待round2进一步处理
round2
round2 的代码我也写好了注释
go
func (round *round2) Start() *tss.Error {
// 标记运行状态
if round.started {
return round.WrapError(errors.New("round already started"))
}
round.number = 2
round.started = true
round.resetOK()
common.Logger.Debugf(
"%s Setting up DLN verification with concurrency level of %d",
round.PartyID(),
round.Concurrency(),
)
dlnVerifier := NewDlnProofVerifier(round.Concurrency())
i := round.PartyID().Index
// 验证第一步的 dln proofs ,也就是验证其他party发过来的round1信息
h1H2Map := make(map[string]struct{}, len(round.temp.kgRound1Messages)*2)
dlnProof1FailCulprits := make([]*tss.PartyID, len(round.temp.kgRound1Messages))
dlnProof2FailCulprits := make([]*tss.PartyID, len(round.temp.kgRound1Messages))
wg := new(sync.WaitGroup)
for j, msg := range round.temp.kgRound1Messages {
r1msg := msg.Content().(*KGRound1Message)
H1j, H2j, NTildej, paillierPKj := r1msg.UnmarshalH1(),
r1msg.UnmarshalH2(),
r1msg.UnmarshalNTilde(),
r1msg.UnmarshalPaillierPK()
if paillierPKj.N.BitLen() != paillierBitsLen {
return round.WrapError(errors.New("got paillier modulus with insufficient bits for this party"), msg.GetFrom())
}
if H1j.Cmp(H2j) == 0 {
return round.WrapError(errors.New("h1j and h2j were equal for this party"), msg.GetFrom())
}
if NTildej.BitLen() != paillierBitsLen {
return round.WrapError(errors.New("got NTildej with insufficient bits for this party"), msg.GetFrom())
}
h1JHex, h2JHex := hex.EncodeToString(H1j.Bytes()), hex.EncodeToString(H2j.Bytes())
if _, found := h1H2Map[h1JHex]; found {
return round.WrapError(errors.New("this h1j was already used by another party"), msg.GetFrom())
}
if _, found := h1H2Map[h2JHex]; found {
return round.WrapError(errors.New("this h2j was already used by another party"), msg.GetFrom())
}
h1H2Map[h1JHex], h1H2Map[h2JHex] = struct{}{}, struct{}{}
wg.Add(2)
_j := j
_msg := msg
dlnVerifier.VerifyDLNProof1(r1msg, H1j, H2j, NTildej, func(isValid bool) {
if !isValid {
dlnProof1FailCulprits[_j] = _msg.GetFrom()
}
wg.Done()
})
dlnVerifier.VerifyDLNProof2(r1msg, H2j, H1j, NTildej, func(isValid bool) {
if !isValid {
dlnProof2FailCulprits[_j] = _msg.GetFrom()
}
wg.Done()
})
}
wg.Wait()
for _, culprit := range append(dlnProof1FailCulprits, dlnProof2FailCulprits...) {
if culprit != nil {
return round.WrapError(errors.New("dln proof verification failed"), culprit)
}
}
// save NTilde_j, h1_j, h2_j, ...
for j, msg := range round.temp.kgRound1Messages {
if j == i {
continue
}
r1msg := msg.Content().(*KGRound1Message)
paillierPK, H1j, H2j, NTildej, KGC := r1msg.UnmarshalPaillierPK(),
r1msg.UnmarshalH1(),
r1msg.UnmarshalH2(),
r1msg.UnmarshalNTilde(),
r1msg.UnmarshalCommitment()
round.save.PaillierPKs[j] = paillierPK // used in round 4
round.save.NTildej[j] = NTildej
round.save.H1j[j], round.save.H2j[j] = H1j, H2j
round.temp.KGCs[j] = KGC
}
// 5. p2p send share ij to Pj
shares := round.temp.shares
ContextI := append(round.temp.ssid, big.NewInt(int64(i)).Bytes()...)
for j, Pj := range round.Parties().IDs() {
facProof := &facproof.ProofFac{
P: zero, Q: zero, A: zero, B: zero, T: zero, Sigma: zero,
Z1: zero, Z2: zero, W1: zero, W2: zero, V: zero,
}
if !round.Params().NoProofFac() {
var err error
facProof, err = facproof.NewProof(ContextI, round.EC(), round.save.PaillierSK.N, round.save.NTildej[j],
round.save.H1j[j], round.save.H2j[j], round.save.PaillierSK.P, round.save.PaillierSK.Q, round.Rand())
if err != nil {
return round.WrapError(err, round.PartyID())
}
}
// 依次往其他party发送消, 相当于Alice 将16,22,30发给Bob, Charilize, David
r2msg1 := NewKGRound2Message1(Pj, round.PartyID(), shares[j], facProof)
// do not send to this Pj, but store for round 3
if j == i {
round.temp.kgRound2Message1s[j] = r2msg1
continue
}
round.out <- r2msg1
}
// 对localPreParms中使用 modProof
modProof := &modproof.ProofMod{W: zero, X: *new([80]*big.Int), A: zero, B: zero, Z: *new([80]*big.Int)}
if !round.Parameters.NoProofMod() {
var err error
modProof, err = modproof.NewProof(ContextI, round.save.PaillierSK.N,
round.save.PaillierSK.P, round.save.PaillierSK.Q, round.Rand())
if err != nil {
return round.WrapError(err, round.PartyID())
}
}
r2msg2 := NewKGRound2Message2(round.PartyID(), round.temp.deCommitPolyG, modProof)
round.temp.kgRound2Message2s[i] = r2msg2
round.out <- r2msg2
return nil
}
round2 的代码非常多, 但主要就做了两件事
- 各种 proof, 证明localPreParms里面参数的合法性, 主要是质数的安全性证明
- 将round1的分片信息广播给其他party
round3
round3 又是keygen中的核心操作了, 非常重要
go
func (round *round3) Start() *tss.Error {
// 标记运行状态
if round.started {
return round.WrapError(errors.New("round already started"))
}
round.number = 3
round.started = true
round.resetOK()
// 取出partyid 和索引
Ps := round.Parties().IDs()
PIdx := round.PartyID().Index
// 1,9. calculate xi
// 计算当前party的私钥分片, 也就是将自己原始私钥的横坐标带入拉格朗日多项式 + 其他party拉格朗日多项式带入各自私钥横坐标 的累积和,得到的一個bigint
// 比如 Alice 计算得到的107 就是Alice 这个 party的 xi
xi := new(big.Int).Set(round.temp.shares[PIdx].Share)
for j := range Ps {
if j == PIdx {
continue
}
r2msg1 := round.temp.kgRound2Message1s[j].Content().(*KGRound2Message1)
share := r2msg1.UnmarshalShare()
xi = new(big.Int).Add(xi, share)
}
// 保存当前party协商出来的最终私钥数据
round.save.Xi = new(big.Int).Mod(xi, round.Params().EC().Params().N)
// 2-3.
// Vc 为 当前party 的拉格朗日多项式系数*离散椭圆曲线基点G得到的点集合, Vc[0] 就為當前party的公鑰
Vc := make(vss.Vs, round.Threshold()+1)
for c := range Vc {
Vc[c] = round.temp.vs[c]
}
type vssOut struct {
unWrappedErr error
pjVs vss.Vs
}
chs := make([]chan vssOut, len(Ps))
for i := range chs {
if i == PIdx {
continue
}
chs[i] = make(chan vssOut)
}
// 验证其他party发送过来的 vs
for j := range Ps {
if j == PIdx {
continue
}
ContextJ := common.AppendBigIntToBytesSlice(round.temp.ssid, big.NewInt(int64(j)))
go func(j int, ch chan<- vssOut) {
// KGCj 为每个party feldman-vss 的commitment部分
KGCj := round.temp.KGCs[j]
r2msg2 := round.temp.kgRound2Message2s[j].Content().(*KGRound2Message2)
// KGDj 为每个party feldman-vss 的decommit
KGDj := r2msg2.UnmarshalDeCommitment()
cmtDeCmt := commitments.HashCommitDecommit{C: KGCj, D: KGDj}
// 进行一次hash(sha256)验证, 並返回 decommit部分
ok, flatPolyGs := cmtDeCmt.DeCommit()
if !ok || flatPolyGs == nil {
ch <- vssOut{errors.New("de-commitment verify failed"), nil}
return
}
// 将 decommit 部分还原成 ecpoint
PjVs, err := crypto.UnFlattenECPoints(round.Params().EC(), flatPolyGs)
if err != nil {
ch <- vssOut{err, nil}
return
}
modProof, err := r2msg2.UnmarshalModProof()
if err != nil && round.Parameters.NoProofMod() {
// For old parties, the modProof could be not exist
// Not return error for compatibility reason
common.Logger.Warningf("modProof not exist:%s", Ps[j])
} else {
if err != nil {
ch <- vssOut{errors.New("modProof verify failed"), nil}
return
}
if ok = modProof.Verify(ContextJ, round.save.PaillierPKs[j].N); !ok {
ch <- vssOut{errors.New("modProof verify failed"), nil}
return
}
}
// Pjshare 为其他party发给当前partyd 的数据, 也就是其他party上的一个分片, 比如上面举例中的 Alice 将 f(2)= 16 分享给Bob
r2msg1 := round.temp.kgRound2Message1s[j].Content().(*KGRound2Message1)
PjShare := vss.Share{
Threshold: round.Threshold(), // 阈值
ID: round.PartyID().KeyInt(), // 相当于 Alice 分享给 Bob 的 2
Share: r2msg1.UnmarshalShare(),// 相当于 Alice 分享给 Bob 的 16
}
// 校验了 分片和其对应的多项式 之间的关系
if ok = PjShare.Verify(round.Params().EC(), round.Threshold(), PjVs); !ok {
ch <- vssOut{errors.New("vss verify failed"), nil}
return
}
facProof, err := r2msg1.UnmarshalFacProof()
if err != nil && round.NoProofFac() {
// For old parties, the facProof could be not exist
// Not return error for compatibility reason
common.Logger.Warningf("facProof not exist:%s", Ps[j])
} else {
if err != nil {
ch <- vssOut{errors.New("facProof verify failed"), nil}
return
}
if ok = facProof.Verify(ContextJ, round.EC(), round.save.PaillierPKs[j].N, round.save.NTildei,
round.save.H1i, round.save.H2i); !ok {
ch <- vssOut{errors.New("facProof verify failed"), nil}
return
}
}
// (9) handled above
ch <- vssOut{nil, PjVs}
}(j, chs[j])
}
// 将其他party的vs送入channel
vssResults := make([]vssOut, len(Ps))
{
culprits := make([]*tss.PartyID, 0, len(Ps)) // who caused the error(s)
for j, Pj := range Ps {
if j == PIdx {
continue
}
vssResults[j] = <-chs[j]
// collect culprits to error out with
if err := vssResults[j].unWrappedErr; err != nil {
culprits = append(culprits, Pj)
}
}
var multiErr error
if len(culprits) > 0 {
for _, vssResult := range vssResults {
if vssResult.unWrappedErr != nil {
multiErr = multierror.Append(multiErr, vssResult.unWrappedErr)
}
}
return round.WrapError(multiErr, culprits...)
}
}
// 将(当前 party 的拉格朗日多项式 的每一项系数和其他party发过来的系数相加)*G, 如
// Alice 的 vc[0]= 10*G + 20*G + 30*G + 40*G =100*G
// vc[1]= 1*G + 3*G + 1*G + 3*G = 6*G = 6*G
// vc[2] = 1*G +2*G -1*G-1*G=1*G
// 最终的vc 就是完整多项式曲线的系数*G,还记得完整的曲线是 100 + 6*x + x^2
{
var err error
culprits := make([]*tss.PartyID, 0, len(Ps)) // who caused the error(s)
for j, Pj := range Ps {
if j == PIdx {
continue
}
// 10-11.
PjVs := vssResults[j].pjVs
for c := 0; c <= round.Threshold(); c++ {
Vc[c], err = Vc[c].Add(PjVs[c])
if err != nil {
culprits = append(culprits, Pj)
}
}
}
if len(culprits) > 0 {
return round.WrapError(errors.New("adding PjVs[c] to Vc[c] resulted in a point not on the curve"), culprits...)
}
}
// 计算 bigXj
// bigXj[j] 为第j个party的Xi*G, 也就是每个party跑完协议后的公钥
// 如 Alice的 bigXj= [(100 + 6*x + x^2)|x=1 \*G=107*G
// Bob的bigXj = [(100 + 6*x + x^2)|x=2] *G=116*G
// 可以发现刚好等于 Xi*G
{
var err error
modQ := common.ModInt(round.Params().EC().Params().N)
culprits := make([]*tss.PartyID, 0, len(Ps)) // who caused the error(s)
bigXj := round.save.BigXj
for j := 0; j < round.PartyCount(); j++ {
// kj 为第 j 个 party 分片的横坐标, 比如 Alice 持有分片的横坐标为1, Bob 持有分片的横坐标为2 ...
Pj := round.Parties().IDs()[j]
kj := Pj.KeyInt()
// Vc[0]为(当前 party 的拉格朗日多项式常數項 + 其他party分享过来的数字相加和)*G
BigXj := Vc[0]
// z 为1
z := new(big.Int).SetInt64(int64(1))
// 将 x=1, 2,3 带入 vc 多项式计算和, 也就是当前 party跑完协议后的公钥
for c := 1; c <= round.Threshold(); c++ {
z = modQ.Mul(z, kj) // party分片横坐标(1,2,3,...)的一次方, 二次方, ... , (threshold-1)次方
BigXj, err = BigXj.Add(Vc[c].ScalarMult(z))
if err != nil {
culprits = append(culprits, Pj)
}
}
bigXj[j] = BigXj
}
if len(culprits) > 0 {
return round.WrapError(errors.New("adding Vc[c].ScalarMult(z) to BigXj resulted in a point not on the curve"), culprits...)
}
round.save.BigXj = bigXj
}
// 计算当前part原始的公钥 也就是拉格朗日多项式的常数项*G
// 也就是 ui*G
ecdsaPubKey, err := crypto.NewECPoint(round.Params().EC(), Vc[0].X(), Vc[0].Y())
if err != nil {
return round.WrapError(errors2.Wrapf(err, "public key is not on the curve"))
}
round.save.ECDSAPub = ecdsaPubKey
// PRINT public key & private share
common.Logger.Debugf("%s public key: %x", round.PartyID(), ecdsaPubKey)
// BROADCAST paillier proof for Pi
ki := round.PartyID().KeyInt()
proof := round.save.PaillierSK.Proof(ki, ecdsaPubKey) // 使用 Paillier 对 ki 和 ecdsaPubKey 进行签名
r3msg := NewKGRound3Message(round.PartyID(), proof)
round.temp.kgRound3Messages[PIdx] = r3msg
round.out <- r3msg
return nil
}
round 3 中的操作还是比较复杂的, 主要是这几方面
- 计算 每个party最终保存的私钥Xi
- 计算BigXj 也就是每一个party的Xi*G
- 计算每个party的原始私钥对应的公钥也就是ui*G= ecdsapubKey
- 使用 paillierproof 对完整的公钥ecdsapubkey和party的横坐标进行签名然后广播出去
round4
go
func (round *round4) Start() *tss.Error {
// 设置运行状态
if round.started {
return round.WrapError(errors.New("round already started"))
}
round.number = 4
round.started = true
round.resetOK()
// 取出当前party索引, id, 横坐标, 完整公钥
i := round.PartyID().Index
Ps := round.Parties().IDs()
PIDs := Ps.Keys()
ecdsaPub := round.save.ECDSAPub
// 验证round3中的签名, 确保最后算出来的公钥和每个party的横坐标的正确性
r3msgs := round.temp.kgRound3Messages
chs := make([]chan bool, len(r3msgs))
for i := range chs {
chs[i] = make(chan bool)
}
for j, msg := range round.temp.kgRound3Messages {
if j == i {
continue
}
r3msg := msg.Content().(*KGRound3Message)
go func(prf paillier.Proof, j int, ch chan<- bool) {
ppk := round.save.PaillierPKs[j]
ok, err := prf.Verify(ppk.N, PIDs[j], ecdsaPub)
if err != nil {
common.Logger.Error(round.WrapError(err, Ps[j]).Error())
ch <- false
return
}
ch <- ok
}(r3msg.UnmarshalProofInts(), j, chs[j])
}
// consume unbuffered channels (end the goroutines)
for j, ch := range chs {
if j == i {
round.ok[j] = true
continue
}
round.ok[j] = <-ch
}
culprits := make([]*tss.PartyID, 0, len(Ps)) // who caused the error(s)
for j, ok := range round.ok {
if !ok {
culprits = append(culprits, Ps[j])
common.Logger.Warningf("paillier verify failed for party %s", Ps[j])
continue
}
common.Logger.Debugf("paillier verify passed for party %s", Ps[j])
}
if len(culprits) > 0 {
return round.WrapError(errors.New("paillier verify failed"), culprits...)
}
round.end <- round.save
return nil
}
round4的操作比较简单, 主要是对round3中的签名进行验证, 然后将最终的savedata送入结束的channel.
至此分布式密钥生成部分已经全部结束。
总结
我们完整的梳理了币安tsslib中ecdsa keygen部分的全部流程, 从单测函数入手, 到最终保存的savedata, 到核心的原理(Pedersen's DKG), 到 round1, round2, round3, round4 四个阶段的运行过程。
其实还有几个密码学的细节的没有解释清楚, 比如dlnproof, modproof, facproof, paillier proof, 但是有不影响大致理解keygen的核心思想。 这几个密码学的证明, 老实说我也只是知道皮毛, 大概是证明 Paillier 同态加密的密钥的合法性。 我们知道 Parillier 同态加密的安全性在于大数的质因数分解, 也就是 N=P*Q, 如果P或者Q不安全 比如P,Q不是safe prime, P或者Q都太小了,那么同态加密也就不安全了。 因为中签名过程中需要用到Paillier同态加密 而相关的参数需要中keygen的时候提前准备好, 所以需要有这么多proof去证明每一个party的Paillier参数安全性。
后面我也会继续更新signing的部分, 欢迎大家点赞, 收藏, 评论, 关注