如何使用Go从头开始构建区块链
随着Web 3.0和区块链每天变得越来越主流,您知道什么是区块链吗?您知道它的技术优势和应用吗?
本教程的目的是通过构建区块链技术,从技术角度剖析区块链技术。
现在,您将从零开始构建一个区块链系统,以真正了解这种对等分布式技术的来龙去脉。
然后,自己决定其未来和优势。
本教程中构建,学习和做什么?
您将在没有任何经验的情况下在本地计算机上设置Go项目
您将生成并分发您的第一个区块链令牌
您将从头开始在Go中开发一个CLI控制的数据库
您会发现在他们最喜欢的应用中,有多少位权限用户拥有
您会发现区块链的主要价值主张
您将使用安全的加密哈希函数使数据库不可变
因此,让我们开始吧,进入我们的故事。
认识主角安德烈(Andrej)。
安德烈是一家酒吧的老板,白天是一家软件开发人员。
安德烈(Andrej)厌倦了:
编程可靠的老式PHP / Java / Javascript应用程序
忘记他的朋友和客户欠他星期五晚上所有未付的伏特加酒花多少钱
花费时间收集和计数硬币,退还找零钱并且通常接触COVID-19暴露的银行票据
维护用于桌上足球,飞镖,台球和扑克的不同塑料芯片
安德烈希望:
对酒吧的活动和销售有完善的可审计历史,以使其酒吧符合税收法规
将他的律师事务所转变为客户可以信任并从中获利的自治,付款高效,去中心化和安全的环境
他的目标是编写一个简单的程序,并以虚拟形式保留其客户的所有余额。
安德烈分享他的想法:
“每个新客户都会给我现金,我会把他们等值的数字代币(硬币/加密货币)记入贷方。代币将代表条形图内外的货币单位。
用户将把令牌用于所有酒吧功能,包括支付酒水,将其借贷给朋友,打乒乓球,玩扑克和踢球。
拥有由区块链令牌驱动的酒吧将为我的客户带来大量价值。与我在这条街上的竞争对手和其他酒吧相反,那里的顾客只花钱并得到宿醉作为交换,我持有酒吧代币的酒吧顾客将拥有股东权利。
与在Apple或Microsoft这样的公司中拥有大量股票类似,持有这些条形码令牌的客户将能够通过投票并决定以下内容来决定条形码的运作方式:
饮料价格
营业时间
新功能(电视,自动点唱机...)
室内和外部设计
利润分配
等等。
哦,这将是一个编程梦想!我将这些代币称为:区块链酒吧代币,“TBB!”
目录
要求
设置项目
01 | MVP数据库
02 | 突变全球数据库状态
03 | 整体事件与交易
04 | 不可改变的
要求
让我们深入研究我们的教程。我推荐2年以上Java / PHP / Javascript或其他类似Go语言的编程经验。
您也可以完成A Tour Of Go的官方讲座,以熟悉语言语法和基本概念(约20分钟)。
像区块链一样,这对于您的整体编程职业来说是一种了不起的技术。Go是一种流行的语言,Go开发人员的薪水比平均Java / PHP / Javascript职位高。
Go针对多核CPU架构进行了优化。您可以毫无问题地生成数千个轻量级线程(Go例程)。对于高度并行和并发的软件(例如区块链网络),这是极其实用的。
通过用Go编写软件,您可以立即获得接近C ++的性能,而不会因为忘记释放内存而丧生。
Go还可以编译为二进制文件,这使其非常易于移植。
设置项目
本文有一个专用的开源Github存储库,其中包含完整的源代码,因此您可以编译代码并在自己的本地计算机上运行该程序。
01 | MVP数据库
现在安德烈赶上创新并开始构建Web 3.0软件的时候了。
幸运的是,安德烈在上周阅读了《精益创业》(The Lean Startup)一书后,觉得他现在还不应该过度设计解决方案。因此,他为酒吧的MVP数据库选择了一个简单但有效的JSON文件。
区块链是一个数据库。
安德烈生成1M实用程序令牌
git checkout c1_genesis_json
在区块链世界中,令牌是区块链数据库内部的单位。他们的美元或欧元实际价值会根据其需求和知名度而波动。
每个区块链都有一个“ Genesis”文件。Genesis文件用于将第一个令牌分发给早期的区块链参与者。
这一切都始于一个简单的虚拟的genesis.json。
Andrej创建了一个文件
./database/genesis.json
他在其中定义Blockchain Bar的数据库将具有1M令牌,并且所有令牌都属于安德烈
{ "genesis_time": "2019-03-18T00:00:00.000000000Z", "chain_id": "the-blockchain-bar-ledger", "balances": { "andrej": 1000000 }}
令牌需要具有真正的“实用性”,即用例。从第一天起,用户就可以使用他们付款!
安德烈必须遵守法律法规(SEC)。发行未注册的安全性是非法的。另一方面,实用程序令牌很好,因此他立即在酒吧的门上打印并粘贴了新的定价白色海报。
安德烈为代币分配初始货币价值,以便他可以将其兑换为欧元,美元或其他法定货币。
1 TBB token = 1€| Item | Price || ------------------------- | ------- || Vodka shot | 1 TBB || Orange juice | 5 TBB || Burger | 2 TBB || Crystal Head Vodka Bottle | 950 TBB |
Andrej还决定,他每天应该获得100个令牌,以维护数据库并拥有如此出色的想法。
有趣的事实
以太坊区块链上的第一个起源以太(ETH)是以与安德烈的效用代币相同的方式创建并分发给早期投资者和开发商的。2017年,在以太坊区块链网络上的ICO(初始代币发行)热潮期间,项目创始人撰写并向投资者介绍了白皮书。白皮书是一份技术文档,概述了一个复杂的问题和可能的解决方案,旨在教育和阐明特定问题。在区块链世界中,白皮书用于概述特定区块链一旦开发后将如何表现和行为的规范。区块链项目为每个白皮书构想筹集了1000万至3亿欧元。为了换钱(ICO“资金”),投资者名称将包括在初始的“创始余额”中,这与安德烈的做法类似。投资者通过ICO寄予的希望是,创始币的价值有所上升,并且团队可以提供概述的区块链。当然,并非所有白皮书的想法都能实现。由于不清楚或不完整的想法而失去的大量投资是为什么区块链在整个ICO中受到媒体的负面报道,以及为什么有些人仍然认为这是炒作。但是底层的区块链技术是神奇而有用的,因为您将在本书中进一步学习。
您在Genesis文件中定义的令牌供应,初始用户余额和全局区块链设置。
突变全球数据库状态
git checkout c2_db_changes_txt
经过一周的工作,酒吧设施已准备就绪,可以接受代币。不幸的是,没有人出现,所以安德烈为自己订购了三杯伏特加酒,并将数据库更改写在一张纸上:
andrej-3; // 3 shots of vodkaandrej+3; // technically purchasing from his own barandrej+700; // Reward for a week of work (7x100 per day)
为了避免重新计算每个客户余额的最新状态,安德烈创建了一个以汇总格式存储余额的文件。
./database/state.json
新的数据库状态:
{ "balances": { "andrej": 1000700 }}
为了将流量带入他的酒吧,安德烈宣布了在接下来的24小时内向购买TBB代币的每个人提供100%的独家奖励。
他得到了第一个客户认购。汤姆预购了价值1000欧元的代币,为了庆祝,他立即花了1个TBB买了酒水。andrej-2000; // transfer to BabaYaga
babayaga+2000; // pre-purchase with 100% bonusbabayaga-1;andrej+1;andrej+100; // 1 day of sun coming up
新的数据库状态:
{ "balances": { "andrej": 998801, "babayaga": 1999 }}
您在Genesis文件中定义的令牌供应,初始用户余额和全局区块链设置。
整体事件与交易
git checkout c3_state_blockchain_component
习惯了事件源架构的开发人员必须立即认识到交易背后的熟悉原理。
区块链交易代表一系列事件,数据库是按特定顺序重播所有交易后的最终汇总,计算状态。
当安德烈误删文件,他在terminal中键入删除命令路径时过早按下Enter键
sudo rm -rf /
他的所有文件,包括酒吧的文件都消失了。
但他并没有惊慌!
genesis.json
虽然他没有备份,但他有更好的东西–一张包含所有数据库事务的纸。他唯一需要做的就是一个接一个地重放所有事务,他的数据库状态将恢复。
他对基于事件的体系结构的优势印象深刻,因此决定扩展其MVP数据库解决方案。每个酒吧的活动,例如个人饮料购买,都必须记录在区块链数据库中。
每个客户将使用帐户结构在数据库中代表:
type Account string
每个事务(TX-数据库更改)将具有以下四个属性:从,到,值和数据。
具有一个可能值(奖励)的数据属性捕获了安德烈发明区块链的奖金,并人为地增加了初始TBB代币的总供应量(通货膨胀)。
type Tx struct { From Account `json:"from"` To Account `json:"to"` Value uint `json:"value"` Data string `json:"data"`}func (t Tx) IsReward() bool { return t.Data == "reward"}
在一个JSON文件:
{ "genesis_time": "2019-03-18T00:00:00.000000000Z", "chain_id": "the-blockchain-bar-ledger", "balances": { "andrej": 1000000 }}
所有以前写在纸上的交易都将存储在一个名为tx.db的本地文本文件数据库中,该数据库以JSON格式序列化并以换行符分隔:
{"from":"andrej","to":"andrej","value":3,"data":""}{"from":"andrej","to":"andrej","value":700,"data":"reward"}{"from":"andrej","to":"babayaga","value":2000,"data":""}{"from":"andrej","to":"andrej","value":100,"data":"reward"}{"from":"babayaga","to":"andrej","value":1,"data":""}
封装所有业务逻辑的最关键的数据库组件将是State:
type State struct { Balances map[Account]uint txMempool []Tx dbFile *os.File}
该结构将了解所有用户余额以及谁将TBB令牌转让给了谁,以及转让了多少。
State
它是通过从文件中读取初始用户余额来构造的:
genesis.json
func NewStateFromDisk() (*State, error) { // get current working directory cwd, err := os.Getwd() if err != nil { return nil, err } genFilePath := filepath.Join(cwd, "database", "genesis.json") gen, err := loadGenesis(genFilePath) if err != nil { return nil, err } balances := make(map[Account]uint) for account, balance := range gen.Balances { balances[account] = balance }
之后,
State
通过所有数据库来更新创始余额
tx.db
txDbFilePath := filepath.Join(cwd, "database", "tx.db") f, err := os.OpenFile(txDbFilePath, os.O_APPEND|os.O_RDWR, 0600) if err != nil { return nil, err } scanner := bufio.NewScanner(f) state := &State // Iterate over each the tx.db file's line for scanner.Scan() { if err := scanner.Err(); err != nil { return nil, err } // Convert JSON encoded TX into an object (struct) var tx Tx json.Unmarshal(scanner.Bytes(), &tx) // Rebuild the state (user balances), // as a series of events if err := state.apply(tx); err != nil { return nil, err } } return state, nil}
State
该组件负责:
向Mempool添加新交易
根据当前状态验证交易(发件人余额充足)
改变状态
将事务持久化到磁盘
通过重播自创世纪以来的所有交易来计算帐户余额
向Mempool添加新交易:
func (s *State) Add(tx Tx) error { if err := s.apply(tx); err != nil { return err } s.txMempool = append(s.txMempool, tx) return nil}
将事务持久化到磁盘:
func (s *State) Persist() error { // Make a copy of mempool because the s.txMempool will be modified // in the loop below mempool := make([]Tx, len(s.txMempool)) copy(mempool, s.txMempool) for i := 0; i < len(mempool); i++ { txJson, err := json.Marshal(s.txMempool[i]) if err != nil { return err } if _, err = s.dbFile.Write(append(txJson, '\n')); err != nil { return err } // Remove the TX written to a file from the mempool // Yes... this particular Go syntax is a bit weird s.txMempool = append(s.txMempool[:i], s.txMempool[i+1:]...) } return nil}
更改,验证状态:
func (s *State) apply(tx Tx) error { if tx.IsReward() { s.Balances[tx.To] += tx.Value return nil } if tx.Value > s.Balances[tx.From] { return fmt.Errorf("insufficient balance") } s.Balances[tx.From] -= tx.Value s.Balances[tx.To] += tx.Value return nil}
构建命令行界面(CLI)
安德烈希望有一种便捷的方法将新交易添加到他的数据库并列出其客户的最新余额。由于Go程序会编译为二进制文件,因此他会为其程序构建一个CLI。
在Go中开发基于CLI的程序的最简单方法是使用第三方。
github.com/spf13/cobra
Andrej为其项目初始化Go的内置依赖项管理器,即
go modules
cd $GOPATH/src/github.com/web3coach/the-blockchain-way-of-programming-newsletter-edition
go mod init github.com/web3coach/the-blockchain-way-of-programming-newsletter-edition
Go modules
该命令将自动获取您在Go文件中引用的所有库。
Andrej创建了一个名为:的新目录,
cmd
并带有一个子目录
tbb
mkdir -p ./cmd/tbb
他在其中创建了一个文件
main.go
用作程序的CLI入口点:
package mainimport ( "github.com/spf13/cobra" "os" "fmt")func main() { var tbbCmd = &cobra.Command{ Use: "tbb", Short: "The Blockchain Bar CLI", Run: func(cmd *cobra.Command, args []string) { }, } err := tbbCmd.Execute() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) }}
Go程序使用cmd编译:
install
go install ./cmd/tbb/...
go: finding github.com/spf13/cobra v1.0.0go: downloading github.com/spf13/cobra v1.0.0go: extracting github.com/spf13/cobra v1.0.0
Go将检测缺少的库并在编译程序之前自动获取它们。根据您的结果,程序将保存在
$GOPATH
$GOPATH/bin
该文件夹中。
echo $GOPATH
/home/web3coach/go
which tbb
/home/web3coach/go/bin/tbb
tbb
你现在可以从终端运行,但由于文件中的函数为空,因此它什么也不做。
Run
main.go
首先需要的是对其CLI程序的版本控制。
tbb
main.go
在文件旁边,他创建了一个命令:
version.go
package mainimport ( "fmt" "github.com/spf13/cobra")const Major = "0"const Minor = "1"const Fix = "0"const Verbal = "TX Add && Balances List"var versionCmd = &cobra.Command{ Use: "version", Short: "Describes version.", Run: func(cmd *cobra.Command, args []string) { fmt.Printf("Version: %s.%s.%s-beta %s", Major, Minor, Fix, Verbal) },}
编译并运行它:
go install ./cmd/tbb/...
tbb version
Version: 0.1.0-beta TX Add && Balances List
version.go
与文件相同,他创建了一个文件:
balances.go
func balancesCmd() *cobra.Command { var balancesCmd = &cobra.Command{ Use: "balances", Short: "Interact with balances (list...).", PreRunE: func(cmd *cobra.Command, args []string) error { return incorrectUsageErr() }, Run: func(cmd *cobra.Command, args []string) { }, } balancesCmd.AddCommand(balancesListCmd) return balancesCmd}
balances
该命令将负责加载最新的数据库状态并将其打印到标准输出:
var balancesListCmd = &cobra.Command{ Use: "list", Short: "Lists all balances.", Run: func(cmd *cobra.Command, args []string) { state, err := database.NewStateFromDisk() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } defer state.Close() fmt.Println("Accounts balances:") fmt.Println("__________________") fmt.Println("") for account, balance := range state.Balances { fmt.Println(fmt.Sprintf("%s: %d", account, balance)) } },}
安德烈验证cmd是否按预期工作。由于该文件仍为空,
tx.db
因此应打印在Genesis文件中定义的确切余额。
go install ./cmd/tbb/...
tbb balances list
Accounts balances:__________________andrej: 1000000
现在,他只需要一个cmd即可记录酒吧的活动。
./cmd/tbb/tx.go
安德烈创建cmd:
func txCmd() *cobra.Command { var txsCmd = &cobra.Command{ Use: "tx", Short: "Interact with txs (add...).", PreRunE: func(cmd *cobra.Command, args []string) error { return incorrectUsageErr() }, Run: func(cmd *cobra.Command, args []string) { }, } txsCmd.AddCommand(txAddCmd()) return txsCmd}
tbb tx add
在CMD用途
State.Add(tx)
为坚持在酒吧的事件到文件系统功能:
func txAddCmd() *cobra.Command { var cmd = &cobra.Command{ Use: "add", Short: "Adds new TX to database.", Run: func(cmd *cobra.Command, args []string) { from, _ := cmd.Flags().GetString(flagFrom) to, _ := cmd.Flags().GetString(flagTo) value, _ := cmd.Flags().GetUint(flagValue) fromAcc := database.NewAccount(from) toAcc := database.NewAccount(to) tx := database.NewTx(fromAcc, toAcc, value, "") state, err := database.NewStateFromDisk() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } // defer means, at the end of this function execution, // execute the following statement (close DB file with all TXs) defer state.Close() // Add the TX to an in-memory array (pool) err = state.Add(tx) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } // Flush the mempool TXs to disk err = state.Persist() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } fmt.Println("TX successfully added to the ledger.") }, }
tbb tx add
在CMD有3个强制性标志。
--from
--to
--value
cmd.Flags().String(flagFrom, "", "From what account to send tokens")cmd.MarkFlagRequired(flagFrom)cmd.Flags().String(flagTo, "", "To what account to send tokens")cmd.MarkFlagRequired(flagTo)cmd.Flags().Uint(flagValue, 0, "How many tokens to send")cmd.MarkFlagRequired(flagValue)return cmd
CLI已完成!
安德烈j将所有交易从纸本迁移到他的新数据库:
tbb tx add --from=andrej --to=andrej --value=3
tbb tx add --from=andrej --to=andrej --value=700
tbb tx add --from=babayaga --to=andrej --value=2000
tbb tx add --from=andrej --to=andrej --value=100 --data=reward
tbb tx add --from=babayaga --to=andrej --value=1
从磁盘读取所有TX,并计算最新状态:
tbb balances list
Accounts balances:__________________andrej: 998801babayaga: 1999
数据成功恢复!
关于Cobra CLI库
Cobra
用于CLI编程的lib的好处是它附带的其他功能。例如,您现在可以运行cmd,
tbb help
它将打印出所有TBB注册的子命令,以及有关如何使用它们的说明。
tbb helpThe Blockchain Bar CLIUsage: tbb [flags] tbb [command]Available Commands: balances Interact with balances (list...). help Help about any command tx Interact with txs (add...). version Describes version.Flags: -h, --help help for tbbUse "tbb [command] --help" for more information about a command.
区块链通过分散数据存储来解决此问题。安德烈通过跳过对标记为奖励的TX的余额验证来融入程序。比特币和以太坊以相同的方式工作。开采区块的账户余额突然增加,这是总代币供应膨胀影响整个链条的主题。Andrej选择了名称和设计来匹配简化的以太坊模型,因此您可以一窥以太坊核心源代码。
区块链是一个数据库。代币供应,初始用户余额和全局区块链设置在Genesis文件中定义。创世余额表明最初的区块链状态是什么,以后再也不会更新。
数据库状态更改称为事务(TX)。事务是老式的事件,代表系统内的动作。
不可改变的数据库
git checkout c6_immutable_hash
技术难度从本节开始!这些概念只会变得更具挑战性,但同时也非常令人兴奋。
如何编程一个不变的数据库?
安德烈如果想弄清楚如何编写一个不变的DB,他必须意识到为什么其他数据库系统在设计上是可变的。
分析一个全能的MySQL数据库表:
| id | name | balance || -- | -------- | ------- || 1 | Andrej | 998951 || 2 | BabaYaga | 949 | | 3 | Caesar | 1000 |
在MySQL DB中,具有访问权限并有充分理由的任何人都可以执行表更新,例如:
UPDATE user_balance SET balance = balance + 100 WHERE id > 1
跨表更新值是可能的,因为表行是独立的,可变的,并且最新状态不明显。
最新的数据库更改是什么?最后一栏改变了?最后一行插入?如果是这样,Andrej如何知道最近删除了哪一行?如果行和表的状态紧密耦合,相互依赖(也称为更新),则更新第1行将生成一个全新的不同表,安德烈将实现其不变性。
您如何判断数据库中的任何字节是否已更改?
通过函数实现不变性
散列是获取任意长度的字符串输入并生成固定长度的散列字符串的过程。输入的任何更改都将导致新的不同的值。
package mainimport ( "crypto/sha256" "fmt")func main() { balancesHash := sha256.Sum256([]byte("| 1 | Andrej | 99895 |")) fmt.Printf("%x\n", balancesHash) // Output: 6a04bd8e2...f70a3902374f21e089ae7cc3b200751 // Change balance from 99895 -> 99896 balancesHashDiff := sha256.Sum256([]byte("| 1 | Andrej | 99896 |")) fmt.Printf("%x\n", balancesHashDiff) // Output: d04279207...ec6d280f6c7b3e2285758030292d5e1}
安德烈还要求其数据库具有某种程度的安全性,因此他决定使用具有以下属性的加密散列函数:
它是确定性的-相同的消息始终导致相同的值
快速计算任何给定消息的值
根据其值生成消息是不可行的,除非尝试所有可能的消息
对消息进行很小的更改就应该广泛更改值,以使新的值看起来与旧的值不相关
找到具有相同值的两个不同消息是不可行的
每当新事务持久化时
Persist()
安德烈都会修改该函数以返回新的内容
Snapshot
type Snapshot [32]byte
Snapshot
该由这个新的生产函数
sha256 secure hashing
func (s *State) doSnapshot() error { // Re-read the whole file from the first byte _, err := s.dbFile.Seek(0, 0) if err != nil { return err } txsData, err := ioutil.ReadAll(s.dbFile) if err != nil { return err } s.snapshot = sha256.Sum256(txsData) return nil}
doSnapshot()
在被修改后的被叫功能。
Persist()
将新事务写入文件时,
tx.db
Persist()
将对整个文件内容进行处理并返回其32个字节的“指纹”。
实践时间
运行cmd,
tbb balances list
并检查天平是否匹配。
tbb balances list
Account balances at 7d4a360f465d...| id | name | balance || -- | -------- | ------- || 1 | Andrej | 999251 || 2 | BabaYaga | 949 | | 3 | Caesar | 1000 |
从中删除最后两行,
./database/tx.db
然后再次检查天平。
tbb balances list
Account balances at 841770dcd3...| id | name | balance || -- | -------- | ------- || 1 | Andrej | 999051 || 2 | BabaYaga | 949 | | 3 | Caesar | 1000 |
完毕!
所以如果您在自己的计算机上执行确切的步骤,则将生成完全相同的数据库状态。
微信扫描关注公众号,及时掌握新动向
2.本文版权归属原作所有,仅代表作者本人观点,不代表比特范的观点或立场
2.本文版权归属原作所有,仅代表作者本人观点,不代表比特范的观点或立场