区块链:第一部分:数字账户、签名和验证

原文链接: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

image.png

image.png

图 1 显示了我在以太坊上的帐户的 etherscan你可以看到我是如何从我的 Coinbase 账户购买以太币的,然后是我为在以太坊名称服务 ( ENS ) 上购买名称而执行的交易。由于地址很容易输入错误,因此创建 ENS 以充当地址的 DNS 查找。

图 2:wkennedy.eth

image.png

图 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值生成的,因此公钥确实代表了正确的帐户。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,843评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,538评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,187评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,264评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,289评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,231评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,116评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,945评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,367评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,581评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,754评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,458评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,068评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,692评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,842评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,797评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,654评论 2 354

推荐阅读更多精彩内容