原文鏈接:https://www.ardanlabs.com/blog/2022/02/blockchain-01-digital-accounts-signatures-verification.html
介紹
這是探索 Ardan 區(qū)塊鏈項(xiàng)目的語義和實(shí)現(xiàn)細(xì)節(jié)的系列文章中的第一篇。該代碼是區(qū)塊鏈的參考實(shí)現(xiàn)崇决,并非是當(dāng)今使用的任何特定區(qū)塊鏈。雖然代碼是按照生產(chǎn)級編碼標(biāo)準(zhǔn)設(shè)計(jì)的互亮,但是我不會將這個項(xiàng)目用于學(xué)習(xí)之外的任何事情慢宗。
我使用以太坊項(xiàng)目作為參考坪蚁,并從該代碼中獲得靈感。我希望理解 Ardan 區(qū)塊鏈中的代碼能給你提供足夠的知識來幫助你理解以太坊的代碼镜沽。
本系列將通過 Ardan 區(qū)塊鏈項(xiàng)目提供的支持實(shí)現(xiàn)來探索區(qū)塊鏈的五個方面敏晤。
- 帶有電子簽名和驗(yàn)證的數(shù)字賬戶
- 計(jì)算機(jī)之間的事務(wù)分發(fā)/同步/通信
- 賬目在不同計(jì)算機(jī)上冗余存儲
- 不同計(jì)算機(jī)達(dá)成共識以處理和存儲新交易
- 檢測過去交易的任何偽造
這篇文章將關(guān)注第一個方面,即 Ardan 區(qū)塊鏈如何為數(shù)字賬戶缅茉、簽名和驗(yàn)證提供支持嘴脾。
源代碼
Ardan 區(qū)塊鏈項(xiàng)目的源代碼可以在下面的鏈接中找到。
https://github.com/ardanlabs/blockchain
初始化
每個區(qū)塊鏈都有一個初始文件宾舅,提供區(qū)塊鏈的全局設(shè)置和初始狀態(tài)统阿。對于 Ardan 區(qū)塊鏈,我也創(chuàng)建了一個初始文件筹我。
** 片段1:初始文件**
{
"date": "2021-12-17T00:00:00.000000000Z",
"chain_id": "the-ardan-blockchain",
"difficulty": 6,
"transactions_per_block": 2,
"mining_reward": 700,
"gas_price": 15,
"balance_sheet": {
"0xF01813E4B85e178A83e29B8E7bF26BD830a25f32": 1000000,
"0xdd6B972ffcc631a62CAE1BB9d80b7ff429c8ebA4": 1000000
}
}
我找到了描述以太坊初始文件的[文檔]扶平,里面提供了很好的解釋。(https://gist.github.com/0mkara/b953cc2585b18ee098cd#create-custom-ethereum-network)
目前這些設(shè)置可能對你沒有多大意義蔬蕊,但稍后的帖子將詳細(xì)介紹這些細(xì)節(jié)〗岢危現(xiàn)在,請關(guān)注資產(chǎn)負(fù)債表下的原始賬戶(十六進(jìn)制地址)岸夯。我和 Pavel 都從一百萬單位的 ARD 開始麻献。
注意:以太坊定義了一個以太單位描述的面額公制系統(tǒng)。最小的單位稱為 a wei
猜扮,它表示一個單位(如一便士)和ether
勉吻,表示 10^18 個單位(如 1,000,000,000,000,000,000 個便士)。
帳戶和地址
再次注意一下初始文件中的原始帳戶旅赢。
片段 2:原始帳戶
"balance_sheet": {
"0xF01813E4B85e178A83e29B8E7bF26BD830a25f32": 1000000,
"0xdd6B972ffcc631a62CAE1BB9d80b7ff429c8ebA4": 1000000
}
我怎么知道片段 2 中哪些地址代表我和 Pavel 的帳戶齿桃?
只是看他們,我不知道煮盼。區(qū)塊鏈的一個特性是賬戶是匿名的短纵,它們只是大的十六進(jìn)制數(shù)字。最終僵控,你將用你的帳戶進(jìn)行交易香到,此時可以表名你是所有者。交易的達(dá)成或只是與他人的一般活動是因?yàn)槟銚碛羞@些資產(chǎn)。
這些地址是如何產(chǎn)生的悠就?
不同的區(qū)塊鏈?zhǔn)褂貌煌膶ぶ贩桨盖鳎ǔK鼈兪鞘褂脵E圓曲線數(shù)字簽名算法(ECDSA)生成的。該算法生成的私鑰/公鑰對只能用于簽名理卑,不能用于加密翘紊。ECDSA 是以太坊使用的算法。
在 Ardan 項(xiàng)目中藐唠,我在zblock/accounts
文件夾下添加了幾個文件,其中包含一組基于Secp256k1曲線的私有 ECDSA 密鑰鹉究。
片段 3:帳戶文件夾
$ ls -l zblock/accounts
-rw------- 1 bill staff 64B Feb 1 08:49 baba.ecdsa
-rw------- 1 bill staff 64B Feb 1 08:49 cesar.ecdsa
-rw------- 1 bill staff 64B Feb 1 08:49 kennedy.ecdsa
-rw------- 1 bill staff 64B Feb 1 08:49 pavel.ecdsa
Ardan 項(xiàng)目使用這些文件來維護(hù)不同測試帳戶的私鑰宇立。你將在該文件夾中看到我和 Pavel 的文件。
片段 4:kennedy.ecdsa
9f332e3700d8fc2446eaf6d15034cf96e0c2745e40353deef032a5dbf1dfed93
片段4 顯示了我的私有 ECDSA 密鑰的十六進(jìn)制編碼版本自赔,它代表我在 Ardan 區(qū)塊鏈上的帳戶妈嘹。Ardan 數(shù)字錢包使用該密鑰代表我對 Ardan 區(qū)塊鏈執(zhí)行活動。你很快就會意識到數(shù)字錢包只不過是一個應(yīng)用程序绍妨,它可以使用私鑰作為你的帳戶身份對區(qū)塊鏈節(jié)點(diǎn)進(jìn)行網(wǎng)絡(luò)調(diào)用润脸。
注意:不同的區(qū)塊鏈?zhǔn)褂貌煌木W(wǎng)絡(luò)協(xié)議。例如他去,以太坊使用JSON-RPC毙驯。
以太坊項(xiàng)目有一個加密包,為使用 ECDSA 提供支持灾测。Ardan 項(xiàng)目使用這個包來管理數(shù)字簽名爆价。
片段5:生成私鑰
01 import "github.com/ethereum/go-ethereum/crypto"
02
03 privateKey, err := crypto.GenerateKey()
04 if err != nil {
05 return err
06 }
07
08 file := filepath.Join("zblock/accounts", "kennedy.ecdsa")
09 if err := crypto.SaveECDSA(file, privateKey); err != nil {
10 return err
11 }
片段 5 顯示了 Ardan 錢包如何在帳戶文件夾中為新用戶生成私鑰文件。
通常媳搪,私鑰是通過生成 12 或 24 個單詞的助記詞來生成的铭段。此助記詞代表你帳戶的私鑰,可用于離線存儲你的私鑰(紙質(zhì)錢包)或在你的機(jī)器壞了或如果你想從不同的機(jī)器(移動設(shè)備秦爆、新計(jì)算機(jī)等)使用你的帳戶時恢復(fù)你的帳戶序愚。
如果有人找到你的助記詞,那么他們就擁有你的私鑰等限,并且可以配置錢包應(yīng)用程序?qū)①Y金從你的帳戶中轉(zhuǎn)出爸吮。無論如何,永遠(yuǎn)不要與任何你不信任的人分享這個助記詞精刷。如果你需要共享助記詞拗胜,那么一種解決方案是使用諸如Shamir secrets之類的系統(tǒng)將助記詞分發(fā)給共享的對等組。像這樣的系統(tǒng)將允許在緊急情況下(你的意外死亡)恢復(fù)你的私鑰怒允,避免沒有人能控制你的帳戶埂软。
通過私鑰,你可以生成相應(yīng)的公鑰。公鑰代表您帳戶的身份
片段 6:地址專用
01 privateKey, err := crypto.LoadECDSA("kennedy.ecdsa")
02 if err != nil {
03 log.Fatal(err)
04 }
05
06 address := crypto.PubkeyToAddress(privateKey.PublicKey)
07 fmt.Println(address)
Output:
0xF01813E4B85e178A83e29B8E7bF26BD830a25f32
片段 6 展示了如何使用 Ethereumcrypto
包從公鑰生成地址勘畔。此地址代表你的帳戶在區(qū)塊鏈上的身份所灸。如果有人知道這個地址,他們可以看到你擁有的東西以及你在區(qū)塊鏈上的所有交易(發(fā)送和接收)炫七。
例如爬立,我有一個帳戶,其地址是0x01D398ECb403BE33Cd6ED8c9Fefa1712Be48d8d8
我在以太坊區(qū)塊鏈上使用的万哪。使用此地址侠驯,你可以查看我的所有交易。
圖 1:Etherscan
圖 1 顯示了我在以太坊上的帳戶的 etherscan你可以看到我是如何從我的 Coinbase 賬戶購買以太幣的奕巍,然后是我為在以太坊名稱服務(wù) ( ENS ) 上購買名稱而執(zhí)行的交易吟策。由于地址很容易輸入錯誤,因此創(chuàng)建 ENS 以充當(dāng)?shù)刂返?DNS 查找的止。
圖 2:wkennedy.eth
圖 2 顯示了我的 ENS 名稱wkennedy.eth解析為與我的帳戶關(guān)聯(lián)的地址檩坚。
交易類型
要向 Ardan 區(qū)塊鏈提交交易,需要發(fā)送特定信息诅福。
片段 5:用戶事務(wù)類型
01 type UserTx struct {
02 Nonce uint `json:"nonce"`
03 To string `json:"to"`
04 Value uint `json:"value"`
05 Tip uint `json:"tip"`
06 Data []byte `json:"data"`
07 }
此類型提供有關(guān)誰在獲得資金匾委、他們獲得多少以及與區(qū)塊鏈節(jié)點(diǎn)關(guān)聯(lián)的帳戶將收到多少小費(fèi)(獎金)以成功將此交易存儲在一個塊中的信息。該Data
字段允許將任何額外信息與交易相關(guān)聯(lián)氓润。
請注意赂乐,此類型缺少一個字段來標(biāo)識正在提交交易的帳戶。這是因?yàn)樘峤唤灰椎馁~戶必須通過簽署交易來表明自己的身份旺芽。
片段 6:簽名交易類型
01 type SignedTx struct {
02 UserTx
03 V *big.Int `json:"v"`
04 R *big.Int `json:"r"`
05 S *big.Int `json:"s"`
06 }
該類型嵌入UserTx
類型并添加三個字段沪猴,表示提交交易的賬戶的 ECDSA 簽名。向 Ardan 區(qū)塊鏈提交交易時需要提供這種類型的值采章。
簽署交易
錢包如何給UserTx
簽名以便提交到區(qū)塊鏈运嗜?它首先將用戶事務(wù)散列成一個 32 字節(jié)的切片。
片段 7:散列
01 func (tx UserTx) HashWithArdanStamp() ([]byte, error) {
02 txData, err := json.Marshal(tx)
03 if err != nil {
04 return nil, err
05 }
06
07 txHash := crypto.Keccak256Hash(txData)
08 stamp := []byte("\x19Ardan Signed Message:\n32")
09 tran := crypto.Keccak256Hash(stamp, txHash.Bytes())
10
11 return tran.Bytes(), nil
12 }
該函數(shù)返回一個 32 字節(jié)的散列悯舟,代表用戶交易担租,其中嵌入了 Ardan 標(biāo)記到最終散列中。此最終哈希用于創(chuàng)建簽名抵怎、公鑰提取和簽名驗(yàn)證奋救。
在第 02 行,接收器值被編組為字節(jié)切片反惕,然后通過第 07 行的哈希函數(shù)運(yùn)行尝艘,以生成表示編組的事務(wù)數(shù)據(jù)的 32 字節(jié)數(shù)組。在第 08 行姿染,Ardan 標(biāo)記被轉(zhuǎn)換為字節(jié)切片背亥,因此它可以與第 09 行的交易數(shù)據(jù)的散列相結(jié)合,以生成用于表示交易的 32 個字節(jié)的最終散列。
Ardan 印章用于確保簽署交易時產(chǎn)生的簽名對于 Ardan 區(qū)塊鏈?zhǔn)冀K是唯一的狡汉。以太坊也使用相同的格式來做到這一點(diǎn)娄徊。
清單 8:區(qū)塊鏈印章
Ethereum Stamp Format
\x19Ethereum Signed Message:\n + length(message) + message
Ardan Stamp
"\x19Ardan Signed Message:\n32" + dataHash
注意:以太坊將此稱為簽名,而不是印章盾戴。我覺得這很令人困惑寄锐,因?yàn)檫@個字符串被用來對要簽名的散列交易數(shù)據(jù)加鹽將其視為印章對我來說不那么令人困惑。
通過將封送處理數(shù)據(jù)的散列長度強(qiáng)制為 32 字節(jié)尖啡,可以將長度硬編碼到標(biāo)記 ( \n32
) 中以簡化操作橄仆。以太坊使用了這個技巧。
使用該HashWithArdanStamp
方法可婶,現(xiàn)在可以簽署交易并準(zhǔn)備提交到區(qū)塊鏈沿癞。
片段 9:簽名
01 func (tx UserTx) Sign(privateKey *ecdsa.PrivateKey) (SignedTx, error) {
02
03 // Prepare the transaction for signing.
04 tran, err := tx.HashWithArdanStamp()
05 if err != nil {
06 return SignedTx{}, err
07 }
08
09 // Sign the hash with the private key to produce a signature.
10 sig, err := crypto.Sign(tran.Bytes(), privateKey)
11 if err != nil {
12 return SignedTx{}, err
13 }
14
15 // Convert the 65 byte signature into the [R|S|V] format.
16 v, r, s := toSignatureValues(sig)
17
18 // Construct the signed transaction.
19 signedTx := SignedTx{
20 UserTx: tx,
21 V: v,
22 R: r,
23 S: s,
24 }
25
26 return signedTx, nil
27 }
返回的簽名是一個 65 字節(jié)的切片,使用 ECDSA 格式 [R | S | V]矛渴。前 32 個字節(jié)代表 R 值,接下來的 32 個字節(jié)代表 S 值惫搏,最后一個字節(jié)代表 V 值具温。
注意:如果您想了解有關(guān) R、S 和 V 值的更多信息筐赔,請閱讀這篇出色的文章铣猩。
以太坊將簽名作為 R、S 和 V 存儲在它們不同的交易類型中茴丰,我決定效仿达皿。
代碼 10:簽名字節(jié)到值
01 const ardanID = 29
02
03 func toSignatureValues(sig []byte) (r, s, v *big.Int) {
04 r = new(big.Int).SetBytes(sig[:32])
05 s = new(big.Int).SetBytes(sig[32:64])
06 v = new(big.Int).SetBytes([]byte{sig[64] + ardanID})
07
08 return r, s, v
09 }
以太坊和比特幣對簽名所做的事情是在 V 上添加一個任意數(shù)字。這樣做是為了清楚地表明簽名來自他們的區(qū)塊鏈贿肩。以太坊和比特幣使用的任意數(shù)字是 27峦椰。對于 Ardan 區(qū)塊鏈,我決定使用 29汰规。重要的是要注意汤功,在簽名可以用于任何加密操作之前,需要減去這個任意數(shù)字溜哮。
代碼 11:簽名值到字節(jié)
01 func toSignatureBytes(v, r, s *big.Int) []byte {
02 sig := make([]byte, crypto.SignatureLength)
03
04 copy(sig, r.Bytes())
05 copy(sig[32:], s.Bytes())
06 sig[64] = byte(v.Uint64() - ardanID)
07
08 return sig
09 }
10
11 func toSignatureBytesForDisplay(v, r, s *big.Int) []byte {
12 sig := make([]byte, crypto.SignatureLength)
13
14 copy(sig, r.Bytes())
15 copy(sig[32:], s.Bytes())
16 sig[64] = byte(v.Uint64())
17
18 return sig
19 }
該toSignatureBytes
函數(shù)從 V中刪除ardanID
滔金,因此該值返回 0 或 1。該toSignatureBytesForDisplay
函數(shù)保留V 中的ardanID
并用于顯示茂嗓。
簽名到地址
現(xiàn)在您已經(jīng)知道如何簽署交易餐茵,接下來您需要了解區(qū)塊鏈節(jié)點(diǎn)如何使用簽名來提取公鑰來識別提交交易的賬戶地址。
代碼 12:地址
01 type BlockTx struct {
02 SignedTx
03 Gas uint `json:"gas"`
04 }
05
06 func (tx BlockTx) FromAddress() (string, error) {
07
08 // Prepare the transaction for public key extraction.
09 tran, err := tx.HashWithArdanStamp()
10 if err != nil {
11 return "", err
13 }
14
15 // Convert the [R|S|V] format into the original 65 bytes.
16 sig := toSignatureBytes(tx.V, tx.R, tx.S)
17
18 // Capture the public key associated with this signature.
19 publicKey, err := crypto.SigToPub(tran, sig)
20 if err != nil {
21 return "", err
22 }
23
24 // Extract the account address from the public key.
25 return crypto.PubkeyToAddress(*publicKey).String(), nil
26 }
該方法由區(qū)塊鏈節(jié)點(diǎn)執(zhí)行以檢索簽署交易的賬戶地址述吸。
在第 09 行忿族,該HashWithArdanStamp
方法用于重新創(chuàng)建用于生成接收到的簽名的散列。該數(shù)據(jù)需要完全相同,否則區(qū)塊鏈節(jié)點(diǎn)將確定錯誤的公鑰肠阱。
現(xiàn)在有了公鑰票唆,可以在第 25 行使用crypto
包中的PubkeyToAddress
函數(shù)來提取提交并簽署交易的帳戶的地址。
驗(yàn)證簽名
最后一步是區(qū)塊鏈節(jié)點(diǎn)驗(yàn)證簽名的能力屹徘。
代碼 13:驗(yàn)證簽名
01 func (tx SignedTx) VerifySignature() error {
. . . CHECKS ARE HERE . . .
36 }
執(zhí)行 3 次檢查以驗(yàn)證隨交易數(shù)據(jù)提供的簽名是否代表正確的帳戶走趋。
代碼 14:檢查恢復(fù) ID
03 // Check the recovery id is either 0 or 1.
04 v := tx.V.Uint64() - ardanID
05 if v != 0 && v != 1 {
06 return errors.New("invalid recovery id")
07 }
代碼 14 顯示了執(zhí)行的第一個檢查,確痹胍粒恢復(fù) id 設(shè)置為 ardan id簿煌。如果這個簽名不是由我們之前的Sign
函數(shù)產(chǎn)生的,那么減去 ardan id 就不會產(chǎn)生 0 或 1 的值鉴吹。
代碼 15:檢查簽名值
09 // Check the signature values are valid.
10 if !crypto.ValidateSignatureValues(byte(v), tx.R, tx.S, false) {
11 return errors.New("invalid signature values")
12 }
代碼 15 顯示了第二個檢查姨伟,以驗(yàn)證整個簽名是否有效。這是通過crypto
包中的ValidateSignatureValues
功能完成的豆励。該函數(shù)接受帶有單獨(dú) R夺荒、S 和 V 值的簽名。該函數(shù)的最后一個參數(shù)是知道簽名是否是以太坊 Homestead 版本的一部分良蒸。Homestead 是以太坊平臺的第二個主要版本技扼,也是以太坊的第一個生產(chǎn)版本。
代碼 16:提取公鑰
14 // Prepare the transaction for recovery and validation.
15 tran, err := tx.HashWithArdanStamp()
16 if err != nil {
17 return err
18 }
19
20 // Convert the [R|S|V] format into the original 65 bytes.
21 sig := toSignatureBytes(tx.V, tx.R, tx.S)
22
23 // Capture the uncompressed public key associated with this signature.
24 sigPublicKey, err := crypto.Ecrecover(tran, sig)
25 if err != nil {
26 return fmt.Errorf("ecrecover, %w", err)
27 }
我正在使用crypto
包中的Ecrecover
函數(shù)嫩痰,因?yàn)槲倚枰獙⒐€作為 33 字節(jié)的未壓縮切片用于下一次調(diào)用剿吻。
代碼 17:檢查公鑰創(chuàng)建的數(shù)據(jù)簽名
29 // Check that the given public key created the signature over the data.
30 rs := sig[:crypto.RecoveryIDOffset]
31 if !crypto.VerifySignature(sigPublicKey, tran, rs) {
32 return errors.New("invalid signature")
33 }
代碼 17 顯示了要執(zhí)行的下一次調(diào)用和最后一次檢查。該函數(shù)需要未壓縮的 33 字節(jié)公鑰串纺、散列和標(biāo)記的交易數(shù)據(jù)丽旅,以及簽名中的 R 和 S 值。此驗(yàn)證對于確保正確的帳戶與交易相關(guān)聯(lián)至關(guān)重要纺棺。
當(dāng)區(qū)塊鏈節(jié)點(diǎn)接收到該SignedTx
值時榄笙,它并不真正知道與該UserTx
部分值關(guān)聯(lián)的字段是否與創(chuàng)建簽名時相同。如果不是五辽,則將生成不同的公鑰办斑,因此將使用不同的帳戶。從某人的帳戶中取出錢是多么完美的黑客行為杆逗。
UserTx
重新散列值很重要∠绯幔現(xiàn)在,可以使用公鑰罪郊、重新散列UserTx
的值以及簽名的 R 和 S 值來驗(yàn)證一切是否同步蠕蚜。如果VerifySignature
函數(shù)沒有失敗,則可以確定該值中提供的簽名SignedTx
是由該UserTx
值生成的悔橄,因此公鑰確實(shí)代表了正確的帳戶靶累。