1.前言
上一章我们实现了交易,到目前为止,我们已经使用任意用户定义的字符串作为地址,比特币的账户地址其实就是用户公钥经过一系列 Hash ( HASH 160,或先进行 SHA256, 然后进行 RIPEMD160 )及编码运算后生成的 160 位( 20 字节)的字符串。
2.知识点
知识点 | 学习网页 | 特性 |
---|---|---|
Public-key | Public-key cryptography | 一对加密密钥与解密密钥 |
Digital signatures | 数字签名、快速理解签名 | 1、 可靠性:签名接受者可以验证签名是否为签名者所签 2、 不可伪造:只有签名者可以生成自己独有的签名 3、 不可重复使用:签名文件包含的信息不会对其他文档进行签名 4、 不可否认性:签名者在任何时候不能否认自己的签名 |
Elliptic curve | 椭圆曲线 | 下面有介绍 |
Address | 比特币地址 | 见下面的图 |
Base58 | base58 | 去掉0(零),O(大写o),I(大写i),l(小写L),因为它们看起来相似。此外,没有+和/符号。 |
3.重要概念与知识理解
比特币地址:
比特币采用了非对称的加密算法,用户自己保留私钥,对自己发出的交易进行签名确 认,并公开公钥。 比特币的账户地址其实就是用户公钥经过一系列 Hash ( HASH 160,或先进行 SHA256, 然后进行 RIPEMD160 )及编码运算后生成的 160 位( 20 字节)的字符串。 一般地,对账户地址串进行 Base58Check 编码,并添加前导字节(表明支持哪种脚本) 和 4 字节校验字节,以提高可读性和准确性。
账户并非直接是公钥内容,而是 Hash 后的值, 以避免公钥过早公开后导致被破解出 私钥 。
公钥加密算法:
公钥加密算法使用成对的密钥:公钥和私钥。公钥是不敏感的,可以向任何人公开。与此相反,私钥不应该被公开:除了所有者之外,没有人可以访问它们,因为它是作为所有者标识符的私钥。你是你的私钥(当然,在密码货币的世界里)。
从本质上讲,比特币钱包就是一对这样的密钥。当您安装一个钱包应用程序或使用一个比特币客户端来生成一个新地址时,将为您生成一对密钥。控制私钥的人控制着所有比特币的比特币。
私钥和公钥只是随机的字节序列,因此它们不能在屏幕上打印并由人读取。这就是为什么比特币使用一种算法将公共密钥转换成人类可读的字符串。
如果您曾经使用过比特币钱包应用程序,那么很可能会为您生成一个助记密码短语。这样的短语代替了私钥,可以用来生成它们。该机制在BIP-039中实现。
好了,现在我们知道了什么是比特币的用户。但是,比特币如何检查交易输出的所有权(以及储存在它们上面的信息)?
数字签名:
在数学和密码学中,有一个数字签名的概念——算法保证:
- 这些数据在从发送者传输到接收方的过程中没有被修改。
- 这些数据是由某个发送者创建的。
- 发送方不能否认发送数据。
签名的操作会产生一个签名,它存储在事务输入中。为了验证签名,需要以下操作:
- 数据签名。
- 签名。
- 公共密钥。
简单地说,验证过程可以被描述为:检查这个签名是从这个数据中获得的,它带有一个用于生成公钥的私钥。
数字签名不是加密,你不能从签名中重建数据。这类似于哈希:你通过一个哈希算法来运行数据,并获得数据的唯一表示。签名和散列的区别是密钥对:它们使签名验证成为可能。
但是密钥对也可以用于加密数据:用于加密的私钥,公钥用于对数据进行解密。不过,比特币并不使用加密算法。
每个比特币的交易输入都由创建该交易的人签名。比特币的每一笔交易都必须经过验证,才能被放入一个区块。验证意味着(除了其他程序):
- 检查输入是否允许使用以前事务的输出。
- 检查事务签名是否正确。
从图表上看,签名数据和验证签名的过程是这样的:
现在让我们回顾一下事务的完整生命周期:
- 在开始的时候,有一个包含coinbase事务的创世纪块。coinbase的交易没有真正的输入,因此没有必要进行签名。coinbase事务的输出包含一个散列公钥。(算法使用:RIPEMD16(SHA256(PubKey))
- 当一个人发送硬币时,就会创建一个事务。事务的输入将引用先前事务(s)的输出。每个输入都将存储一个公钥(非散列)和整个事务的签名。
- 接受交易的比特币网络中的其他节点将验证它。除了其他东西,他们还会检查:输入的公钥的散列与所引用的输出的散列匹配(这确保发送者只花费属于他们的硬币);签名是正确的(这确保了该事务是由真正的硬币所有者创建的)。
- 当一个矿工节点准备挖掘一个新的块时,它将把事务放入一个块中并开始挖掘它。
- 当阻塞被挖掘时,网络中的每一个节点都会收到一条消息,说这个块被挖掘了,并将这个块添加到块链块中。
- 区块链被添加到区块链后,交易完成,它的输出可以在新交易中被引用。
椭圆曲线密码学(Elliptic Curve Cryptography):
如上所述,公钥和私钥是随机字节的序列。由于它是用于识别硬币所有者的私钥,所以有一个必要条件:随机性算法必须产生真正的随机字节。我们不希望意外地生成由其他人拥有的私有密钥。
比特币使用椭圆曲线来生成私钥。椭圆曲线是一个复杂的数学概念,我们不打算在这里详细解释(如果你很好奇,看看这个this gentle introduction to elliptic curves:数学公式!)我们需要知道的是这些曲线可以用来产生非常大的随机数。比特币使用的曲线可以在0到2之间随机选择一个数字(大约10个,当在可见的宇宙中有10到10个原子)。如此巨大的上限意味着几乎不可能两次生成相同的私钥。
此外,比特币使用(我们将会)ECDSA(椭圆曲线数字签名算法)算法来签署交易。
Base58:
现在让我们回到上面提到的比特币地址:1 a1zp1ep5qgefi2dmptftl5slmv7divfna。现在我们知道这是一个公共密钥的人类可读的表示。如果我们对它进行解码,这就是公钥的样子(在十六进制系统中写入一个字节序列):
0062E907B15CBF27D5425399EBF6F0FB50EBB88F18C29B7D93
比特币使用Base58算法将公共密钥转换为人类可读的格式。该算法与著名的Base64非常相似,但它使用更短的字母:一些字母从字母表中去掉,以避免使用字母相似的攻击。因此,没有这些符号:0(零),O(大写的O),I(大写I)L(小写L),因为它们看起来很相似。同样,也没有+和/符号。
让我们从一个公共密钥的角度来设想一个地址的过程:
因此,上面提到的解码公钥包括三个部分:
Version Public key hash Checksum
00 62E907B15CBF27D5425399EBF6F0FB50EBB88F18 C29B7D93
因为哈希函数是一种方法(即它们不能被逆转),不可能从散列中提取公钥。但是,我们可以检查是否使用了公钥来获取散列,以便运行它认为save散列函数并比较散列。
好了上面的算法很多比较难理解,但知道了算法后帮助我们实现代码思路更清晰。
4.地址代码实现
Wallet:
const version = byte(0x00) //16进制0 版本号
const walletFile = "db/wallet.dat"
const addressChecksumLen = 4 //地址检查长度4
// 钱包
type Wallet struct {
/**
PrivateKey: ECDSA基于椭圆曲线
使用曲线生成私钥,并从私钥生成公钥
*/
PrivateKey ecdsa.PrivateKey //私钥
PublicKey []byte //公钥
}
// 创建一个新钱包
func NewWallet() *Wallet {
//公钥私钥生成
private, public := newKeyPair()
wallet := Wallet{private, public}
return &wallet
}
// 得到一个钱包地址
func (w Wallet) GetAddress() []byte {
pubKeyHash := HashPubKey(w.PublicKey)
//将版本号+pubKeyHash得到一个散列
versionedPayload := append([]byte{version}, pubKeyHash...)
//校验前4个字节的散列
checksum := checksum(versionedPayload)
//将校验和附加到version+PubKeyHash组合。
fullPayload := append(versionedPayload, checksum...)
//BASE58得到一个钱包地址
address := utils.Base58Encode(fullPayload)
return address
}
// 使用RIPEMD160(SHA256(PubKey))哈希算法得到hsahpubkey
func HashPubKey(pubKey []byte) []byte {
publicSHA256 := sha256.Sum256(pubKey)
RIPEMD160Hasher := ripemd160.New()
_, err := RIPEMD160Hasher.Write(publicSHA256[:])
if err != nil {
log.Panic(err)
}
publicRIPEMD160 := RIPEMD160Hasher.Sum(nil)
return publicRIPEMD160
}
// 校验地址
func ValidateAddress(address string) bool {
pubKeyHash := utils.Base58Decode([]byte(address))
actualChecksum := pubKeyHash[len(pubKeyHash)-addressChecksumLen:]
version := pubKeyHash[0]
pubKeyHash = pubKeyHash[1 : len(pubKeyHash)-addressChecksumLen]
targetChecksum := checksum(append([]byte{version}, pubKeyHash...))
return bytes.Compare(actualChecksum, targetChecksum) == 0
}
//SHA256(SHA256(payload))算法返回前4个字节
func checksum(payload []byte) []byte {
firstSHA := sha256.Sum256(payload)
secondSHA := sha256.Sum256(firstSHA[:])
return secondSHA[:addressChecksumLen]
}
//椭圆算法返回私钥与公钥
func newKeyPair() (ecdsa.PrivateKey, []byte) {
curve := elliptic.P256()
//获取私钥
private, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
log.Panic(err)
}
//在基于椭圆曲线的算法中,公钥是曲线上的点。因此,公钥是X,Y坐标的组合。在比特币中,这些坐标被连接起来形成一个公钥。
pubKey := append(private.PublicKey.X.Bytes(), private.PublicKey.Y.Bytes()...)
return *private, pubKey
}
以下是将公钥转换为Base58地址的步骤:
- 取公钥并用RIPEMD160(SHA256(PubKey))哈希算法对它进行两次散列。
- 将地址生成算法的版本添加到散列。
- 用步骤2的结果散列来计算校验和SHA256(SHA256(payload))。校验和是结果散列的前四个字节。
- 将校验和附加到version+PubKeyHash组合。
- version+PubKeyHash+checksum使用Base58 编码组合。
Wallets:
//钱包集合
type Wallets struct {
Wallets map[string]*Wallet //map集合
}
func NewWallets() (*Wallets, error) {
wallets := Wallets{}
wallets.Wallets = make(map[string]*Wallet)
err := wallets.LoadFromFile()
return &wallets, err
}
// 创建一个钱包
func (ws *Wallets) CreateWallet() string {
wallet := NewWallet()
address := fmt.Sprintf("%s", wallet.GetAddress())
ws.Wallets[address] = wallet
return address
}
// 迭代所有钱包地址返回到数组中
func (ws *Wallets) GetAddresses() []string {
var addresses []string
for address := range ws.Wallets {
addresses = append(addresses, address)
}
return addresses
}
// 获取并返回一个钱包地址
func (ws Wallets) GetWallet(address string) Wallet {
return *ws.Wallets[address]
}
// 加载钱包
func (ws *Wallets) LoadFromFile() error {
if _, err := os.Stat(walletFile); os.IsNotExist(err) {
return err
}
fileContent, err := ioutil.ReadFile(walletFile)
if err != nil {
log.Panic(err)
}
var wallets Wallets
gob.Register(elliptic.P256())
decoder := gob.NewDecoder(bytes.NewReader(fileContent))
err = decoder.Decode(&wallets)
if err != nil {
log.Panic(err)
}
ws.Wallets = wallets.Wallets
return nil
}
// 保存钱包
func (ws Wallets) SaveToFile() {
var content bytes.Buffer
gob.Register(elliptic.P256())
encoder := gob.NewEncoder(&content)
err := encoder.Encode(ws)
if err != nil {
log.Panic(err)
}
err = ioutil.WriteFile(walletFile, content.Bytes(), 0644)
if err != nil {
log.Panic(err)
}
}
将生成的钱包地址保存到文件中。
交易输入:
//输入事物
type TXInput struct {
Txid []byte //事物hash
Vout int //输出值
Signature []byte //签名
PubKey []byte //公钥
}
//检查输入是否使用特定的键来解锁输出
func (in *TXInput) UsesKey(pubKeyHash []byte) bool {
lockingHash := wallet.HashPubKey(in.PubKey)
return bytes.Compare(lockingHash, pubKeyHash) == 0
}
交易输出:
//一个事物输出
type TXOutput struct {
Value int //值
PubKeyHash []byte //解锁脚本key
}
// Lock只需锁定输出
func (out *TXOutput) Lock(address []byte) {
pubKeyHash := utils.Base58Decode(address)
pubKeyHash = pubKeyHash[1 : len(pubKeyHash)-4]
out.PubKeyHash = pubKeyHash
}
// 检查提供的公钥散列是否用于锁定输出
func (out *TXOutput) IsLockedWithKey(pubKeyHash []byte) bool {
return bytes.Compare(out.PubKeyHash, pubKeyHash) == 0
}
// 新的交易输出
func NewTXOutput(value int, address string) *TXOutput {
txo := &TXOutput{value, nil}
txo.Lock([]byte(address))
return txo
}
请注意,我们不再使用ScriptPubKey和ScriptSig领域,因为我们不打算执行的脚本语言。相反,ScriptSig被分成Signature和PubKey字段,并被ScriptPubKey重命名为PubKeyHash。我们将像比特币一样实现相同的输出锁定/解锁和输入签名逻辑,但是我们会在方法中执行此操作。
该UsesKey方法检查输入是否使用特定的键来解锁输出。请注意,输入存储原始公钥(即,不散列),但该函数需要散列一个。IsLockedWithKey检查提供的公钥散列是否用于锁定输出。这是一个补充功能UsesKey,并且它们都用于在FindUnspentTransactions事务之间建立连接。
Lock只需锁定输出。当我们向别人发送硬币时,我们只知道他们的地址,因此函数将地址作为唯一的参数。然后解码该地址,并从中提取公钥哈希并保存在该PubKeyHash字段中。
我们现在创建一个钱包地址:
修改cli
//创建钱包
func (cli *CLI) createWallet() {
wallets, _ := wallet.NewWallets()
address := wallets.CreateWallet()
wallets.SaveToFile()
fmt.Printf("Your new address: %s\n", address)
}
执行编译命令、执行创建钱包命令:
C:\go-worke\src\github.com\study-bitcoin-go>go build github.com/study-bitcoin-go
C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go createwallet
Your new address: 13qBhEZ9edWk7Kr4mDM4Shs9qVVVfG3XrB
C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go createblockchain -address 13qBhEZ9edWk7Kr4mDM4Shs9qVVVfG3XrB
Mining the block containing "�╔4���tȢ�l�+�酝���.3
�"�Z\/��"
Dig into mine 00000d35a6c93bfc05cefc690822a4e083cf5922d0f398f6fe975abf01530da0
Done!
C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go getbalance -address 13qBhEZ9edWk7Kr4mDM4Shs9qVVVfG3XrB
Balance of '13qBhEZ9edWk7Kr4mDM4Shs9qVVVfG3XrB': 10
5.实现签名
交易必须签署,因为这是比特币唯一能够保证不能花钱购买属于他人的硬币的方法。如果签名无效,交易也被视为无效,因此无法添加到区块链。
除了一件事:签署的数据之外,我们已经完成了所有的事务签名。交易的哪些部分实际签署了?或者一项交易是整体签署的?选择要签名的数据非常重要。问题是要签名的数据必须包含以独特方式标识数据的信息。例如,仅对输出值进行签名是没有意义的,因为此签名不会考虑发件人和收件人。
考虑到事务解锁先前的输出,重新分配其值并锁定新的输出,必须对以下数据进行签名:
- 公钥哈希存储在解锁输出中。这标识了交易的“发件人”。
- 公钥哈希存储在新的锁定输出中。这标识了交易的“收件人”。
- 新产出的价值。
在比特币,锁定/解锁逻辑被存储在脚本,被存储在ScriptSig和ScriptPubKey的分别输入和输出,字段。由于比特币允许不同类型的这种脚本,因此它签署了整个内容ScriptPubKey。
正如你所看到的,我们不需要签名存储在输入中的公钥。正因为如此,在比特币中,这不是一个已签署的交易,而是其修剪后的副本,其输入存储ScriptPubKey在参考输出中。
这里描述获取修剪后的交易副本的详细过程。它很可能已经过时,但我没有设法找到更可靠的信息来源。
好吧,它看起来很复杂,所以让我们开始编码吧。我们将从Sign方法开始:
func (tx *Transaction) Sign(privKey ecdsa.PrivateKey, prevTXs map[string]Transaction) {
if tx.IsCoinbase() {
return
}
txCopy := tx.TrimmedCopy()
for inID, vin := range txCopy.Vin {
prevTx := prevTXs[hex.EncodeToString(vin.Txid)]
txCopy.Vin[inID].Signature = nil
txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash
txCopy.ID = txCopy.Hash()
txCopy.Vin[inID].PubKey = nil
r, s, err := ecdsa.Sign(rand.Reader, &privKey, txCopy.ID)
signature := append(r.Bytes(), s.Bytes()...)
tx.Vin[inID].Signature = signature
}
}
该方法采用私钥和先前事务的映射。如上所述,为了签署交易,我们需要访问交易输入中引用的输出,因此我们需要存储这些输出的交易。
让我们一步一步回顾这个方法:
if tx.IsCoinbase() {
return
}
Coinbase交易没有签名,因为它们没有真正的输入。
txCopy := tx.TrimmedCopy()
剪裁的副本将被签名,而不是完整的交易:
func (tx *Transaction) TrimmedCopy() Transaction {
var inputs []TXInput
var outputs []TXOutput
for _, vin := range tx.Vin {
inputs = append(inputs, TXInput{vin.Txid, vin.Vout, nil, nil})
}
for _, vout := range tx.Vout {
outputs = append(outputs, TXOutput{vout.Value, vout.PubKeyHash})
}
txCopy := Transaction{tx.ID, inputs, outputs}
return txCopy
}
该副本将包括所有的输入和输出,但TXInput.Signature并TXInput.PubKey设置为零。
接下来,我们遍历副本中的每个输入:
for inID, vin := range txCopy.Vin {
prevTx := prevTXs[hex.EncodeToString(vin.Txid)]
txCopy.Vin[inID].Signature = nil
txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash
在每个输入中,Signature被设置为nil(只是一个双重检查)并被PubKey设置为PubKeyHash参考输出。在这一刻,所有的交易,但目前的一个是“空”的,即它们的Signature和PubKey字段设置为零。因此,输入是分开签署的,虽然这对于我们的应用程序不是必需的,但比特币允许交易包含引用不同地址的输入。
txCopy.ID = txCopy.Hash()
txCopy.Vin[inID].PubKey = nil
该Hash方法将事务序列化并使用SHA-256算法对其进行散列处理。结果散列是我们要签署的数据。得到散列后,我们应该重置该PubKey字段,所以它不会影响进一步的迭代。
现在,中心部分:
r, s, err := ecdsa.Sign(rand.Reader, &privKey, txCopy.ID)
signature := append(r.Bytes(), s.Bytes()...)
tx.Vin[inID].Signature = signature
我们签txCopy.ID有privKey。ECDSA签名是一对数字,我们连接并存储在输入Signature字段中。
现在,验证功能:
// 验证方法
func (tx *Transaction) Verify(prevTXs map[string]Transaction) bool {
if tx.IsCoinbase() {
return true
}
for _, vin := range tx.Vin {
if prevTXs[hex.EncodeToString(vin.Txid)].ID == nil {
log.Panic("ERROR: Previous transaction is not correct")
}
}
txCopy := tx.TrimmedCopy()
curve := elliptic.P256()
for inID, vin := range tx.Vin {
prevTx := prevTXs[hex.EncodeToString(vin.Txid)]
txCopy.Vin[inID].Signature = nil
txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash
txCopy.ID = txCopy.Hash()
txCopy.Vin[inID].PubKey = nil
r := big.Int{}
s := big.Int{}
sigLen := len(vin.Signature)
r.SetBytes(vin.Signature[:(sigLen / 2)])
s.SetBytes(vin.Signature[(sigLen / 2):])
x := big.Int{}
y := big.Int{}
keyLen := len(vin.PubKey)
x.SetBytes(vin.PubKey[:(keyLen / 2)])
y.SetBytes(vin.PubKey[(keyLen / 2):])
rawPubKey := ecdsa.PublicKey{curve, &x, &y}
if ecdsa.Verify(&rawPubKey, txCopy.ID, &r, &s) == false {
return false
}
}
return true
}
该方法非常简单。首先,我们需要相同的交易副本:
txCopy := tx.TrimmedCopy()
接下来,我们将需要用于生成密钥对的相同曲线:
for inID, vin := range tx.Vin {
prevTx := prevTXs[hex.EncodeToString(vin.Txid)]
txCopy.Vin[inID].Signature = nil
txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash
txCopy.ID = txCopy.Hash()
txCopy.Vin[inID].PubKey = nil
这部分与Sign方法中的相同,因为在验证过程中我们需要签署相同的数据。
r := big.Int{}
s := big.Int{}
sigLen := len(vin.Signature)
r.SetBytes(vin.Signature[:(sigLen / 2)])
s.SetBytes(vin.Signature[(sigLen / 2):])
x := big.Int{}
y := big.Int{}
keyLen := len(vin.PubKey)
x.SetBytes(vin.PubKey[:(keyLen / 2)])
y.SetBytes(vin.PubKey[(keyLen / 2):])
在这里,我们解压存储在TXInput.Signature和中的值TXInput.PubKey,因为签名是一对数字,公钥是一对坐标。我们将它们连接在一起进行存储,现在我们需要解压它们以用于crypto/ecdsa功能。
rawPubKey := ecdsa.PublicKey{curve, &x, &y}
if ecdsa.Verify(&rawPubKey, txCopy.ID, &r, &s) == false {
return false
}
}
return true
这里是:我们创建一个ecdsa.PublicKey使用从输入中提取的公钥并执行ecdsa.Verify传递从输入中提取的签名。如果所有输入都已验证,则返回true; 如果至少有一个输入未通过验证,则返回false。
现在,我们需要一个函数来获取以前的事务。由于这需要与区块链互动,我们将使其成为一种方法Blockchain:
func (bc *Blockchain) FindTransaction(ID []byte) (Transaction, error) {
bci := bc.Iterator()
for {
block := bci.Next()
for _, tx := range block.Transactions {
if bytes.Compare(tx.ID, ID) == 0 {
return *tx, nil
}
}
if len(block.PrevBlockHash) == 0 {
break
}
}
return Transaction{}, errors.New("Transaction is not found")
}
func (bc *Blockchain) SignTransaction(tx *Transaction, privKey ecdsa.PrivateKey) {
prevTXs := make(map[string]Transaction)
for _, vin := range tx.Vin {
prevTX, err := bc.FindTransaction(vin.Txid)
prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX
}
tx.Sign(privKey, prevTXs)
}
func (bc *Blockchain) VerifyTransaction(tx *Transaction) bool {
prevTXs := make(map[string]Transaction)
for _, vin := range tx.Vin {
prevTX, err := bc.FindTransaction(vin.Txid)
prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX
}
return tx.Verify(prevTXs)
}
这些功能很简单:FindTransaction按ID查找事务(这需要遍历区块链中的所有区块); SignTransaction采取交易,找到它引用的交易并签名; VerifyTransaction做同样的事情,而是验证交易。
现在,我们需要实际签署和验证交易。签署发生在NewUTXOTransaction:
func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction {
...
tx := Transaction{nil, inputs, outputs}
tx.ID = tx.Hash()
bc.SignTransaction(&tx, wallet.PrivateKey)
return &tx
}
验证发生在交易被放入块之前:
func (bc *Blockchain) MineBlock(transactions []*Transaction) {
var lastHash []byte
for _, tx := range transactions {
if bc.VerifyTransaction(tx) != true {
log.Panic("ERROR: Invalid transaction")
}
}
...
}
实现以下签名后的命令:
C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go send -from 13qBhEZ9edWk7Kr4mDM4Shs9qVVVfG3XrB -to 1BB5hnQVMqiQ2JC6fGU8a3gVkRNMzKVgnX -amount 1
Mining the block containing "��C����siP �~PA8�����G�╝�m羖"
Dig into mine 00000082b57010bdd217b837f7ecae20a20e65f7435ee03ee06d38a2e67598e9
Success!
6.总结
我们到现在实现了地址、签名等功能。后续我们将继续完成挖矿矿工费,以及网络等。本章涉及的一些算法笔记多,需要花时间去了解这些算法。
资料
- 原文来源:https://jeiwan.cc/posts/building-blockchain-in-go-part-5/
- 本文源码:https://github.com/Even521/study-bitcion-go/tree/part5
- java学习:https://www.jianshu.com/p/66c065018c7a
- 区块链基础视频学习:https://www.bilibili.com/video/av19620321/
- 区块链测试demo:https://anders.com/blockchain/blockchain.html
- 区块链QQ交流群:489512556