交易流程

比特币交易

比特币使用的非对称加密算法是ECC(椭圆曲线算法)

1. 比特币地址的生成流程

  1. 私钥生成公钥

  2. 对公钥做两次哈希,sha256,ripemd160(生成160bit)的哈希值==>公钥哈希

  3. 将比特币网络的版本号和公钥哈希拼接到一起:21字节bytes

  4. 对这21字节做两次sha256哈希运算,对结果去前四字节,得到校验码

  5. 将21字节与4字节的checksum拼接到一起,得到25字节数据

  6. 做base58编码

    与base64相同,但是字符集去掉了6个容易混淆的字符

    0和O, I(大写i)和l(小写L), +/(把这6个字符去掉,避免混淆)

2. 总金额

  • 传统
    • 使用数据库,里面建立某人的数据库,星星,总额字段,每次交易时,动态修改这个字段。查询余额时,只需要读取这个字段的数据。
  • 比特币
    • 没有一个特定数据库存储某个地址的总额
    • 比特币的数据库,写的就是交易,所有的信息都来自于交易本身
  • 比特币没有总额的概念,它会遍历整个账本,找到每一笔交易中,属于某个地址的钱,累加起来,作为总额

3. 转账

  • 对比

    • 传统
      • 数据库字段的改写,A->B转账100元,A的字段-100,B的字段+100
    • 比特币
      • 区块链不允许修改数据,没有特定字段。
      • 比特币使用找零机制完成转账
        • 如果转账金额大于单笔交易存在的额度,那么需要多笔交易完成这一次的转账(想转120,凑了两笔:100,20)
        • 如果有找零,需要把找零的钱转给自己(明确的转给自己,否则这个找零就变成了手续费)
  • 输入输出

    • 输入:input
      • 这笔钱的来源
      • 输入可以有多个
    • 输出:output
      • 表明了这笔钱的去向
      • 可以有多个输出
  • UTXO:未花费的交易输出,Unspent Transaction Output

  • 一个地址的总额,就是散落在整个账本中的utxo的集合

在这里插入图片描述

  • 转账演示

在这里插入图片描述

  • 交易步骤
  1. 申请一个新的地址,发起转账交易
  2. 拷贝交易id,使用getTransaction<交易id>得到hex值
  3. decoderawtransaction得到交易详情

每笔交易都会追根溯源,认为这笔钱从源头来说是没有问题的,才会产生这笔交易。所以说是安全的。

校验

  • 解锁脚本ScripSig:

    • 存在input中,每一个交易可以有多个input,每一个input必须包含一个解锁脚本
    • 解锁脚本包含了交易发起人使用自己的私钥对这笔交易的签名
    • (所谓解锁脚本:就是将私钥签名和公钥带入到一个公式中,公式1)
    • 创建交易时,只是提供了解锁的数据,还没有执行解锁(真正能否解锁,由矿工校验后确定)
  • 锁定脚本:

    • 使用收款人的地址进行锁定一笔钱,只有持有私钥的人才能解开
    • (所谓锁定脚本:就是使用公钥哈希值带入到一个公式中,公式2)
  • 创建好交易后,tx1,广播到网络

  • 矿工:拿到tx1,会解出公式1和公式2

    • 公式1+公式2,校验true还是false
      • true:交易有效
      • false:交易无效

在这里插入图片描述

交易输入(TXInput)

指明交易发起人可支付资金的来源,包含:

  • 引用utxo所在交易的ID(知道在哪个房间)
  • 所消费utxo在output中的索引(具体位置)
  • 解锁脚本(签名、公钥)

交易输出(TXOutput)

包含资金接收方的相关信息,包含:

  • 接收金额(数字)
  • 锁定脚本(对方公钥的哈希,这个哈希可以通过地址反推出来)

交易结构

  1. 交易id
  2. 交易输出input,它由历史中的某个output转换而来
    1. 所引用的交易id
    2. 索引
    3. 解锁脚本
  3. 交易输出output,它表明钱的流向
    1. 锁定脚本(用收款人的地址,会反推出公钥哈希)
    2. 转账金额
  4. 时间戳

在这里插入图片描述

在这里插入图片描述

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 获取挖矿人金额

  1. 先判断这个output是不是某人的的

    • 否:对比下一个output

    • 是:每次添加前,需要查看后面的交易的input里面是否把当前对的output消耗了

  2. 每个交易遍历完outputs都要遍历交易的inputs,查看是否有当前遍历地址的inputs

流程:

  1. 先遍历output,找到和李四相关的,直接统计(后面没有人使用)
  2. 遍历一下inputs,找到和李四有关的,放入一个容器中map1
  3. 继续向前遍历,找和李四有关的output
  4. 如果和李四无关,直接跳过
  5. 如果和李司有关,那么不能直接统计,需要和map1逐个对比
    • 如果发现交易id和索引完全相同,说明这个output被消耗过了,直接过滤不统计了
  6. 继续遍历当前交易的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 创建普通交易

  1. from 付款人, to 收款人 , amout 输入参数
  2. 遍历账本,找到from满足条件utxo集合,返回这些utxo包含的总金额
  3. 如果金额不足,创建交易失败
  4. 拼接inputs
    • 遍历inputs集合,每一个output都要转换为一个input
  5. 拼接outputs
    • 创建一个属于to的output
    • 如果总金额大于需要转账的金额,进行找零:给from创建一个output
  6. 设置哈希,返回
// 创建普通交易
//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. 钱包

  1. 定义钱包结构并提供生成函数
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

具体签名细节

  1. 每一个input都要有一个签名
  2. 签名是对当前交易的签名0x555
  3. 签名的交易需要包含哪些数据?
    1. 每一个输出的value
    2. 每一个输出的公钥哈希(收款人)
    3. 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"

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值