# go-blockchain-learning **Repository Path**: wujian2023/go-blockchain-learning ## Basic Information - **Project Name**: go-blockchain-learning - **Description**: 区块链比特币实现 - **Primary Language**: Go - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-02-22 - **Last Updated**: 2023-05-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 比特币原理及其代码实践1-区块链数据结构 [Jeiwan/blockchain_go: A simplified blockchain implementation in Golang (github.com)](https://github.com/Jeiwan/blockchain_go) https://gitee.com/Oracion/go-blockchain-learning ## 引言: **区块链**本质只是一个`分布式数据库`而已。 区块链具有`去中心化(全节点保留所有区块)`、`匿名(address(公私所生成的)和我本人没了联系)`、`可追溯(UTXO)`、`不可更改(merkleTree)`的特点 那么这些特点是如何实现的呢?? 如果从**原理**和**代码**的角度,相信大家可以更好的理解这些特点。 ### 区块链的开山之作,比特币 [中本聪](https://baike.baidu.com/item/中本聪/5740822?fromModule=lemma_inlink)在2008年11月1日提出,并于2009年1月3日正式诞生,区块链也因此进入了人们的视野 ### 区块链的层次结构 ![区块链层次结构](https://gitee.com/Oracion/typora_images/raw/master/blockchain/20210516153901777_1.jpg) 我们将实现最下面的三层 ## 区块链结构 如果要我们自己设计`区块链的数据结构`,应该如何设计: - 指向前一个区块链的hash值 - 自己的hash值 - 时间戳 - 数据(交易信息) ![image-20230222104609842](https://gitee.com/Oracion/typora_images/raw/master/blockchain/image-20230222104609842.png) ### 数据结构-区块 ``` //区块 type Block struct { Timestamp int64 //时间戳 Data []byte //数据 PrevBlockHash []byte //前一个区块的hash Hash []byte //自己的hash } ``` 其中,时间戳、前一个hash、自己的hash就被叫做区块的`header` 真实的比特币的区块比这个会再复杂一些,会多版本、挖矿难度、merkleTree的hash等字段 这些字段在之后的讲解中也会加上的,现在我们暂时不做那么复杂 区块需要实现的方法 ``` //计算区块自己的hash值 //区块的hash= hash(PrevBlockHash + Data(交易的merkleTree的hash) + timestamp ) func (b *Block) SetHash() { //伪代码 b.hash= sha256(PrevBlockHash + Data + timestamp) } ``` 可以看到,我们使用的**hash函数是sha256** 于是,我们有了新建区块的方法 ``` //新建一个区块 func NewBlock(data string, prevBlockHash []byte) *Block { //伪代码 block := &Block{now, data, prevBlockHash} block.SetHash() return block } ``` ### 数据结构-区块链 把**区块连起来**,就是区块链 链式结构的实现有两种方式: - 数组 - 指针 我们先使用最简单的数组,之后的实现中会用到指针 ``` //区块链 type Blockchain struct { blocks []*Block } //添加区块 func (bc *Blockchain) AddBlock(data string) { prevBlock := bc.blocks[len(bc.blocks)-1] newBlock := NewBlock(data, prevBlock.Hash) bc.blocks = append(bc.blocks, newBlock) } //创世区块 func NewGenesisBlock() *Block { return NewBlock("Genesis Block", []byte{}) } //创建区块链 func NewBlockchain() *Blockchain { return &Blockchain{[]*Block{NewGenesisBlock()}} } ``` 运行 ``` func main() { bc := NewBlockchain() bc.AddBlock("Send 1 BTC to Ivan") bc.AddBlock("Send 2 more BTC to Ivan") for _, block := range bc.blocks { fmt.Printf("Prev. hash: %x\n", block.PrevBlockHash) fmt.Printf("Data: %s\n", block.Data) fmt.Printf("Hash: %x\n", block.Hash) fmt.Println() } } ``` # 比特币原理及其代码实践2-工作量证明 ## 工作量证明(proof-of-work) ### 哈希计算 获得指定数据的一个哈希值的过程,就叫做哈希计算。一个哈希,就是对所计算数据的一个唯一表示。对于一个哈希函数,**输入任意大小的数据**,它会**输出一个固定大小的哈希值**。下面是哈希的几个关键特性: 1. 无法从一个哈希值恢复原始数据。也就是说,哈希并不是加密。 2. 对于特定的数据,只能有一个哈希,并且这个哈希是唯一的。 3. 即使是仅仅改变输入数据中的一个字节,也会导致输出一个完全不同的哈希。 ### Hashcash 比特币使用 [Hashcash](https://en.wikipedia.org/wiki/Hashcash) ,它可以被分解为以下步骤: 1. 取一些公开的数据(在比特币中,它是区块头) 2. 给这个公开数据添加一个计数器。计数器默认从 0 开始 3. 将 **data(数据)** 和 **counter(计数器)** 组合到一起,获得一个哈希 4. 检查哈希是否符合一定的条件: 1. 如果符合条件(前多少位是0),结束 2. 如果不符合,增加计数器,重复步骤 3-4 ## 实现 ``` //挖矿难度,前24位都是0,用16进制的话则是前6位都是0 const targetBits = 24 ``` 我们并不会像比特币那样动态调整难度,就是说targetBits是常量 ``` //构造一个pow的结构体去实现功能 //我们使用bitInt,在对比hash值的时候,转化成数字比大小 type ProofOfWork struct { block *Block target *big.Int } func NewProofOfWork(b *Block) *ProofOfWork { target := big.NewInt(1) //0000010000000000000000000... //左移256-targetBits位 target.Lsh(target, uint(256-targetBits)) pow := &ProofOfWork{b, target} return pow } //准备数据 //data = PrevBlockHash + blockData(交易的hash值) + Timestamp + targetBits + nonce func (pow *ProofOfWork) prepareData(nonce int) []byte { data := bytes.Join( [][]byte{ pow.block.PrevBlockHash, pow.block.Data, IntToHex(pow.block.Timestamp), IntToHex(int64(targetBits)), IntToHex(int64(nonce)), }, []byte{}, ) return data } //pow运算 //1. hash = hash(PrevBlockHash + blockData + Timestamp + targetBits + nonce) //2. 如果hash < target 则成功 //3. 否则 nonce++ 重复运算 func (pow *ProofOfWork) Run() (int, []byte) { var hashInt big.Int var hash [32]byte nonce := 0 fmt.Printf("Mining the block containing \"%s\"\n", pow.block.Data) for nonce < maxNonce { //计算hash data := pow.prepareData(nonce) hash = sha256.Sum256(data) hashInt.SetBytes(hash[:]) //这里对比hash值和目标值的大小,如果小于,则pow成功 if hashInt.Cmp(pow.target) == -1 { fmt.Printf("\r%x", hash) break } else { nonce++ } } fmt.Print("\n\n") return nonce, hash[:] } //pow验证 func (pow *ProofOfWork) Validate() bool { var hashInt big.Int data := pow.prepareData(pow.block.Nonce) hash := sha256.Sum256(data) hashInt.SetBytes(hash[:]) isValid := hashInt.Cmp(pow.target) == -1 return isValid } func main() { ... for _, block := range bc.blocks { ... pow := NewProofOfWork(block) fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate())) fmt.Println() } } ``` # 比特币原理及其代码实践3-本地存储 ## 比特币如何存储数据 比特币使用 [LevelDB](https://github.com/google/leveldb)来存储数据,这是一个本地存储的高效的key-value的数据库->map Bitcoin Core 使用两个 “空间” 来存储数据: 1. 其中一个 “空间”是 **blocks**,它存储了描述一条链中所有块的`元数据(区块数据,最新的区块hash等)` 2. 另一个 “空间”是 **chainstate**,存储了一条链的状态,`也就是当前所有的未花费的交易输出`UTXO,和一些元数据 在 **blocks** 中,**key -> value** 为: | key | value | | -------------------------------------------------- | --------------------------------------------------- | | `b` + **32 字节的 block hash** | **block index record** | | `f` + 4 字节的 file number | file information record | | `l` + **4 字节的 file number** | | | `R` + 1 字节的 boolean | 是否正在 reindex | | `F` + 1 字节的 flag name length + flag name string | 1 byte boolean: various flags that can be on or off | | `t` + 32 字节的 transaction hash | transaction index record | 在 **chainstate**,**key -> value** 为: | key | value | | ------------------------------------ | ------------------------------------------------------------ | | `c` + **32 字节的 transaction hash** | **unspent transaction output record for that transaction** | | `B**` | **32 字节的 block hash: the block hash up to which the database represents the unspent transaction outputs** | 详情可见 **[这里](https://en.bitcoin.it/wiki/Bitcoin_Core_0.11_(ch_2):_Data_Storage)**。 ## 简单实现 因为目前还没有交易,所以我们只需要 **blocks** 。另外,正如上面提到的,我们会将整个数据库存储为单个文件,而不是将区块存储在不同的文件中。所以,我们也不会需要文件编号(file number)相关的东西。最终,我们会用到的键值对有: 1. `32 字节的 block-hash` -> block 结构 2. `l` -> 链中最后一个块的 hash 为方便演示,我们使用json存储数据 ### 区块链数据结构更新 ``` //区块链 type Blockchain struct { //map[区块的hash] = 区块 DB map[string]Block // l 存最新的区块hash值 L string } //创建区块链 func CreateBlockchain() *Blockchain { if !PathExists(dbFile) { //如果文件不存在,则创建一个blockchain的json文件 f, err := os.Create(dbFile) } //创世区块 block := NewGenesisBlock() var db map[string]Block = map[string]Block{} hash := hex.EncodeToString(block.Hash) db[hash] = *block //新建blockchain对象 bc := &Blockchain{ DB: db, L: hash, } //写入文件 data, _ := json.Marshal(bc) err := ioutil.WriteFile(dbFile, data, 0666) return bc } //添加区块 func (bc *Blockchain) AddBlock(data string) { //最新的区块 prevBlock := bc.DB[bc.L] //新建区块 newBlock := NewBlock(data, prevBlock.Hash, prevBlock.Height+1) hash := newBlock.GetHashString() bc.DB[hash] = *newBlock //更新blockchain指针L bc.L = hash //保存 bc.SaveBlockchain() } ``` ### 遍历区块链 ``` //区块链迭代器 //我们不会一次性加载所有的区块数据 //而是一个一个的迭代加载 type BlockchainInterator struct { currentHash string DB map[string]Block } //返回当前块,并指向上一个区块 func (i *BlockchainInterator) Next() *Block { //返回当前块 block := i.DB[i.currentHash] //并指向前一个块 i.currentHash = block.GetPrevHashString() return &block } ``` # 比特币原理及其代码实践4-交易(1) **交易(transaction)是比特币的核心所在。** **在区块链中,交易一旦被创建,就没有任何人能够再去修改或是删除它。** **比特币采用的是 `UTXO(Unspent Transaction Output)模型`,并非账户模型,并`不直接存在“余额”`这个概念** **余额需要通过遍历整个交易历史得来** ## 比特币交易 [Bitcoin - BTC Price, Live Chart, and News | Blockchain.com](https://www.blockchain.com/explorer/assets/btc) 我们可以在上面网站查看比特币的区块中的交易 ![](https://gitee.com/Oracion/typora_images/raw/master/blockchain/%E6%AF%94%E7%89%B9%E5%B8%81%E4%BA%A4%E6%98%93%E5%9B%BE1.png) 那么大家可以想一想,在每一笔交易中,**txin**都会指向**之前的某一比交易**的**txout** 在之前的交易中,**txout**的转账又来自**txin** 如此循环下去,是不是这比钱会有一个**源头** 这个源头就是**coinbase挖矿奖励** ![](https://gitee.com/Oracion/typora_images/raw/master/blockchain/127313-4f8e668c826fd31a.png) 注意: 1. 有一些输出并没有被关联到某个输入上(找零) 2. 一笔交易的输入可以引用之前多笔交易的输出(钱不够的话可以汇总多笔交易) 3. 一个输入必须引用一个输出(txin会有txid,txid就是之前交易的hash值)(coinbase 交易除外) 这就是`UTXO模型` 在比特币中,其实并不存钱的概念。**所有的都是交易**。交易仅仅是通过一个脚本(script)来锁定(lock)一些值(value),而这些值只可以被锁定它们的人解锁(unlock)(解锁意味着可以你可以用它转账)。(address中包含的用户公钥的hash,用户用自己的公钥+私钥签名即可解锁) ### 交易 ``` //交易 type Transaction struct { ID []byte //交易hash值 = hash(vin + vout) Vin []TXInput //输入 Vout []TXOutput //输出 } ``` ### 输出 ``` type TXOutput struct { Value int //多少值,就是多少比特币 ScriptPubKey string //解锁的脚本,我们目前暂时就当作收货人地址就行 } ``` 关于输出,非常重要的一点是:它们是**不可再分的(indivisible)**。也就是说,你无法仅引用它的其中某一部分。要么不用,如果要用,必须一次性用完。如果用不完,则产生一个新的output找零。 ### 输入 ``` type TXInput struct { Txid []byte //之前的交易hash值 Vout int //是之前交易的第几个output ScriptSig string //解锁脚本,目前暂时当作发货人的发货地址 } ``` 所以目前 `ScriptSig` 将仅仅存储一个用户自定义的任意钱包地址。我们会在下一篇文章中实现公钥(public key)和签名(signature)。 ### 钱的来源 coinbase交易 ``` //挖矿奖励 const subsidy = 10 //新建一个挖矿奖励交易 //会有一个凭空产生的input和一个subsidy的output func NewCoinbaseTX(to, data string) *Transaction { if data == "" { data = fmt.Sprintf("Reward to '%s'", to) } //Coinbas的input的hash为空,不需要 txin := TXInput{[]byte{}, -1, data} //subsidy奖励 txout := TXOutput{subsidy, to} //组成一个交易 tx := Transaction{nil, []TXInput{txin}, []TXOutput{txout}} tx.ID = tx.Hash() return &tx } //区块链第一笔钱的诞生 func CreateBlockchain() *Blockchain { ... //创世区块 firstAddress := "zhangsan" //产生一笔CoinbaseTX cbtx := NewCoinbaseTX(firstAddress, "") //放入创世区块 block := NewGenesisBlock(cbtx) //保存区块链 ... return bc } ``` ### 找到自己的余额和未使用的output, utxo **关键代码** ``` //找到没有花费的output集合,并返回全部余额 //返回: //1.全部余额 //2.可用的outputIndex(key是交易的hash,value是第几个output) //3.可用的output(key是交易的hash,value是output) func (bc *Blockchain) FindSpendableOutputs(address string) (int, map[string][]int, map[string][]TXOutput) { ... //未使用output unspentOutputIndexList := make(map[string][]int) unspentOutputMap := make(map[string][]TXOutput) //已经使用的output spentTXOs := make(map[string][]int) //金额 accumulated := 0 for { //循环所有的区块 block := bci.Next() //伪代码 遍历区块所有的交易 遍历交易所有的output //如果这个output没有被其他的交易的input引用,则保存 save outputIndex,output对象(key为交易hash) accumulated += output.value 遍历交易的所有input //缓存input,作为output是否被使用的标记 cache spentTXOs if block 没有prevHash break } return accumulated, unspentOutputIndexList, unspentOutputMap } ``` ### 创建交易 ``` //新建交易,发送币 func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction { var inputs []TXInput var outputs []TXOutput acc, allValidOutputIndex, unspentOutputMap := bc.FindSpendableOutputs(from) if acc < amount { log.Panic("ERROR: Not enough funds") } //找到足够的output validOutputs := make(map[string][]int) accumulated := 0 循环自己的所有output,找到足够量的output存到validOutputs //构建input,把找到的input的打个包inputs for txid, outs := range validOutputs { for _, out := range outs { //这里可以看到,我们的output成为了新交易的input input := TXInput{txID, out, from} inputs = append(inputs, input) } } //构建output outputs = append(outputs, TXOutput{amount, to}) if acc > amount { //如果需要找零,新建一个output outputs = append(outputs, TXOutput{acc - amount, from}) } tx := Transaction{nil, inputs, outputs} tx.ID = tx.Hash() return &tx } ``` ### 发送交易,产生新的区块 ``` //区块更新 type Block struct { Timestamp int64 //时间戳 Transactions []Transaction //数据 TransactionHash []byte //交易的hash值 PrevBlockHash []byte //前一个区块的hash Hash []byte //自己的hash Nonce int Height int } //给某个地址发送金额 func (bc *Blockchain) Send(from, to string, amount int) { tx := NewUTXOTransaction(from, to, amount, bc) //挖矿奖励 cbtx := NewCoinbaseTX(from, "") transactions = append(transactions, cbtx) transactions = append(transactions, tx) bc.AddBlock(transactions) } //添加区块 func (bc *Blockchain) AddBlock(transactions []Transaction) { //最新的区块 prevBlock := bc.DB[bc.L] //新建区块,把交易放进来并运行pow //pow -> hash(PrevBlockHash + transactionHash + Timestamp + targetBits + nonce) newBlock := NewBlock(transactions, prevBlock.Hash, prevBlock.Height+1) //更新区块链数据 hash := newBlock.GetHashString() bc.DB[hash] = *newBlock bc.L = hash //保存到文件 bc.SaveBlockchain() } ``` ### 账户余额 ``` //余额 func (bc *Blockchain) GetBalance(address string) int { balance, _, _ := bc.FindSpendableOutputs(address) fmt.Printf("balance of %s is %d\n", address, balance) return balance } ``` [Transaction - Bitcoin Wiki](https://en.bitcoin.it/wiki/Transaction) ## 区块链刻字 CoinbaseTX中的input的sigscript其实是可以刻字的!!! 比特币的第一份交易的[Transaction: 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b | Blockchain.com](https://www.blockchain.com/explorer/transactions/btc/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b?show_adv=true) 查看第一个区块中包含的隐藏信息 ``` func TestShow1(t *testing.T) { //中本聪的留言 s := "04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73" b, _ := hex.DecodeString(s) fmt.Printf("result: %s\n", string(b)) } ``` ![](https://gitee.com/Oracion/typora_images/raw/master/blockchain/20230228165129.png) 程序员的浪漫 [Transaction: c633a5c53b03ce188c6ba376f7e9d1428838284fdeab6d600d6dfcd14a860140 | Blockchain.com](https://www.blockchain.com/explorer/transactions/btc/c633a5c53b03ce188c6ba376f7e9d1428838284fdeab6d600d6dfcd14a860140?show_adv=true) ![](https://gitee.com/Oracion/typora_images/raw/master/blockchain/20230228165149.png) # 比特币原理及其代码实践5-地址 **review:UTXO和余额模型到底有没有区别?** 有,举个例子,比如小红转我80元,小绿转我20元,我再转50元给了小黑。 `UTXO模型`可以明确的告诉我,我转给小黑的50元到底是来自小红给我的80元加找零,还是小绿的20元加上小红给我的80元加找零。 `余额模型`却无法做到可追溯,因为我转给小黑50元,是我的余额减50,小黑的余额加50,然后产生一笔交易记录(我->小黑:50元),不能确定我转出去的50来自谁。 ## 引言 在比特币中,`没有用户账户`,不需要,也不会在任何地方存储个人数据(比如姓名,电话,身份信息等)。 但我们必须要能够实现**交易的输出者可以使用这比交易**,用官方的话来说,就是**你拥有在这些输出上锁定的币**。 这一节,我们将要实现一个跟比特币一样的**真实地址address**。 需用用到的知识: - 公钥加密 - 数字签名 - base58算法 ## 公钥加密 公钥加密(public-key cryptography)算法使用的是成对的密钥:公钥和私钥。私钥加密公钥解密,或者公钥加密私钥解密。 在加密货币的世界中,**你的私钥代表的就是你**,私钥就是一切。 **比特币钱包**也只不过是这样的密钥对而已。 **比特币**使用`椭圆曲线`来产生私钥。 ## 数字签名 ### 签名过程: hashData = hash(data) 加密的hashData = 私钥加密(hashData) (data+加密的hashData) 一起发给接受方 ### 验证的过程: hashData_1 = hash(data) hashData_2=公钥解密(加密的hashData) if hashData_1 == hashData_2 : 验证通过 ### 数字签名特点: 1. 当数据从发送方传送到接收方时,数据不会被修改; 2. 数据由某一确定的发送方创建; 3. 发送方无法否认发送过数据这一事实。 在**比特币**中,每一笔交易输入都会由`创建交易的人签名`。在被放入到一个块之前,必须要`对每一笔交易进行验证`。 ## Base58 **比特币**使用 `Base58 算法`将字节串转换成人类可读的形式。 Base58算法是可逆的,我们对数据进行Base58编码后,可以解码还原成原来的数据。 ## 比特币地址的产生 我们设pubKey=公钥 1. hash_1 = SHA256(pubKey) //对公钥进行第一次hash 2. hash_2 = RIPEMD160(hash_1) // 对hash_1进行hash 3. versionedPayload = version + hash_2 //byte拼接 4. hash_3 =SHA256(SHA256(versionedPayload )) // 再对versionedPayload 进行两次hash 5. s = version(1字节) + hash_2(20字节) + hash_3(前4字节) //三个byte拼接 6. address = Base58Encode(s) //对s进行base58编码,转成人类可读的字符串 s字符串: ``` Version Public key hash Checksum 00 62E907B15CBF27D5425399EBF6F0FB50EBB88F18 C29B7D93 ``` 最终加密结果 ``` 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa \\ 属于中本聪 ``` 图: ![](https://gitee.com/Oracion/typora_images/raw/master/blockchain/PubKeyToAddr.png) ## 代码实现 ``` //钱包 type Wallet struct { Privatekey ecdsa.PrivateKey \\私钥 PublicKey []byte \\公钥 } //新建一个钱包 func NewWallet() *Wallet { private, public = 椭圆曲线生成公私钥 wallet := Wallet{private, public} return &wallet } const versionByte = byte(0x00) const addressChecksumLen = 4 //获得地址 //address = base58(version + publickey hash + checksum) func (w Wallet) GetAddress() []byte { //RIPEMD160(SHA256(pubKey)) pubkeyhash := HashPubkey(w.PublicKey) //versionByte + RIPEMD160(SHA256(pubKey)) versionedPayload := append([]byte{versionByte}, pubkeyhash...) // sha256(sha256(versionByte + RIPEMD160(SHA256(pubKey)))) checksum := checksum(versionedPayload) //versionedPayload + checksum fullPayload := append(versionedPayload, checksum...) //base56 address := Base58Encode(fullPayload) return address } ``` 这就是为什么选择一个合适的公钥加密算法是如此重要:考虑到私钥是随机数,生成同一个数字的概率必须是尽可能地低。理想情况下,必须是低到“永远”不会重复。 这个算法有一个好处,就是可以通过address推出收货人的公钥hash值。 以这个为基础,我们就可以对交易的input和output进行`锁定`/`解锁`和`签名`等操作。 # 比特币原理及其代码实践6-交易签名和解锁 ## 实现签名 **交易必须被签名**,因为这是比特币里面保证发送方不会花费属于其他人的币的唯一方式。如果一个签名是无效的,那么这笔交易就会被认为是无效的,因此,这笔交易也就无法被加到区块链中。 思考:哪些数据需要签名? 1. **之前交易的某个输出的公钥hash**,也就是我们钱的源头 2. **当前交易的全部输出**,也就是我们把钱转给了谁 签名在哪个地方? **签名在当前交易的每一个输入** ![](https://gitee.com/Oracion/typora_images/raw/master/blockchain/123321.png) 伪代码: ``` func (tx *Transaction) Sign(privKey ecdsa.PrivateKey, prevTXs map[string]Transaction) { ... //针对每一个输入Vin for inID, vin := range txCopy.Vin { prevVout := 获得之前交易的输出 hashValue := hash(prevVout的公钥hash+当前交易的全部输出) //使用私钥对hash值进行数字签名 ecdsa.Sign signature := 数字签名(privKey, hashValue) //赋值 vin.Signature = signature //把自己的公钥也带上,为了方便别人验证 //vin.Pubkey = Pubkey } } ``` 验证交易: ``` func (tx *Transaction) Verify(prevTXs map[string]Transaction) bool { ... //针对每一个vin都需要验证 for inID, vin := range tx.Vin { prevVout := 获得之前交易的输出 hashValue := hash(prevVout的公钥hash+当前交易的全部输出) //获得公钥 PubKey := vin.Pubkey //验证 ecdsa.Verify Verify(PubKey,hashValue) } ... } ``` ## 理解解锁和上锁 ### 上锁 我们从对方的address中提取到对方的公钥hash 在output中带上对方的公钥hash就意味着对这比output上锁 ### 解锁 对方在新的交易中,引用我们的output,指向一个新的input 并对新的input用自己的私钥进行签名 这就是解锁这笔钱为自己所用 **总结:** 用对方的**公钥hash**上锁输出,对方用自己的**私钥签名**和**公钥**解锁这比输出作为自己的输入 ## 验证 矿工在打包区块的时候,会对每一笔input进行验证 ``` func (bc *Blockchain) MineBlock(transactions []*Transaction) { var lastHash []byte for _, tx := range transactions { if bc.VerifyTransaction(tx) != true { log.Panic("ERROR: Invalid transaction") } } ... } ``` **代码演示** ## 真实的比特币是如何做的? 在比特币中,锁定/解锁逻辑被存储在**脚本**中,它们被分别存储在输入和输出的 `ScriptSig` 和 `ScriptPubKey` 字段。 比特币的脚本语言: - 基于堆栈的编程语言 - 非图灵完备 - 一个脚本能在任何系统上以相同的方式执行 **锁定脚本**是一个放在一个输出值上的“障碍”,同时它明确了今后花费这笔输出的条件。由于锁定脚本往往含有一个公钥(**即比特币地址**),它也曾被称作**脚本公钥代码**。 **解锁脚本**是一个“解决”或满足被锁定脚本在一个输出上设定的花费条件的脚本,同时它将允许输出被消费。解锁脚本是每一笔比特币交易输出的一部分,而且往往含有一个被用户的比特币钱包**(通过用户的私钥)生成的数字签名**。由于解锁脚本常常包含一个数字签名,因此它曾被称作ScriptSig。但是并非所有的解锁脚本都会包含签名。 ![](https://gitee.com/Oracion/typora_images/raw/master/blockchain/v2-e9d8f281f7d8f21e7515caec0a79bd97_r.png) 基于堆栈的脚本: ![](https://gitee.com/Oracion/typora_images/raw/master/blockchain/v2-0c01c511bece48588319a3386b23bf52_r.jpg) ![](https://gitee.com/Oracion/typora_images/raw/master/blockchain/v2-056dfae3c87e2efa705405f44f2bea8d_720w.jpg) 从脚本的角度来解释交易,交易就是:**解锁前一个锁定脚本,建一个新的锁定脚本。** # 比特币原理及其代码实践7-Merkle树 **回忆:**前面说过,比特币交易的解锁和上锁其实是一种基于堆栈的脚本语言,大家可以想一下,这种脚本语言还能干什么? 比特币的脚本语言启发了**太坊创始人Vitalik Buterin(那年他20岁)**创建以太坊,开发了智能合约。 ## 全节点和轻节点 因为比特币的去中心化特性,网络中的每个节点必须是独立,自给自足的,也就是每个节点必须存储一个**区块链的完整副本**。 随着越来越多的人使用比特币,这条规则变得越来越难以遵守:因为不太可能每个人都去运行一个全节点。并且,由于节点是网络中的完全参与者,它们负有相关责任:节点必须验证交易和区块。另外,要想与其他节点交互和下载新块,也有一定的网络流量需求。 对这个问题也有一个解决方案:**简易支付验证(Simplified Payment Verification, SPV)**。SPV 是一个**比特币轻节点**,它不需要下载整个区块链,也**不需要验证区块和交易**,它只用维护区块链header的信息。相反,它会在区块链查找交易(为了验证支付),并且需要**连接到一个全节点**来检索必要的数据。这个机制允许在仅运行一个全节点的情况下有多个轻钱包。 为了实现 SPV,需要**有一个方式来检查是否一个区块包含了某笔交易,而无须下载整个区块**。这就是 **Merkle 树**所要完成的事情。 ## Merkle树 ![](https://gitee.com/Oracion/typora_images/raw/master/blockchain/127313-9c708d3c3d6a19c2.png) 每个块都会有一个 Merkle 树,一个叶子节点就是一个交易哈希。 叶子节点的数量必须是双数。 **根哈希**会当做是整个块交易的唯一标示,将它保存到区块头,然后用于工作量证明。 ``` //Merkle树 type MerkleTree struct { //根节点 RootNode *MerkleNode } //Merkle节点 type MerkleNode struct { Left *MerkleNode Right *MerkleNode //hash值 Data []byte } //创建节点 func NewMerkleNode(left, right *MerkleNode, data []byte) *MerkleNode { mNode := MerkleNode{} if left == nil && right == nil { hash := sha256.Sum256(data) mNode.Data = hash[:] } else { prevHashes := append(left.Data, right.Data...) hash := sha256.Sum256(prevHashes) mNode.Data = hash[:] } mNode.Left = left mNode.Right = right return &mNode } //生成MerkleTree func NewMerkleTree(data [][]byte) *MerkleTree { var nodes []MerkleNode if len(data)%2 != 0 { data = append(data, data[len(data)-1]) } for _, datum := range data { node := NewMerkleNode(nil, nil, datum) nodes = append(nodes, *node) } for i := 0; i < len(data)/2; i++ { var newLevel []MerkleNode for j := 0; j < len(nodes); j += 2 { node := NewMerkleNode(&nodes[j], &nodes[j+1], nil) newLevel = append(newLevel, *node) } nodes = newLevel } mTree := MerkleTree{&nodes[0]} return &mTree } //获得block的bash值,实际就是取的根节点的hash值 func (b *Block) HashTransactions() []byte { var transactions [][]byte for _, tx := range b.Transactions { transactions = append(transactions, tx.Serialize()) } mTree := NewMerkleTree(transactions) return mTree.RootNode.Data } ``` ## SPV支付验证的实现步骤 (轻节点只知道自己的交易的hash和区块链的头部信息) SPV进行简单支付验证时步骤如下: 1. 计算待验证支付的交易哈希值; 2. 轻节点会向**全节点**请求交易所在的区块和区块的MerkleTree; 3. 根据哈希认证路径,计算默克尔树的**根哈希值**,将计算结果与本地区块头中的默克尔树的根哈希值进行比较; 4. 验证通过后,再看一下自己的交易是否已经在最长的链上(即后面至少还有6个区块)。 # 比特币原理及其代码实践8-比特币网络 ## 引言 ​ 我们所构建的原型已经具备了区块链所有的关键特性:**匿名**,**安全**,**随机生成的地址**`(公私钥加密)`;**区块链数据存储**`(key-value存储)`;**工作量证明系统**`(pow)`;**可靠地存储交易**`(MerkleTree)`。 ​ 尽管这些特性都不可或缺,但是仍有不足。能够使得这些特性真正发光发热,使得加密货币成为可能的,是**网络(network)** ​ 你可以将这些区块链特性认为是规则(rule),类似于人类在一起生活,繁衍生息建立的规则,一种社会安排。区块链网络就是一个程序社区,里面的每个程序都遵循同样的规则,正是由于遵循着同一个规则,才使得网络能够长存。类似的,当人们都有着同样的想法,就能够将拳头攥在一起构建一个更好的生活。如果有人遵循着不同的规则,那么他们就将生活在一个分裂的社区(州,公社,等等)中。同样的,如果有区块链节点遵循不同的规则,那么也会形成一个分裂的网络。 ## 区块链网络 ​ 区块链网络是去中心化的,这意味着没有服务器,客户端也不需要依赖服务器来获取或处理数据。在区块链网络中,有的是节点,每个节点是网络的一个完全(full-fledged)成员。**节点就是一切:它既是一个客户端,也是一个服务器**。这一点需要牢记于心,因为这与传统的网页应用非常不同。 ​ 区块链网络是一个 P2P(Peer-to-Peer,端到端)的网络,即节点直接连接到其他节点。它的拓扑是扁平的,因为在**节点的世界中没有层级之分**。下面是它的示意图: ![](https://gitee.com/Oracion/typora_images/raw/master/blockchain/127313-3c4174ad7cf6edf3.png) ## 节点角色 尽管节点具有完备成熟的属性,但是它们也可以在网络中扮演不同角色。比如: 1. 矿工:这样的节点运行于强大或专用的硬件(比如 ASIC)之上,它们唯一的目标是,尽可能快地挖出新块。矿工是区块链中唯一可能会用到工作量证明的角色,因为挖矿实际上意味着解决 PoW 难题。在权益证明 PoS 的区块链中,没有挖矿。 2. 全节点:这些节点验证矿工挖出来的块的有效性,并对交易进行确认。为此,他们必须拥有区块链的完整拷贝。同时,全节点执行路由操作,帮助其他节点发现彼此。对于网络来说,非常重要的一段就是要有足够多的全节点。因为正是这些节点执行了决策功能:他们决定了一个块或一笔交易的有效性。 3. SPV:SPV 表示 Simplified Payment Verification,简单支付验证。这些节点并不存储整个区块链副本,但是仍然能够对交易进行验证(不过不是验证全部交易,而是一个交易子集,比如,发送到某个指定地址的交易)。一个 SPV 节点依赖一个全节点来获取数据,可能有多个 SPV 节点连接到一个全节点。SPV 使得钱包应用成为可能:一个人不需要下载整个区块链,但是仍能够验证他的交易。 ## 比特币节点的通信流程 1. 获取到当前比特币网络中网络状况正常的节点列表(获取种子节点列表)。 2. 运行比特币的协议栈,与比特币网络中的其他节点通信。 ### 1.获得比特币其他节点 #### 使用“DNS种子”(DNS seeds) 比特币源码的chainparams.cpp文件中,一共包含了6个DNS域名,可以用于新节点加入比特币网络时获取种子节点列表。 | seed.bitcoin.sipa.be | | ----------------------------- | | dnsseed.bluematt.me | | dnsseed.bitcoin.dashjr.org | | seed.bitcoinstats.com | | seed.bitcoin.jonasschnelli.ch | | seed.btc.petertodd.org | ``` dig seed.bitcoinstats.com ``` seed.bitcoinstats.com. 3591 IN A 172.104.154.196 seed.bitcoinstats.com. 3591 IN A 89.116.26.27 seed.bitcoinstats.com. 3591 IN A 146.4.124.134 seed.bitcoinstats.com. 3591 IN A 77.174.251.3 seed.bitcoinstats.com. 3591 IN A 93.216.157.252 seed.bitcoinstats.com. 3591 IN A 94.19.7.55 seed.bitcoinstats.com. 3591 IN A 50.38.34.178 seed.bitcoinstats.com. 3591 IN A 178.14.78.87 seed.bitcoinstats.com. 3591 IN A 62.168.65.42 seed.bitcoinstats.com. 3591 IN A 93.198.213.253 seed.bitcoinstats.com. 3591 IN A 190.2.152.245 seed.bitcoinstats.com. 3591 IN A 95.216.3.111 seed.bitcoinstats.com. 3591 IN A 193.232.158.22 seed.bitcoinstats.com. 3591 IN A 188.165.211.112 seed.bitcoinstats.com. 3591 IN A 128.199.55.88 seed.bitcoinstats.com. 3591 IN A 207.180.251.119 seed.bitcoinstats.com. 3591 IN A 134.209.84.163 seed.bitcoinstats.com. 3591 IN A 188.120.255.115 seed.bitcoinstats.com. 3591 IN A 45.227.253.238 seed.bitcoinstats.com. 3591 IN A 62.210.110.208 seed.bitcoinstats.com. 3591 IN A 95.216.226.214 seed.bitcoinstats.com. 3591 IN A 202.172.102.5 seed.bitcoinstats.com. 3591 IN A 185.78.209.40 seed.bitcoinstats.com. 3591 IN A 92.100.48.186 seed.bitcoinstats.com. 3591 IN A 92.58.171.140 DNS种子提供比特币节点的IP地址列表,Bitcoin Core客户端提供五种不同的DNS种子,通常默认使用即可。 #### 手动指定节点 手动通过-seednode命令指定一个比特币节点的IP地址作为比特币种子节点。 ### 2.与比特币节点进行初始“握手”,建立连接 [P2P Network — Bitcoin](https://developer.bitcoin.org/reference/p2p_networking.html) 握手的发起者会向目标节点发起TCP连接,默认为8333端口(部分节点不使用8333端口),连接建立成功后,双方需要根据比特币协议的规定,完成握手的过程,主要是确认双方版本、IP地址、端口等信息。 **version请求** ![](https://gitee.com/Oracion/typora_images/raw/master/blockchain/123.jpg) ​ 节点1为握手的发起者,希望与节点B进行握手,首先向2节点发送一个version消息(含有自己的协议版本等),节点2收到节点1的version消息后,会对数据包进行验证,若验证无误,则会向1回应自己的version消息,否则,节点2将忽略掉节点1的消息。节点2在回应了自己的version消息之后,会再回应一个version ack的消息。节点A在收到2节点的version ack消息后,向节点2回应一个version ack,握手过程即正常结束。 ### 3.获取节点周围缓存节点 在完成握手后,通过向对方节点发送getaddr消息,即可获取其周围活跃节点。 **getaddr请求** ![](https://gitee.com/Oracion/typora_images/raw/master/blockchain/123321.jpg) addr:返回节点个数和节点列表 ### 4.下载区块链头 **getblocks 请求** 在version请求中,我们可以知道自己的区块高度和对方的区块高度,如果我们的区块链高度小,则需要请求区块头数据。 ### 5.同步区块链数据,并验证 节点下载完区块头后,会开始下载区块。 **Inv-type=block 请求** 比特币区块链中的每个区块都包含了一系列交易记录和一些元数据,例如区块的哈希值、时间戳、难度目标、交易数量等信息。 节点会从其他节点中请求缺失的区块数据,直到下载完整个区块链。 ### 6.处理交易 **Inv-type=transaction 请求** 1. 如果交易是自己节点产生的,自己先把交易缓存到mempool中,并**转发给所有已知的节点**。 2. 如果节点收到了其他节点的交易,验证交易,并放到自己的mempool,并转发给其他节点。 ### 7.挖矿 如果这个节点是矿工节点,当mempool大于某个值时,执行挖矿操作,并清空所有的mempool。 广播自己的新区块到其他所有节点。 其他节点会验证区块,并判断这个区块是不是最长的链上,如果不是最长链节点会拒绝这个区块。 ## 比特币分叉 ​ 比特币网络正常运行依赖于节点按一致的规则检验和收录区块,这种情况下即便偶尔同时有多个相同高度的正确的区块产生,`分支博弈`也会导致全网迅速归集到同一链上。 ​ 但当涉及规则改变时,即网络中存在遵守不同的规则的旧节点和新节点时,就会存在区块链分叉且不能迅速回归同一链的可能,按具体情况又分为硬分叉和软分叉: #### 软分叉 ​ 比特币软分叉(Soft Fork)是指对比特币协议进行一项改进,该改进是向后兼容的,也就是说,旧版本的比特币节点可以继续运行,而无需升级。在软分叉中,升级后的节点能够处理新规则的交易,但旧版本的节点会将这些交易视为有效。 ​ 软分叉通常是为了改进比特币协议而进行的,以提高其安全性、可扩展性和其他方面的性能。软分叉通常需要比特币网络中**大部分**的矿工和节点支持,以确保网络的顺畅升级。如果不支持软分叉,那么这些节点和矿工将被视为“分叉”,从而分离出去,并与其他支持该软分叉的节点和矿工形成两个独立的网络。 ​ 总之,软分叉是比特币协议的一种改进方法,它可以通过向后兼容的方式升级,而无需强制用户升级其软件。 #### 硬分叉 ​ 比特币硬分叉(Hard Fork)是指对比特币协议进行一项改进,该改进不向后兼容,也就是说,旧版本的比特币节点无法处理新规则的交易,因此必须升级节点软件。在硬分叉中,新版本的比特币节点和旧版本的比特币节点无法互相通信。 ​ 硬分叉通常是为了解决协议中存在的不兼容性问题而进行的。硬分叉可能会导致比特币网络分裂成两个独立的网络,其中一个网络遵循旧规则,另一个网络遵循新规则。在这种情况下,两个网络都可以独立运作,但可能会导致比特币的价值和市场流动性受到影响。 #### 同时挖出区块会导致分叉吗? ​ 会,但是短暂的。 ​ 比特币网络会选择最长的分支作为有效的区块链。**矿工和节点会比较两个分支的长度**,选择**最长**的分支,然后在其上继续进行工作。这意味着在区块链的另一个分支上工作的矿工会停止工作,并加入最长的分支。这个过程通常被称为“链的重组”。 ​ 这是因为矿工的工作量越大,他们获得的收益也就越高,因此他们更**有动力在最长的分支上继续工作**,而不是在较短的分支上浪费资源。因此,分叉通常是短暂的,并很快被解决。 ​ 所以这并不会影响比特币网络的稳定性。 [Jeiwan/blockchain_go: A simplified blockchain implementation in Golang (github.com)](https://github.com/Jeiwan/blockchain_go)