原文链接:https://www.ardanlabs.com/blog/2022/02/blockchain-01-digital-accounts-signatures-verification.html
介绍
这是探索 Ardan 区块链项目的语义和实现细节的系列文章中的第一篇。该代码是区块链的参考实现,并非是当今使用的任何特定区块链。虽然代码是按照生产级编码标准设计的,但是我不会将这个项目用于学习之外的任何事情。
我使用以太坊项目作为参考,并从该代码中获得灵感。我希望理解 Ardan 区块链中的代码能给你提供足够的知识来帮助你理解以太坊的代码。
本系列将通过 Ardan 区块链项目提供的支持实现来探索区块链的五个方面。
- 带有电子签名和验证的数字账户
- 计算机之间的事务分发/同步/通信
- 账目在不同计算机上冗余存储
- 不同计算机达成共识以处理和存储新交易
- 检测过去交易的任何伪造
这篇文章将关注第一个方面,即 Ardan 区块链如何为数字账户、签名和验证提供支持。
源代码
Ardan 区块链项目的源代码可以在下面的链接中找到。
https://github.com/ardanlabs/blockchain
初始化
每个区块链都有一个初始文件,提供区块链的全局设置和初始状态。对于 Ardan 区块链,我也创建了一个初始文件。
** 片段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)
目前这些设置可能对你没有多大意义,但稍后的帖子将详细介绍这些细节。现在,请关注资产负债表下的原始账户(十六进制地址)。我和 Pavel 都从一百万单位的 ARD 开始。
注意:以太坊定义了一个以太单位描述的面额公制系统。最小的单位称为 a wei
,它表示一个单位(如一便士)和ether
,表示 10^18 个单位(如 1,000,000,000,000,000,000 个便士)。
帐户和地址
再次注意一下初始文件中的原始帐户。
片段 2:原始帐户
"balance_sheet": {
"0xF01813E4B85e178A83e29B8E7bF26BD830a25f32": 1000000,
"0xdd6B972ffcc631a62CAE1BB9d80b7ff429c8ebA4": 1000000
}
我怎么知道片段 2 中哪些地址代表我和 Pavel 的帐户?
只是看他们,我不知道。区块链的一个特性是账户是匿名的,它们只是大的十六进制数字。最终,你将用你的帐户进行交易,此时可以表名你是所有者。交易的达成或只是与他人的一般活动是因为你拥有这些资产。
这些地址是如何产生的?
不同的区块链使用不同的寻址方案,但通常它们是使用椭圆曲线数字签名算法(ECDSA)生成的。该算法生成的私钥/公钥对只能用于签名,不能用于加密。ECDSA 是以太坊使用的算法。
在 Ardan 项目中,我在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 项目使用这些文件来维护不同测试帐户的私钥。你将在该文件夹中看到我和 Pavel 的文件。
片段 4:kennedy.ecdsa
9f332e3700d8fc2446eaf6d15034cf96e0c2745e40353deef032a5dbf1dfed93
片段4 显示了我的私有 ECDSA 密钥的十六进制编码版本,它代表我在 Ardan 区块链上的帐户。Ardan 数字钱包使用该密钥代表我对 Ardan 区块链执行活动。你很快就会意识到数字钱包只不过是一个应用程序,它可以使用私钥作为你的帐户身份对区块链节点进行网络调用。
注意:不同的区块链使用不同的网络协议。例如,以太坊使用JSON-RPC。
以太坊项目有一个加密包,为使用 ECDSA 提供支持。Ardan 项目使用这个包来管理数字签名。
片段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 个单词的助记词来生成的。此助记词代表你帐户的私钥,可用于离线存储你的私钥(纸质钱包)或在你的机器坏了或如果你想从不同的机器(移动设备、新计算机等)使用你的帐户时恢复你的帐户。
如果有人找到你的助记词,那么他们就拥有你的私钥,并且可以配置钱包应用程序将资金从你的帐户中转出。无论如何,永远不要与任何你不信任的人分享这个助记词。如果你需要共享助记词,那么一种解决方案是使用诸如Shamir secrets之类的系统将助记词分发给共享的对等组。像这样的系统将允许在紧急情况下(你的意外死亡)恢复你的私钥,避免没有人能控制你的帐户。
通过私钥,你可以生成相应的公钥。公钥代表您帐户的身份
片段 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
包从公钥生成地址。此地址代表你的帐户在区块链上的身份。如果有人知道这个地址,他们可以看到你拥有的东西以及你在区块链上的所有交易(发送和接收)。
例如,我有一个帐户,其地址是0x01D398ECb403BE33Cd6ED8c9Fefa1712Be48d8d8
我在以太坊区块链上使用的。使用此地址,你可以查看我的所有交易。
图 1:Etherscan
图 1 显示了我在以太坊上的帐户的 etherscan你可以看到我是如何从我的 Coinbase 账户购买以太币的,然后是我为在以太坊名称服务 ( ENS ) 上购买名称而执行的交易。由于地址很容易输入错误,因此创建 ENS 以充当地址的 DNS 查找。
图 2:wkennedy.eth
图 2 显示了我的 ENS 名称wkennedy.eth解析为与我的帐户关联的地址。
交易类型
要向 Ardan 区块链提交交易,需要发送特定信息。
片段 5:用户事务类型
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 }
此类型提供有关谁在获得资金、他们获得多少以及与区块链节点关联的帐户将收到多少小费(奖金)以成功将此交易存储在一个块中的信息。该Data
字段允许将任何额外信息与交易相关联。
请注意,此类型缺少一个字段来标识正在提交交易的帐户。这是因为提交交易的账户必须通过签署交易来表明自己的身份。
片段 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 区块链提交交易时需要提供这种类型的值。
签署交易
钱包如何给UserTx
签名以便提交到区块链?它首先将用户事务散列成一个 32 字节的切片。
片段 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 }
该函数返回一个 32 字节的散列,代表用户交易,其中嵌入了 Ardan 标记到最终散列中。此最终哈希用于创建签名、公钥提取和签名验证。
在第 02 行,接收器值被编组为字节切片,然后通过第 07 行的哈希函数运行,以生成表示编组的事务数据的 32 字节数组。在第 08 行,Ardan 标记被转换为字节切片,因此它可以与第 09 行的交易数据的散列相结合,以生成用于表示交易的 32 个字节的最终散列。
Ardan 印章用于确保签署交易时产生的签名对于 Ardan 区块链始终是唯一的。以太坊也使用相同的格式来做到这一点。
清单 8:区块链印章
Ethereum Stamp Format
\x19Ethereum Signed Message:\n + length(message) + message
Ardan Stamp
"\x19Ardan Signed Message:\n32" + dataHash
注意:以太坊将此称为签名,而不是印章。我觉得这很令人困惑,因为这个字符串被用来对要签名的散列交易数据加盐将其视为印章对我来说不那么令人困惑。
通过将封送处理数据的散列长度强制为 32 字节,可以将长度硬编码到标记 ( \n32
) 中以简化操作。以太坊使用了这个技巧。
使用该HashWithArdanStamp
方法,现在可以签署交易并准备提交到区块链。
片段 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 字节的切片,使用 ECDSA 格式 [R | S | V]。前 32 个字节代表 R 值,接下来的 32 个字节代表 S 值,最后一个字节代表 V 值。
注意:如果您想了解有关 R、S 和 V 值的更多信息,请阅读这篇出色的文章。
以太坊将签名作为 R、S 和 V 存储在它们不同的交易类型中,我决定效仿。
代码 10:签名字节到值
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 上添加一个任意数字。这样做是为了清楚地表明签名来自他们的区块链。以太坊和比特币使用的任意数字是 27。对于 Ardan 区块链,我决定使用 29。重要的是要注意,在签名可以用于任何加密操作之前,需要减去这个任意数字。
代码 11:签名值到字节
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
函数从 V中删除ardanID
,因此该值返回 0 或 1。该toSignatureBytesForDisplay
函数保留V 中的ardanID
并用于显示。
签名到地址
现在您已经知道如何签署交易,接下来您需要了解区块链节点如何使用签名来提取公钥来识别提交交易的账户地址。
代码 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 }
该方法由区块链节点执行以检索签署交易的账户地址。
在第 09 行,该HashWithArdanStamp
方法用于重新创建用于生成接收到的签名的散列。该数据需要完全相同,否则区块链节点将确定错误的公钥。
现在有了公钥,可以在第 25 行使用crypto
包中的PubkeyToAddress
函数来提取提交并签署交易的帐户的地址。
验证签名
最后一步是区块链节点验证签名的能力。
代码 13:验证签名
01 func (tx SignedTx) VerifySignature() error {
. . . CHECKS ARE HERE . . .
36 }
执行 3 次检查以验证随交易数据提供的签名是否代表正确的帐户。
代码 14:检查恢复 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 显示了执行的第一个检查,确保恢复 id 设置为 ardan id。如果这个签名不是由我们之前的Sign
函数产生的,那么减去 ardan id 就不会产生 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 显示了第二个检查,以验证整个签名是否有效。这是通过crypto
包中的ValidateSignatureValues
功能完成的。该函数接受带有单独 R、S 和 V 值的签名。该函数的最后一个参数是知道签名是否是以太坊 Homestead 版本的一部分。Homestead 是以太坊平台的第二个主要版本,也是以太坊的第一个生产版本。
代码 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
函数,因为我需要将公钥作为 33 字节的未压缩切片用于下一次调用。
代码 17:检查公钥创建的数据签名
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 显示了要执行的下一次调用和最后一次检查。该函数需要未压缩的 33 字节公钥、散列和标记的交易数据,以及签名中的 R 和 S 值。此验证对于确保正确的帐户与交易相关联至关重要。
当区块链节点接收到该SignedTx
值时,它并不真正知道与该UserTx
部分值关联的字段是否与创建签名时相同。如果不是,则将生成不同的公钥,因此将使用不同的帐户。从某人的帐户中取出钱是多么完美的黑客行为。
UserTx
重新散列值很重要。现在,可以使用公钥、重新散列UserTx
的值以及签名的 R 和 S 值来验证一切是否同步。如果VerifySignature
函数没有失败,则可以确定该值中提供的签名SignedTx
是由该UserTx
值生成的,因此公钥确实代表了正确的帐户。