比特币交易
比特币使用的非对称加密算法是ECC(椭圆曲线算法)
1. 比特币地址的生成流程
-
私钥生成公钥
-
对公钥做两次哈希,sha256,ripemd160(生成160bit)的哈希值==>公钥哈希
-
将比特币网络的版本号和公钥哈希拼接到一起:21字节bytes
-
对这21字节做两次sha256哈希运算,对结果去前四字节,得到校验码
-
将21字节与4字节的checksum拼接到一起,得到25字节数据
-
做base58编码
与base64相同,但是字符集去掉了6个容易混淆的字符
0和O, I(大写i)和l(小写L), +/(把这6个字符去掉,避免混淆)
2. 总金额
- 传统
- 使用数据库,里面建立某人的数据库,星星,总额字段,每次交易时,动态修改这个字段。查询余额时,只需要读取这个字段的数据。
- 比特币
- 没有一个特定数据库存储某个地址的总额
- 比特币的数据库,写的就是交易,所有的信息都来自于交易本身
- 比特币没有总额的概念,它会遍历整个账本,找到每一笔交易中,属于某个地址的钱,累加起来,作为总额
3. 转账
-
对比
- 传统
- 数据库字段的改写,A->B转账100元,A的字段-100,B的字段+100
- 比特币
- 区块链不允许修改数据,没有特定字段。
- 比特币使用找零机制完成转账
- 如果转账金额大于单笔交易存在的额度,那么需要多笔交易完成这一次的转账(想转120,凑了两笔:100,20)
- 如果有找零,需要把找零的钱转给自己(明确的转给自己,否则这个找零就变成了手续费)
- 传统
-
输入输出
- 输入:input
- 这笔钱的来源
- 输入可以有多个
- 输出:output
- 表明了这笔钱的去向
- 可以有多个输出
- 输入:input
-
UTXO:未花费的交易输出,Unspent Transaction Output
-
一个地址的总额,就是散落在整个账本中的utxo的集合
- 转账演示
- 交易步骤
- 申请一个新的地址,发起转账交易
- 拷贝交易id,使用getTransaction<交易id>得到hex值
- decoderawtransaction得到交易详情
每笔交易都会追根溯源,认为这笔钱从源头来说是没有问题的,才会产生这笔交易。所以说是安全的。
校验
-
解锁脚本ScripSig:
- 存在input中,每一个交易可以有多个input,每一个input必须包含一个解锁脚本
- 解锁脚本包含了交易发起人使用自己的私钥对这笔交易的签名
- (所谓解锁脚本:就是将私钥签名和公钥带入到一个公式中,公式1)
- 创建交易时,只是提供了解锁的数据,还没有执行解锁(真正能否解锁,由矿工校验后确定)
-
锁定脚本:
- 使用收款人的地址进行锁定一笔钱,只有持有私钥的人才能解开
- (所谓锁定脚本:就是使用公钥哈希值带入到一个公式中,公式2)
-
创建好交易后,tx1,广播到网络
-
矿工:拿到tx1,会解出公式1和公式2
- 公式1+公式2,校验true还是false
- true:交易有效
- false:交易无效
- 公式1+公式2,校验true还是false
交易输入(TXInput)
指明交易发起人可支付资金的来源,包含:
- 引用utxo所在交易的ID(知道在哪个房间)
- 所消费utxo在output中的索引(具体位置)
- 解锁脚本(签名、公钥)
交易输出(TXOutput)
包含资金接收方的相关信息,包含:
- 接收金额(数字)
- 锁定脚本(对方公钥的哈希,这个哈希可以通过地址反推出来)
交易结构
- 交易id
- 交易输出input,它由历史中的某个output转换而来
- 所引用的交易id
- 索引
- 解锁脚本
- 交易输出output,它表明钱的流向
- 锁定脚本(用收款人的地址,会反推出公钥哈希)
- 转账金额
- 时间戳
4. 将交易内容添加到代码
4.1 定义交易结构
package main
// 定义交易结构
type Transaction struct {
TXID []byte // 交易id
TXInputs []TXInput // 可以有多个输入
TXOutputs []TXOutput // 可以有多个输出
TimeStamp uint64 // 创建交易的时间
}
type TXInput struct {
Txid []byte // 这个input所引用的output所在的交易id
Index uint64 // 这个input所引用的output在交易中的索引
ScriptSig string // 付款人对当前交易(新交易,而不是引用的交易)的签名
}
type TXOutput struct {
ScriptPubk string // 收款人的公钥哈希值,先理解为地址
Value float64
}
4.2 获取交易ID
对交易做哈希处理
// 获取交易ID
// 对交易做哈希
func (tx *Transaction) setHash() error {
// 对tx做gob编码得到字节流,做sha256,赋值给TXID
var buffer bytes.Buffer
encoder := gob.NewEncoder(&buffer)
err := encoder.Encode(tx)
if err != nil {
fmt.Println("encoder err:", err)
return err
}
hash := sha256.Sum256(buffer.Bytes())
// 使用tx字节流的哈希值作为交易id
tx.TXID = hash[:]
return nil
}
4.3 创建挖矿交易
input比较特殊
// 创建挖矿交易
func NewCoinbaseTx(miner /*挖矿人*/ string, data string) *Transaction {
//TODO
// 特点:没有输入,只有一个输出,得到挖矿奖励
// 挖矿交易需要能够识别出来,没有input,所以不需要签名
// 挖矿交易不需要签名,所以这个签名字段可以书写任意值,只有矿工有权利写
// 中本聪:写的创世语
// 现在都是由矿池来写,写自己矿池的名字
input := TXInput{
Txid: nil,
Index: -1,
ScriptSig: data,
}
output := TXOutput{
Value: reward,
ScriptPubk: miner,
}
timeStamp := time.Now().Unix()
tx := Transaction{
TXID: nil,
TXInputs: []TXInput{input},
TXOutputs: []TXOutput{output},
TimeStamp: uint64(timeStamp),
}
tx.setHash()
return &tx
}
4.4 使用Transaction改写程序
将block中的data字段改写为transaction字段
type Block struct {
// 版本号
Version uint64
// 前区块哈希
PrevHash []byte
// 交易的根哈希值
MerkleRoot []byte
// 时间戳
TimeStamp uint64
// 难度值,系统提供一个数据,用于计算出一个哈希值
Bits uint64
// 随机数,挖矿要求的数值
Nonce uint64
// 哈希,为了方便,把当前区块的哈希放入block中
Hash []byte
// 数据
// Data []byte
// 一个区块可以有很多个交易(交易的集合)
Transactions []*Transaction
}
4.5 获取挖矿人金额
-
先判断这个output是不是某人的的
-
否:对比下一个output
-
是:每次添加前,需要查看后面的交易的input里面是否把当前对的output消耗了
-
-
每个交易遍历完outputs都要遍历交易的inputs,查看是否有当前遍历地址的inputs
流程:
- 先遍历output,找到和李四相关的,直接统计(后面没有人使用)
- 遍历一下inputs,找到和李四有关的,放入一个容器中map1
- 继续向前遍历,找和李四有关的output
- 如果和李四无关,直接跳过
- 如果和李司有关,那么不能直接统计,需要和map1逐个对比
- 如果发现交易id和索引完全相同,说明这个output被消耗过了,直接过滤不统计了
- 继续遍历当前交易的inputs,看看是否和自己(李四)有关,是:则加入map1;否:跳过
4.6 遍历交易
// 获取指定地址的金额,实现遍历账本的通用函数
// 给定一个地址,返回所有的utxo
func (bc *BlockChain) FindMyUTXO(address string) []TXOutput {
// 存储所有和目标地址相关的utxo集合
var utxos []TXOutput
it := bc.NewIterator()
for {
// 遍历区块
block := it.Next()
// 遍历交易
for _, tx := range block.Transactions {
// 遍历output,判断这个output的锁定脚本是否为我们的目标地址
for outputIndex, output := range tx.TXOutputs {
fmt.Print("outputIndex:", outputIndex)
if output.ScriptPubk == address {
// 找到属于目标地址的output
utxos = append(utxos, output)
}
}
}
if len(block.PrevHash) == 0 {
break
}
}
return utxos
}
4.7 获取余额
在commandline中添加获取余额函数
func (cli *CLI) getBalance (address string) {
bc, err := GetBlockChainInstance()
if err != nil {
fmt.Println("GetBlockChainInstance error:", err)
return
}
// 获取所有的相关的utxo集合
utxos := bc.FindMyUTXO(address)
total := 0.0
for _, utxo := range utxos {
total += utxo.Value
}
fmt.Printf("'%s'的金额为:%f\n", address, total)
}
4.8 遍历inputs
for _, input := range tx.TXInputs {
if input.ScriptSig == address {
// map[key:交易id][value:[]int(下标)]
// map[string][]int{
// 0x333:{0, 1}
// }
spentKey := string(input.Txid)
// 向篮子中添加已经消耗的output
spentUtxos[spentKey] = append(spentUtxos[spentKey], int(input.Index))
// map[0x333] = []int{0}
// map[0x333] = []int{0, 1}
// map[0x222] = []int{0}
}
}
4.7 创建普通交易
- from 付款人, to 收款人 , amout 输入参数
- 遍历账本,找到from满足条件utxo集合,返回这些utxo包含的总金额
- 如果金额不足,创建交易失败
- 拼接inputs
- 遍历inputs集合,每一个output都要转换为一个input
- 拼接outputs
- 创建一个属于to的output
- 如果总金额大于需要转账的金额,进行找零:给from创建一个output
- 设置哈希,返回
// 创建普通交易
//1. 遍历账本,找到from满足条件utxo集合,返回这些utxo包含的总金额
func NewTransaction(from, to string, amount float64, bc *BlockChain) *Transaction {
// 包含所有将要使用的utxo集合
var spentUTXO = make(map[string][]int64)
// 这些使用utxo包含总金额
var retValue float64
//TODO
// 遍历账本,找到from能够使用utxo集合,以及这些utxo包含的钱
spentUTXO, retValue = bc.findNeedUTXO(from, amount)
//map[0x222] = []int{0}
//map[0x333] = []int{0, 1}
//2. 如果金额不足,创建交易失败
if retValue < amount {
fmt.Println("金额不足,创建交易失败!")
return nil
}
var inputs []TXInput
var outpus []TXOutput
//3. 拼接inputs
//- 遍历inputs集合,每一个output都要转换为一个input
for txid, indexArray := range spentUTXO {
// 遍历下标,注意value才是我们消耗的output的下标
for _, i := range indexArray {
input := TXInput{[]byte(txid), i, from}
inputs = append(inputs, input)
}
}
//4. 拼接outputs
//- 创建一个属于to的output
output1 := TXOutput{to, amount}
outpus = append(outpus, output1)
//- 如果总金额大于需要转账的金额,进行找零:给from创建一个output
if retValue > amount {
output2 := TXOutput{from, retValue - amount}
outpus = append(outpus, output2)
}
timeStamp := time.Now().Unix()
//5. 设置哈希,返回
tx := Transaction{nil, inputs, outpus, timeStamp}
return &tx
}
4.8 转账
写一个send命令
// from,to,amount
// 每次send时,都会添加一个区块
// 创建挖矿交易,创建普通交易
// 执行addBlock
func (cli *CLI) send(from, to string, amount float64, miner, data string) {
fmt.Println("from:", from)
fmt.Println("to:", to)
fmt.Println("amount:", amount)
fmt.Println("miner:", miner)
fmt.Println("data:", data)
//TODO,需要关闭bc.db
bc, err := GetBlockChainInstance()
if err != nil {
fmt.Println("send err:", err)
return
}
// 每次send时,都会添加一个区块
// 创建挖矿交易,创建普通交易
// 执行addBlock
// 创建挖矿交易
coinbaseTx := NewCoinbaseTx(miner, data)
// 创建txs数组,将有效地交易添加进来
txs := []*Transaction{coinbaseTx}
// 创建普通交易
tx := NewTransaction(from, to, amount, bc)
if tx != nil {
fmt.Println("找到一笔有效的转账交易!")
txs = append(txs, tx)
} else {
fmt.Println("注意,找到一笔无效的转账交易,不添加到区块!")
}
// 调用AddBlock
err = bc.AddBlock(txs)
if err != nil {
fmt.Println("添加区块失败,转账失败!")
}
fmt.Println("添加区块成功,转账成功!")
}
5. 钱包
- 定义钱包结构并提供生成函数
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"fmt"
"github.com/btcsuite/btcutil/base58"
"golang.org/x/crypto/ripemd160"
)
// - 结构定义
type wallet struct {
//私钥
PriKey *ecdsa.PrivateKey
//公钥原型定义
// type PublicKey struct {
// elliptic.Curve
// X, Y *big.Int
// }
// 公钥, X,Y类型一致,长度一致,我们将X和Y拼接成字节流,赋值给pubKey字段,用于传输
// 验证时,将X,Y截取出来(类似r,s),再创建一条曲线,就可以还原公钥,进一步进行校验
PubKey []byte
}
// - ==创建秘钥对==
func newWalletKeyPair() *wallet {
curve := elliptic.P256()
//创建私钥
priKey, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
fmt.Println("ecdsa.GenerateKey err:", err)
return nil
}
//获取公钥
pubKeyRaw := priKey.PublicKey
//将公钥X,Y拼接到一起
pubKey := append(pubKeyRaw.X.Bytes(), pubKeyRaw.Y.Bytes()...)
//创建wallet结构返回
wallet := wallet{priKey, pubKey}
return &wallet
}
// - ==根据私钥生成地址==
func (w *wallet) getAddress() string {
//公钥
pubKey := w.PubKey
hash1 := sha256.Sum256(pubKey)
//hash160处理
hasher := ripemd160.New()
hasher.Write(hash1[:])
//公钥哈希,锁定output时就是使用这值
pubKeyHash := hasher.Sum(nil)
//拼接version和公钥哈希,得到21字节的数据
payload := append([]byte{byte(0x00)}, pubKeyHash...)
//生成4字节的校验码
first := sha256.Sum256(payload)
second := sha256.Sum256(first[:])
//4字节checksum
checksum := second[0:4]
//25字节数据
payload = append(payload, checksum...)
address := base58.Encode(payload)
return address
}
6. 签名
- 签名需要内容
- 私钥
- 签名内容的哈希值
===> 得到数字签名
- 校验
- 公钥
- 校验内容的哈希值
- 签名
===>校验结果:true/false
具体签名细节
- 每一个input都要有一个签名
- 签名是对当前交易的签名0x555
- 签名的交易需要包含哪些数据?
- 每一个输出的value
- 每一个输出的公钥哈希(收款人)
- input引用的output的公钥哈希(付款人)
// 实现具体签名动作(copy,设置为空,签名动作)
// 参数1:私钥
// 参数2:inputs所引用的output所在交易的集合
// key:交易id
// value:交易本身
func (tx *Transaction) sign(priKey *ecdsa.PrivateKey, prevTxs map[string]*Transaction) bool {
fmt.Println("具体对交易签名sign...")
// 挖矿交易不需要签名
if tx.isCoinbaseTx() {
fmt.Println("找到挖矿交易,无需签名!")
return true
}
// 1. 获取交易copy,pubKey,ScriptSig字段置空
txCopy := tx.trimmedCopy()
// 2. 遍历交易的inputs for
for i, input := range txCopy.TXInputs {
fmt.Printf("开始对input[%d]进行签名...", i)
prevTxs := prevTxs[string(input.Txid)]
if prevTxs == nil {
return false
}
// input引用的output
output := prevTxs.TXOutputs[input.Index]
// 获取引用的output的公钥哈希
// 这里的input是副本,不会影响到遍历的结构
//input.PubKey = output.ScriptPubKeyHash
txCopy.TXInputs[i].PubKey = output.ScriptPubKeyHash
// 对copy交易进行签名,需要得到交易的哈希值
txCopy.setHash()
// 将input的pubKey字段设为nil,还原数据,防止干扰后面input的签名
txCopy.TXInputs[i].PubKey = nil
hashData := txCopy.TXID // 我们去签名的具体数据
// 开始签名
r, s, err := ecdsa.Sign(rand.Reader, priKey, hashData)
if err != nil {
fmt.Println("签名失败!")
return false
}
signature := append(r.Bytes(), s.Bytes()...)
// 将数字签名赋值给原始tx
tx.TXInputs[i].ScriptSig = signature
}
//TODO
fmt.Println("交易签名成功!")
return true
}
// trim修剪,签名和校验时都会使用
func (tx *Transaction) trimmedCopy() *Transaction {
var inputs []TXInput
var outputs []TXOutput
// 创建一个交易副本,每一个input的pubKey和Sig都设置为空
for _, input := range tx.TXInputs {
input := TXInput{
Txid:input.Txid,
Index:input.Index,
ScriptSig:nil,
PubKey:nil,
}
inputs = append(inputs, input)
}
outputs = tx.TXOutputs
txCopy := Transaction{tx.TXID, inputs, outputs, tx.TimeStamp}
return &txCopy
}
校验:
// 校验单笔交易
func (bc *BlockChain) verifyTransaction(tx *Transaction) bool {
fmt.Println("verifyTransaction 开始了...")
// 根据传递进来的tx得到所有需要的前交易prevTxs
prevTxs := make(map[string]*Transaction)
//map[0x222]=tx1
//map[0x333]=tx2
//TODO 遍历账本,找到所有的交易集合
for _, input := range tx.TXInputs {
prevTx /*这个input引用的交易*/ := bc.findTransaction(input.Txid)
if prevTxs == nil {
fmt.Println("没有找到有效的引用交易!")
return false
}
fmt.Println("找到了引用交易!")
// 容易出现的错误:tx.TXID
prevTxs[string(input.Txid)] = prevTx
}
return tx.verify(prevTxs)
}
// 具体校验
func (tx *Transaction) verify(prevTxs map[string]*Transaction) bool {
//TODO
// 1. 获取交易副本txCopy
txCopy := tx.trimmedCopy()
// 2. 遍历交易,inputs
for i, input := range tx.TXInputs {
prevTx := prevTxs[string(input.Txid)]
if prevTx == nil {
return false
}
// 3. 还原数据(得到引用output的公钥哈希)获取交易的哈希值
output := prevTx.TXOutputs[input.Index]
txCopy.TXInputs[i].PubKey = output.ScriptPubKeyHash
txCopy.setHash()
// 具体还原的签名数据
hashData := txCopy.TXID
// 签名
signature := input.ScriptSig
// 公钥的字节流
pubKey := input.PubKey
// 开始校验
var r, s, x, y big.Int
// r,s 从signature截取出来
r.SetBytes(signature[:len(signature)/2])
s.SetBytes(signature[len(signature)/2:])
// x,y 从pubKey截取出来,还原为公钥本身
x.SetBytes(signature[:len(pubKey)/2])
y.SetBytes(signature[len(pubKey)/2:])
curve := elliptic.P256()
pubKeyRaw := ecdsa.PublicKey{Curve:curve, X:&x, Y:&y}
// 进行校验
res := ecdsa.Verify(&pubKeyRaw, hashData, &r, &s)
if !res {
fmt.Println("发现校验失败的交易!")
return false
}
}
// 4. 通过tx.ScriptSig, tx.PubKey进行校验
fmt.Println("交易校验成功!")
return true
}
-
测试命令
-
./build.sh
-
./blockchain create 12aeWh7pKwCAfFY34KuGzVPCro5FszLmRo
-
./blockchain send 12aeWh7pKwCAfFY34KuGzVPCro5FszLmRo 186y63BjCdhMDcVDgaVY8yS1JjGbqC9vKX 2.5 18xDdzRhwTddLuXPixKNbZidowRGSxDT4 "hello"