主页 > imtoken在线官网 > 1、比特币交易和传统银行交易有什么区别?

1、比特币交易和传统银行交易有什么区别?

imtoken在线官网 2023-07-16 05:18:36

一、比特币交易介绍 1、比特币交易与传统银行交易有什么区别?

传统银行的每个账户都有一个数据库来保存用户信息,比如姓名、卡号、余额等信息。 每产生一笔交易,银行系统都会更新用户账户的余额字段。

比特币的数据库只有交易,没有用户账户信息。 那么比特币系统是如何维护用户的比特币的呢?

image-20181207221818512

与银行系统不同,比特币系统中的每笔交易都基于之前的交易。 用户的比特币分散在不同的交易中。 如果想知道某个钱包地址的余额,需要遍历整个账本,统计属于该地址的交易。

2.变更机制

比如张三钱包里有100个比特币,给李四转了80个比特币。 相当于张三钱包里有一张100元的钞票,给李四转了80元。 那么剩下的20块钱怎么办呢?

答案是比特币系统会扣掉李四的钱,然后把剩下的转给自己。 这就是比特币的变化机制。

image-20180902175804340

三、交接单

比特币系统中没有付款人和收款人,只有输入和输出。 每一个输入对应一个之前别人给你转账时产生的输出。

(1) 多对一转账(众筹)

image-20180902175832201

(2) 一对多转账(红包)

image-20180902175927530

(3) 多对多传输

image-20181012111755026

4. 费用

大多数交易都包含交易费(矿工费),这是为保护网络安全而给予比特币矿工的一种补偿形式。 费用本身也充当一种安全机制,使攻击者在经济上不利地用交易淹没网络。

交易费用是根据交易数据的大小计算的,而不是比特币交易的价值。

交易数据结构没有交易费用字段。 相反比特币的数据库在哪里比特币的数据库在哪里,交易费用是指输入和输出之间的差额。 从所有输入中扣除所有输出后的多余部分由矿工收取作为汽油费。

计算公式:交易手续费=所有输入-所有输出

5. 比特币交易结构

一笔比特币交易由两部分组成:输入和输出。

(1) 交易输出

比如张三给李四转账,比特币系统会产生一个输出,里面包含两个东西:“转账金额”和“锁定脚本”。

转账金额:比特币数量

Locking script:李四的公钥哈希(可以理解为用李四的公钥加密,只有李四的私钥才能解密)

(2) 交易输入

与输出对应的是输入,每个输入都来自一个输出。 比如李四给王五转账,系统会创建一个输入,包括:

引用交易的ID:在哪笔交易中,需要从张三->李四转账的交易ID

引用事务的 OUTPUT 的索引:

解锁脚本:张三的签名和公钥。 签名用于验证钱是我的,公钥用于查找引用的输出。

注:挖矿奖励不需要参考任何产出,相当于有一个特殊的投入。

完整的验证脚本如下图所示:

image-20180908174756528

6. 未消费输出(UTXO)

这就涉及到输出消耗的问题。 对于未使用的输出,我们有一个特殊的名称,称为未花费的交易输出(UTXO)。 UTXO是比特币交易中最小的支付单位,不可分割。 每一个 UTXO 都必须被一次性消耗掉,然后产生一个新的 UTXO 并存储在比特币网络的 UTXO 池中。

“用户的比特币余额”的概念是通过比特币钱包应用程序创建的衍生产品。 比特币钱包通过扫描区块链并汇总属于该用户的所有 UTXO 来计算用户的余额。

二、继续完善功能 1、增加交易

(1) 定义交易结构

type TXInput struct {
	TXID        []byte // 引用输出的交易ID
	OutputIndex int64  // 引用输出的索引
	//ScriptSig   string // 解锁脚本(实际上这里应该是签名和公钥)
	Signature []byte // 签名
	PubKey    []byte // 公钥
}
type TXOutput struct {
	Value float64 // 金额
	//ScriptPubKey string  // 锁定脚本(公钥哈希)
	PubKeyHash []byte // 公钥哈希
}
type Transaction struct {
	TXID    []byte     // 交易ID
	Inputs  []TXInput  // 交易输入
	Outputs []TXOutput // 交易输出
}

(2) 设置交易ID

// 设置交易ID
func (tx *Transaction) SetHash() {
	// 对tx进行加密处理,生成字节流
	var buffer bytes.Buffer
	encoder := gob.NewEncoder(&buffer)
	encoder.Encode(&tx)
	// 使用sha256对字节流再次假面,生成一个hash值
	data := buffer.Bytes()
	hash := sha256.Sum256(data)
	// 把hash作为交易的ID
	tx.TXID = hash[:]
}

(3) 创建CoinBase挖矿交易

const reward = 50 // 挖矿奖励
// 创建挖矿交易
// 参数一:矿工地址
// 参数二:矿工附加信息
func NewCoinBaseTx(address string, data string) *Transaction {
	input := TXInput{nil, -1, data}
	output := TXOutput{reward, address}
	tx := Transaction{nil, []TXInput{input}, []TXOutput{output}}
	tx.SetHash()
	return &tx
}

(4)重写Block结构,使用交易数组代替Data。

// 定义区块
type Block struct {
	// 版本号
	Version uint64
	// 前区块哈希值
	PrevHash []byte
	// 梅克尔根
	MerKleRoot []byte
	// 时间戳
	TimeStamp uint64
	// 难度值
	Difficulty uint64
	//随机数
	Nonce uint64
	// 当前区块哈希值
	Hash []byte
	// 区块数据
	//Data []byte
	Data []*Transaction // 区块的交易数据
}

(5)重写NewBlock函数

// 创建方法
func NewBlock(data []*Transaction, prevHash []byte) *Block {
	block := Block{
		Version:    00,
		PrevHash:    prevHash,
		MerKleRoot: []byte{},
		TimeStamp:  uint64(time.Now().Unix()),
		Difficulty: 1,
		Nonce:      1,
		//Data: []byte(data),
		Data: data,
	}
	//提供一个设置哈希的方法
	//block.SetHash()
	pow := NewProofOfWork(block)
	hash, nonce := pow.Run()
	block.Hash = hash
	block.Nonce = nonce
	return &block
}

(6)重写NewBlockChain函数,添加address参数,调用NewCoinBase创建和添加挖矿交易。

// 创建方法
//func NewBlockChain() *BlockChain {
func NewBlockChain(address string) *BlockChain {
	/*// 创建创世块
	genericBlock := NewBlock([]byte(genesisInfo), []byte{})
	// 创建BlockChain
	bc := BlockChain{[]*Block{genericBlock}}
	return &bc*/
	var db *bolt.DB
	var lastHash []byte
	// 1. 打开数据库
	db, err := bolt.Open(dbName, 0600, nil)
	if err != nil {
		panic("bolt.Open err!")
	}
	db.Update(func(tx *bolt.Tx) error {
		// 2. 打开抽屉Bucket
		bucket := tx.Bucket([]byte(bucketName))
		// 3. 如果Bucket是否为nil,则创建一个Bucket
		if bucket == nil {
			bucket, err = tx.CreateBucket([]byte(bucketName))
			if err != nil {
				panic("tx.CreateBucket err!")
			}
			// 创建创世块
			//genericBlock := NewBlock([]byte(genesisInfo), []byte{})
			//创建coinbase交易  <---这里修改
			coinbaseTx := NewCoinBaseTx(address, genesisInfo)
			//创建创世块 <---这里修改
			genericBlock := NewBlock([]*Transaction{coinbaseTx}, []byte{})
			// 把创世块保存在bucket中
			bucket.Put(genericBlock.Hash, genericBlock.Serialize())
			// 把创世块的hash保存在last中
			bucket.Put([]byte(last), genericBlock.Hash)
			// 记录lastHash
			lastHash = genericBlock.Hash
		} else {
			// 4. 如果Bucket不为nil,记录lastHash
			lastHash = bucket.Get([]byte(last))
		}
		return nil
	})
	return &BlockChain{db, lastHash}
}

(7)重写AddBlock函数,将参数data改为txs。

// 添加区块
//func (bc *BlockChain) AddBlock(data string) {
func (bc *BlockChain) AddBlock(txs []*Transaction) {
	/*// 获取最后区块
	lastBlock := bc.Blocks[len(bc.Blocks)-1]
	// 创建一个新区块
	block := NewBlock([]byte(data), lastBlock.Hash)
	// 添加新区块
	bc.Blocks = append(bc.Blocks, block)*/
	// 获取最后区块hash
	lastHash := bc.Tail
	// 创建区块
	//block := NewBlock([]byte(data), lastHash)
	block := NewBlock(txs, lastHash)
	// 更新操作
	bc.Db.Update(func(tx *bolt.Tx) error {
		bucket := tx.Bucket([]byte(bucketName))
		if bucket == nil {
			panic("bucket should not be nil!")
		}
		// 向bolt数据库添加新区块
		bucket.Put(block.Hash, block.Serialize())
		// 更新数据库的last
		bucket.Put([]byte(last), block.Hash)
		// 更新bc.Tail
		bc.Tail = block.Hash
		return nil
	})
}

(8)修改PrepareData函数,注释掉block.Data。

// 根据nonce生成区块哈希,该方法与SetHash方法类似
func (pow *ProofOfWork) PrepareData(nonce uint64) []byte {
	block := pow.Block
	block.MerKleRoot = block.HashTransactions()
	tmp := [][]byte{
		block.PrevHash,
		//block.Data,
		block.MerKleRoot,
		uint64ToByte(block.Version),
		uint64ToByte(block.TimeStamp),
		uint64ToByte(block.Difficulty),
		uint64ToByte(nonce),
	}
	blockInfo := bytes.Join(tmp, []byte{})
	// 2.使用sha256加密
	hash := sha256.Sum256(blockInfo)
	return hash[:]
}

(9) 添加HashTransaction函数

该函数是生成默克尔树根哈希值。 正常的生成过程是利用所有交易的哈希值生成一棵平衡二叉树。 这里,为了简化代码,我们目前直接拼接区块中交易的哈希值 然后进行哈希运算。

// 对区块中所有交易ID进行哈希运算,作为梅克尔根
func (block *Block)HashTransactions() []byte {
	var tmp [][]byte
	for _, tx := range block.Data {
		//交易的ID就是交易的哈希值,还记得吗?我们在Transaction里面提供了方法。
		tmp = append(tmp, tx.TXID)
	}
	data := bytes.Join(tmp, []byte{})
	hash := sha256.Sum256(data)
	return hash[:]
}

(10)修改命令行

注释掉 addBlock 函数。

/*// 添加区块
func (cli *Cli) addBlock(data string) {
	cli.bc.AddBlock(data)
}*/

将“block addBlock --data DATA”的内容放在常量Usage中。

const Usage  = `
   block printChain "打印区块链" 
   block getBalance --address ADDRESS "查询账户余额"
   block send FROM TO AMOUNT MINER DATA "由FROM给TO转AMOUNT个比特币,并指定MINER为矿工"
   block newWallet "创建一个钱包"
   block listAddress "列出所有钱包地址"
`

修改run函数,注释掉“addblock”情况下的代码。

/*case "addBlock":
	if len(os.Args) == 4 && os.Args[2] == "--data" {
		data := os.Args[3]
		if data == "" {
			fmt.Println("data should not be empty!")
			return
		}
	cli.addBlock(data)
}*/

修改main函数,调用NewBlockChain函数时传入矿工地址。

func main() {
	bc := NewBlockChain("张三")
	cli := Cli{bc}
	cli.Run()
}

2.查询余额

比特币通过转账在系统中流通,付款人的钱包会使用付款人的解锁脚本解锁可控的UTXO,完成消费。 同时,通过收款人地址锁定支付金额,完成收款,从而实现金额的转移。

(1)解锁脚本

解锁脚本是检查输入是否可以使用某个地址锁定的utxo。 所以对于解锁脚本,外部提供了锁定信息,我去看看能不能解锁。

func (input *TXInput) CanUnlockUTXOWith(unlockData string /*收款人的地址*/) bool {
    //ScriptSig是签名,v4版本中使用付款人的地址填充
    return input.ScriptSig == unlockData
}

(2) 锁定脚本

锁定脚本用于指定比特币的新所有者,并在创建输出时创建。 对于这个输出,它一直在等待签名的到来,以检查签名是否可以解锁锁定的比特币。

func (output *TXOutput) CanBeUnlockedWith(unlockData string/*付款人的地址(签名)*/) bool {
    //ScriptPubKey是锁定信息,v4版本中使用收款人的地址填充
    return output.ScriptPubKey == unlockData
}

(3) 找出UTXO所在的交易集合

所有未花费的比特币都在 UTXO 中,而 UTXO 又包含在交易中。 因此,要想获得一次性的UTXO,就必须找到这些UTXO所在的交易。 如果每个人都有N个UTXO,那么我们需要找到所有的交易,也就是要找到包含UTXO的交易集合。

// 查询所有UTXO交易
// 参数:账户地址
func (bc *BlockChain) FindUTXOTransaction(address string) []Transaction {
	var txs []Transaction
	spentUTXOs := make(map[string][]int64) // 已消费的UTXO, Key代表交易地址,Value为引用output的索引
	// 遍历所有区块
	itr := bc.NewIterator()
	for {
		block := itr.Prev()
		// 遍历交易
		for _, tx := range block.Data {
			// 遍历output
		OUTPUT_TAG:
			for i, output := range tx.Outputs {
				if spentUTXOs[string(tx.TXID)] != nil {
					// 获取消费过的utxo的索引
					indexs := spentUTXOs[string(tx.TXID)]
					// 循环比较当前output的索引是否在indexs中存在,如果存在就代表该output已经被消费
					for _, index := range indexs {
						if index == int64(i) {
							continue OUTPUT_TAG
						}
					}
				}
				// 如果output的ScriptPubKey等于address,代表该output是属于adddres指定的账户
				if output.ScriptPubKey == address {
					txs = append(txs, *tx)
				}
			}
			// 遍历input
			if !tx.isCoinBase() {
				for _, input := range tx.Inputs {
					// 如果input的ScriptSig等于address,就代表该input是属于指定address的账户
					if input.ScriptSig == address {
						spentUTXOs[string(input.TXID)] = append(spentUTXOs[string(input.TXID)], int64(input.OutputIndex))
					}
				}
			}
		}
		if (len(block.PrevHash) == 0) {
			return txs
		}
	}
}

(4)定义isCoinBase函数

// 判断当前交易是否是挖矿交易
func (tx *Transaction) isCoinBase() bool {
	return len(tx.Inputs) == 1 && tx.Inputs[0].TXID == nil && tx.Inputs[0].OutputIndex == -1
}

(5) 获取指定地址的UTXO集合

// 查找所有的utxo
func (bc *BlockChain) FindUTXOs(address string) []TXOutput {
	txs := bc.FindUTXOTransaction(address)
	var outputs []TXOutput
	// 遍历所有utxos交易
	for _, tx := range txs {
		// 遍历utxo交易的所有output
		for _, output := range tx.Outputs {
			// 如果output的ScriptPubKey等于address,就代表该output是我们要找到output
			if output.ScriptPubKey == address {
				outputs = append(outputs, output)
			}
		}
	}
	return outputs
}

(6) 添加GetBalance函数获取余额。

// 查询余额
func (cli *Cli) GetBalance(address string) {
	// 查询所有未消费的output
	utxos := cli.bc.FindUTXOs(address)
	var total float64
	for _, output := range utxos {
		total += output.Value
	}
	fmt.Printf("%s的余额: %f\n", address, total)
}

(7)修改Run函数,增加getBalance的case判断。

func (cli *Cli) Run() {
	args := os.Args
	if len(args) < 2 {
		fmt.Println(Usage)
		return
	}
	// 获取命令名称
	command := args[1]
	switch command {
		case "printChain":
			cli.printChain()
		case "getBalance":
			if len(os.Args) == 4 && os.Args[2] == "--address" {
				address := os.Args[3]
				if len(address) == 0 {
					fmt.Println("地址不能为空!")
					return
				}
				cli.GetBalance(address)
			} else {
				fmt.Println(Usage)
			}
		default:
			fmt.Println(Usage)
	}
}

3. 交易转账

(1) 创建交易

// 创建交易
func NewTransaction(from, to string, amount float64, bc *BlockChain) *Transaction {
	// 1.找到最优的utxos
	utxos, total := bc.FindNeedUTXOs(from, amount)
	// 2.检查余额是否足够
	if total < amount {
		fmt.Println("余额不足!")
		return nil
	}
	// 3.如果余额足够,那么创建新的区块
	var inputs []TXInput
	var outputs []TXOutput
	for txId, outputIndexs := range utxos {
		for _, outputIndex := range outputIndexs {
			input := TXInput{[]byte(txId), outputIndex, from}
			inputs = append(inputs, input)
		}
	}
	output := TXOutput{amount, to}
	outputs = append(outputs, output)
	// 找零
	if total > amount {
		output = TXOutput{total - amount, from}
		outputs = append(outputs, output)
	}
	// 4.创建Transaction
	tx := Transaction{nil, inputs, outputs}
	tx.SetHash()
	return &tx
}

(2)定义FindNeedUTXOs函数

// 查找最合理的utxo
func (bc *BlockChain) FindNeedUTXOs(address string, amount float64) (map[string][]int64, float64) {
	txs := bc.FindUTXOTransaction(address)
	needUTXOs := make(map[string][]int64) // 保存最合理的utxo
	var total float64                     // 最合理utxo的总金额
OUTPUT_TAG:
	for _, tx := range txs {
		for i, output := range tx.Outputs {
			if output.ScriptPubKey == address {
				if total < amount {
					total += output.Value
					needUTXOs[string(tx.TXID)] = append(needUTXOs[string(tx.TXID)], int64(i))
				} else {
					break OUTPUT_TAG
				}
			}
		}
	}
	return needUTXOs, total
}

(3) 添加发送命令

// 转账方法
func (cli *Cli) Send(from, to string, amount float64, miner string, data string) {
	// 创建交易
	tx := NewTransaction(from, to, amount, cli.bc)
	// 创建挖矿交易
	coinbase := NewCoinBaseTx(miner, data)
	// 添加区块
	cli.bc.AddBlock([]*Transaction{coinbase, tx})
	fmt.Println("转账成功...")
	// 打印余额
	cli.GetBalance(from)
}

(4)修改Run方法,增加case发送条件判断。

func (cli *Cli) Run() {
	args := os.Args
	if len(args) < 2 {
		fmt.Println(Usage)
		return
	}
	// 获取命令名称
	command := args[1]
	switch command {
        ...
        ...
		case  "send":
			args := os.Args
			if len(args) < 7 {
				fmt.Println("参数不正确!")
				fmt.Println(Usage)
				return
			}
			//block send FROM TO AMOUNT MINER DATA "由FROM给TO转AMOUNT个比特币,并指定MINER为矿工"
			from := args[2]
			to := args[3]
			num, _ := strconv.ParseFloat(args[4], 16)
			miner := args[5]
			data := args[6]
			cli.Send(from, to , num, miner, data)
		default:
			fmt.Println(Usage)
	}
}