0%

区块链实验

本学期相对比较比较比较有意思的一门课,要把学习的过程记录下来。

Week1

环境安装

安装go环境,下载jb公司的Goland(半年前下载的),自带了一个go 16.8 windows/amd64版本,在linux下

1
sudo apt install golang-go

一键就能装好新版本,是go version go1.13.8 linux/amd64

然后配置gopath,gopath的作用是存放sdk以外的第三方类库和自己复用的代码,一般GOPATH分为Global GOPATH和Project GOPATH,而gopath里包含三个文件:src(源代码),pkg(中间文件),bin(可执行文件),具体的我还没搞明白,反正目前长这样:

image-20211019134251450

然后在Run/Debug Configurations里选用go build就能编译代码了。

我的helloworld:

1
2
3
4
5
6
package main
import "fmt"

func main(){
fmt.Printf("hello world\n")
}

GO初学踩坑

bx

发现了一个好东西,叫做bitcoin-explorer,中文名是区块链-比特币浏览器,它是一个独立的、跨平台的比特币命令行工具,方便我debug用的,但是我在linux上没下到。

网址是https://blockexplorer.com/

Bitcoin Command Line Tool:命令行下载网址:https://github.com/libbitcoin/libbitcoin-explorer

防止翻车我去下到我的虚拟机里了,需要进行一波巨长的install.sh,最后一波有连续六个git clone,能不能克隆下来完全靠脸,我最高记录是到第五轮寄了,要改的话得去改800多行的.sh文件,但我懒得改了,就这样吧。

文件位置

设置好gopath后,为了调用golang.org/x/crypto/ripemd160这个函数,我把golang.org这个文件夹放在了$GOPATH/src下,但是仍然无法在我的项目下build,一直显示这个错误:

1
2
no required module provides package golang.org/x/crypto/ripemd160; to add it
go get golang.org/x/crypto/ripemd160

当时就一脸懵逼,我不是库都装好了吗,为啥他找不到,最后的解决是我把项目文件放到$GOPATH/src下的同目录下了,终于能进行正常的import了。

package问题

当你做这个实验,你要整个项目都以package形式build的话,你要修改你的package main,表示这是一个入口文件,然后才能go build.

等你实验做完了,要把当成一个package了,以后想用这个函数,再把package name改成原来那个就行。

实验

三个数lcm

秒了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func gcd(a, b int) int {
if b == 0 {
return a
}
return gcd(b, a%b)
}
func lcm(a, b int) int {
return a * b / gcd(a, b)
}
func three_lcm_test() {
var a, b, c int
fmt.Scan(&a, &b, &c)
fmt.Printf("%d", lcm(lcm(a, b), c))
}

生成比特币交易地址

image-20211022133136523

收集到的比特币公私钥和地址各种格式前缀

种类 版本前缀 (hex)
Bitcoin Address 0x00
Pay-to-Script-Hash Address 0x05
Bitcoin Testnet Address 0x6F
Private Key WIF 0x80
BIP38 Encrypted Private Key 0x0142
BIP32 Extended Public Key 0x0488B21E

在本次实验中我们选用的Bitcoin Testnet Address,故版本前缀为0x6F

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
"base58"
"crypto/sha256"
"encoding/hex"
"fmt"
"golang.org/x/crypto/ripemd160"
)

var VersionByte = []byte("\x6f")

func Sha256(src []byte) []byte {
m := sha256.New()
m.Write(src)
return m.Sum(nil)
}
func Ripemd160(src []byte) []byte {
m := ripemd160.New()
m.Write(src)
return m.Sum(nil)
}
func Hash160(src []byte) []byte {
return Ripemd160(Sha256(src))
}
func Hash256(src []byte) []byte {
return Sha256(Sha256(src))
}
func Base58(src []byte) string {
myAlphabet := base58.BitcoinAlphabet
encodedString := base58.Encode(src, myAlphabet)
return encodedString
}
func getAddress(src []byte) string{
fingerprint := Hash160(src)
Checksum := Hash256((append(VersionByte, fingerprint...)))[:4]
var base = append(append(VersionByte, fingerprint...), Checksum...)
return Base58(base)
}
func TestURL() {
var data string
fmt.Scanf("%s", &data)
pubkey, _ := hex.DecodeString(data)
fmt.Printf("%s\n", getAddress(pubkey))
}

在代码实现的时候纠结了一会[]byte和string的事情,还有上来不知道hex.DecodeString,还自己手动对读进来的字符串进行两两分组然后sscanf..

Merkle Tree

可以简单理解为一棵二叉树的哈希,只有叶节点存交易数据块,如果订单发生改变,那么这条链的哈希值都会发生改变,所以在分析哪里发生交易了,就可以从根节点往下dfs,一直往哈希值改变的儿子走,走$log$次就能走到发生改变的节点。

于是实现的时候采用了动态开节点,为了方便定位叶子节点的位置,所以在传参的时候传了个当前编号,编号规则类似与线段树,在比较两棵树的时候就往下递归查询即可,compareMerkleTree函数会返回发生数据变化的节点块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package main

import (
"encoding/hex"
"fmt"
)

type MerkleNode struct {
lson *MerkleNode
rson *MerkleNode
Data []byte
}

func createNode(left, right *MerkleNode, data []byte) *MerkleNode {
now := MerkleNode{}
if left == nil && right == nil {
now.Data = Sha256(data)
} else {
mix := append(left.Data, right.Data...)
now.Data = Sha256(mix)
}
now.lson = left
now.rson = right
return &now
}
func build(depth, id, total int, params [][]byte) *MerkleNode {
if depth == total {
here := createNode(nil, nil, params[id-(1<<depth)])
return here
} else {
lson := build(depth+1, id<<1, total, params)
rson := build(depth+1, id<<1|1, total, params)
here := createNode(lson, rson, nil)
return here
}
}
func compareMerkleTree(tree1, tree2 *MerkleNode, depth, id, total int) int {
if depth == total {
if hex.EncodeToString(tree1.Data) != hex.EncodeToString(tree2.Data) {
return id - (1 << depth)
}
}
if tree1.lson != nil && tree2.lson != nil {
res := compareMerkleTree(tree1.lson, tree2.lson, depth+1, id<<1, total)
if res != -1 {
return res
}
}
if tree1.rson != nil && tree2.rson != nil {
res := compareMerkleTree(tree1.rson, tree2.rson, depth+1, id<<1|1, total)
if res != -1 {
return res
}
}
return -1
}

var weight_A = [][]byte{[]byte("\x00"), []byte("\x01"), []byte("\x02"), []byte("\x03"), []byte("\x04"), []byte("\x05"), []byte("\x06"), []byte("\x07"), []byte("\x08"), []byte("\x09"), []byte("\x0a"), []byte("\x0b"), []byte("\x0c"), []byte("\x0d"), []byte("\x0e"), []byte("\x0f")}
var weight_B = [][]byte{[]byte("\x00"), []byte("\x01"), []byte("\x03"), []byte("\x03"), []byte("\x04"), []byte("\x05"), []byte("\x06"), []byte("\x07"), []byte("\x08"), []byte("\x09"), []byte("\x0a"), []byte("\x0b"), []byte("\x0c"), []byte("\x0d"), []byte("\x0e"), []byte("\x0f")}

func MerkleTree_test() {
var Aroot = build(0, 1, 4, weight_A)
var Broot = build(0, 1, 4, weight_B)
fmt.Printf("%s\n", hex.EncodeToString(Aroot.Data))
fmt.Printf("%s\n", hex.EncodeToString(Broot.Data))
fmt.Printf("%d\n", compareMerkleTree(Aroot, Broot, 0, 1, 4))
}

拓展实验

挖矿,用自己的学号当种子算私钥,然后再算公钥,再用第二份算地址的代码算base58,如果里面包含ccc的子串,那就提交到网址,获取小额测试用比特币。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package main

import (
"crypto/ecdsa"
"crypto/elliptic"
"encoding/hex"
"fmt"
"math/rand"
"strings"
)
var src = rand.New(rand.NewSource(xxxx))

func RandStringBytesMaskImprSrc(n int) string {
b := make([]byte, (n+1)/2)
if _, err := src.Read(b); err != nil {
panic(err)
}
return hex.EncodeToString(b)[:n]
}
func newKeyPair() (ecdsa.PrivateKey,[]byte){
curve :=elliptic.P256()
private,err :=ecdsa.GenerateKey(curve,strings.NewReader(RandStringBytesMaskImprSrc(50)))
if err !=nil{
fmt.Println("error")
}

pubkey :=append(private.PublicKey.X.Bytes(),private.PublicKey.Y.Bytes()...)
return *private,pubkey
}
func Checkccc(src string) bool {
length := len(src)
for i := 0; i < length - 2; i++ {
if src[i] == 'c' && src[i + 1] =='c' && src[i + 2] =='c' {
return true
}
}
return false
}
func MineCracker_test() {
for {
privatekey,public :=newKeyPair()
address := getAddress(public)
if Checkccc(address) == true {
fmt.Printf("%x\n",privatekey.D.Bytes())
fmt.Printf("%s\n",hex.EncodeToString(public))
fmt.Printf("%s\n", address)
break
}
}
}

在生成随机数环节费劲了一些周折,GenerateKey的第二个参数要求是io.reader类型,正好能对应上crypto/rand里的rand.reader函数,但可惜crypto/rand无法设置种子。math/rand可以设置种子,但是我们需要找到一个能对接上io.reader的rand函数填充进去。经过我不懈的努力,使劲的读go里面ecdsa的源码,了解到了这里面是从一个熵源中不断获取字节串然后转为私钥,所以我们需要生成一个生成足够长字符串的函数,然后利用strings.NewReader这个函数转化为输入流,即io.reader,源码中对字节串长度要求是50,所以我们生成一个长度为50的字符串即可。

以下是我的私钥和公钥

1
2
3
6969695c49dcc6b9871b1d65838faceb4727bb1ffa7b3a621b47fadf265d2b368
62d3a4bfe04b3d36356ca63ae50c50696e2c3bb366eae2f5349b0eda1bcf54b29288153f70581ab5dd7b0ed963998b7c0526eb93743d72f842768d50baee0372
mgnLo3xn5CzEyD3N6LKTg9GtcccvrixwXs

存疑一:我不知道公钥是啥格式,我这是128长度的,之前文件的样例是66长度的,是压缩公钥形式,具体形式待向助教求证。

存疑二: 用的是GO库里自带的P256曲线,别名是 secp256r1,好像应该用secp256k1??

此为我领取成功的截图。

image-20211021234404398

挖到了人生第一枚币!虽然是测试用的。

经过助教核实,公钥就要33字节的,于是我又花了一上午往我的goroot下安装第三方库,跑到go的官方文档去找到了

https://pkg.go.dev/github.com/BSNDA/PCNGateway-Go-SDK/pkg/util/crypto/secp256k1#pkg-variables,有我们熟悉的函数,接口可以直接对接。

至于压缩公钥的,有这个https://pkg.go.dev/github.com/decred/dcrd/dcrec/secp256k1#section-readme,但可惜调用不了,还有C的底层代码,懒得去装了,直接手写了一个压缩公钥的(又是特别丑的代码

为了彻底跑通这个代码还去补充了这个第三方库https://github.com/pkg/errors/blob/master/errors.go

把第三方库装齐了之后终于能跑通啦,这是全新的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package main

import (
"bytes"
"crypto/ecdsa"
"encoding/binary"
"encoding/hex"
"fmt"
"github.com/BSNDA/PCNGateway-Go-SDK/pkg/util/crypto/secp256k1"
"math/rand"
"strings"
)
var src = rand.New(rand.NewSource(xxxxxxxx))

func RandStringBytesMaskImprSrc(n int) string {
b := make([]byte, (n+1)/2)
if _, err := src.Read(b); err != nil {
panic(err)
}
return hex.EncodeToString(b)[:n]
}
func BytesToInt(b []byte) int {
bytesBuffer := bytes.NewBuffer(b)
var x int32
binary.Read(bytesBuffer, binary.BigEndian, &x)
return int(x)
}
func secp256() (ecdsa.PrivateKey,[]byte){
curve := secp256k1.SECP256K1()
privKey, err := ecdsa.GenerateKey(curve, strings.NewReader(RandStringBytesMaskImprSrc(50)))
if err !=nil{
fmt.Println("error")
}
Y := hex.EncodeToString(privKey.PublicKey.Y.Bytes())
length := len(Y)
var pubKey []byte
if Y[length - 1] % 2 == 1 {
pubKey = append([]byte("\x03"),privKey.PublicKey.X.Bytes()...)
} else {
pubKey = append([]byte("\x02"),privKey.PublicKey.X.Bytes()...)
}
return *privKey, pubKey
}
func Checkccc(src string) bool {
length := len(src)
for i := 0; i < length - 2; i++ {
if src[i] == 'c' && src[i + 1] =='c' && src[i + 2] =='c' {
return true
}
}
return false
}
func MineCracker_test() {
for {
privatekey,public :=secp256()
address := getAddress(public)
if Checkccc(address) == true {
fmt.Printf("%x\n",privatekey.D.Bytes())
fmt.Printf("%s\n",hex.EncodeToString(public))
fmt.Printf("%s\n", address)
break
}
}
}

改了个seed,挖到了地址,提交,成功获得0.001测试币,这次我真的拿到私钥了,这次绝对能用了。

1
2
3
326138383534xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx(私钥保密
038301a3c40bea42c622ecbeb453900aaad4124397993d2282781b4f2a5c32410d
mwaXcViKVmtYHomvL7eHh9pncccPtyyHrb

image-20211023135225388

队友s0uthwood推荐的另外一种装secp256k1更方便的方法:https://blog.csdn.net/lancefox/article/details/107321807

Week2

因为疫情重庆比赛计划被学校拦了,心态炸裂。

实验

构建区块

不得不说实验设计的是真的好,手把手教学。

这一个实验就让你写几行代码,一个区块要维护的信息有:时间戳,数据块,前一节点哈希,还有自身哈希,自身哈希由前三个计算得来,所以我们只需要把前三个拼起来算个哈希即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"crypto/sha256"
"time"
)

type Block struct {
Time int64
Data []byte
PrevHash []byte
Hash []byte
}

func NewBlock(data string, prevHash []byte) *Block {
block := &Block{time.Now().Unix(), []byte(data), prevHash, []byte{}}
block.SetHash()
return block
}

func (b *Block) SetHash() {
Hash := sha256.Sum256(append(append(b.PrevHash, IntToHex(b.Time)...), b.Data...))
b.Hash = Hash[:]
}

测试代码:

1
2
3
4
5
6
7
func block_test() {
block := NewBlock("Genesis Block", []byte{})
fmt.Printf("Prev. hash: %x\n", block.PrevHash)
fmt.Printf("Time: %s\n", time.Unix(block.Time, 0).Format("2006-01-02 15:04:05"))
fmt.Printf("Data: %s\n", block.Data)
fmt.Printf("Hash: %x\n", block.Hash)
}

输出成功截图

image-20211026111516645

实现一条链

这个就是字面意思,实现一个创世链头,实现一个往尾部插的函数,就结束了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

type Blockchain struct {
blocks []*Block
}

func (bc *Blockchain) AddBlock(data string) {
Hash := bc.blocks[len(bc.blocks) - 1].Hash
block := NewBlock(data, Hash)
bc.blocks = append(bc.blocks, block)
}

func NewGenesisBlock() *Block {
return NewBlock("Genesis Block", []byte(""))
}

func NewBlockchain() *Blockchain {
return &Blockchain{[]*Block{NewGenesisBlock()}}
}

实验截图:
image-20211026141111899

添加工作量证明模块

这个对区块计算哈希的方法进行了修改,新增了区块中nonce的属性,然后time和data的顺序都有变化,但我们不必到block.go里修改,因为这里的prepareData函数已经帮我们拼好了,我们只需要对这个函数的返回值算哈希就好了。

然后在Run这个函数的时候,我们就把nonce从0开始加,每次新算一遍hash,如果转成BigInt如果小于$2^{236}$就表示前20位为0了,工作量证明完毕,区块可以进行添加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package main

import (
"bytes"
"crypto/sha256"
"fmt"
"math/big"
)

const targetBits = 20

type ProofOfWork struct {
block *Block
target *big.Int
}

func NewProofOfWork(b *Block) *ProofOfWork {
target := big.NewInt(1)
target.Lsh(target, uint(256-targetBits))

pow := &ProofOfWork{b, target}

return pow
}

func (pow *ProofOfWork) prepareData(nonce int64) []byte {
data := bytes.Join(
[][]byte{
pow.block.PrevHash,
pow.block.Data,
IntToHex(pow.block.Time),
IntToHex(int64(targetBits)),
IntToHex(nonce),
},
[]byte{},
)

return data
}

func (pow *ProofOfWork) Run() (int64, []byte) {
var hashInt big.Int
var hash [32]byte
var nonce int64
nonce = 0
fmt.Printf("Mining the block containing \"%s\"\n", pow.block.Data)
for {
here := pow.prepareData(nonce)
hash = sha256.Sum256(here)
hashInt.SetBytes(hash[:])
if hashInt.Cmp(pow.target) < 0 {
break
}
nonce += 1
}
fmt.Printf("\r%x", hash)
fmt.Print("\n\n")
return nonce, hash[:]
}

func (pow *ProofOfWork) Validate() bool {
var hashInt big.Int
hashInt.SetBytes(pow.block.Hash)
if hashInt.Cmp(pow.target) < 0 {
return true
}
return false
}

同时区块部分代码修改, 重点在于SetHash,要经过一段时间的计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Block struct {
Time int64
Data []byte
PrevHash []byte
Hash []byte
Nonce int64
}

func NewBlock(data string, prevHash []byte) *Block {
block := &Block{time.Now().Unix(), []byte(data), prevHash, []byte{}, 0}
block.SetHash()
return block
}

func (b *Block) SetHash() {
pow := NewProofOfWork(b)
b.Nonce, b.Hash = pow.Run()
}

image-20211026194736013

阅读代码:添加数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
"fmt"
"github.com/boltdb/bolt"
)

type Blockchain_in_dbIterator struct {
currentHash []byte
db *bolt.DB
}

func (bc *Blockchain_in_db) Iterator() *Blockchain_in_dbIterator {
bci := &Blockchain_in_dbIterator{bc.tip, bc.db}

return bci
}

func (i *Blockchain_in_dbIterator) Next() *Block {
var block *Block

err := i.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
encodedBlock := b.Get(i.currentHash)
block = DeserializeBlock(encodedBlock)

return nil
})
if err != nil {
fmt.Println(err)
}

i.currentHash = block.PrevHash

return block
}

三个问题:

  • 为什么需要在block类中添加Serialize()和DeserializeBlock()两个函数?他们主要做了什么?

    序列化是将对象转化为字节串从而能存进数据库进行保存,而反序列化是能从字节串完全恢复出一个对象来。

  • 描述一下NewBlockchain()和NewBlock()的执行逻辑。

    打开数据库文件,检查是否存在一个区块链,若存在则创建实例, tip设置为最后一个区块hash,若不存在则创建创世区块,存进数据库,把key1设位创世块的hash,tip指向他。

  • Blockchain类中的tip变量是做什么用的?

    tip变量定义为从数据中读取出来的变量,当存在区块链时,tip设置为读取到的最后一个区块hash;当不存在区块链时,tip设置为创世区块的 hash。实际上可以理解tip为区块链的一种标识符。

  • 迭代器Interator是如何工作使得我们能够从数据库中遍历出区块信息的?

    数据库迭代器,通过迭代器的next操作遍历整个数据库,从而做到区块的遍历。

实现命令行接口

添加两个命令行参数:

  • listblocks 把当前链的所有节点的信息输出
  • newblock后跟-data 参数外加节点信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
package main

import (
"flag"
"fmt"
"github.com/boltdb/bolt"
"log"
"os"
"strconv"
)

const dbFile = "Blockchain_in_db_demo.db"
const blocksBucket = "blocks"

type Blockchain_in_db struct {
tip []byte
db *bolt.DB
}

func (bc *Blockchain_in_db) AddBlock(data string) {
var lastHash []byte

err := bc.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
lastHash = b.Get([]byte("l"))

return nil
})
if err != nil {
log.Panic(err)
}
//fmt.Printf("abbb %s %s\n",string(data), lastHash)
newBlock := NewBlock(data, lastHash)

err = bc.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
//fmt.Printf("aaa%s\n",string(newBlock.Hash))
err := b.Put(newBlock.Hash, newBlock.Serialize())
if err != nil {
log.Panic(err)
}

err = b.Put([]byte("l"), newBlock.Hash)
if err != nil {
log.Panic(err)
}

bc.tip = newBlock.Hash

return nil
})
if err != nil {
log.Panic(err)
}
}

func NewBlockchain_in_db() *Blockchain_in_db {
fmt.Print("No existing blockchain found. creating a new one...\n")
var tip []byte
db, err := bolt.Open(dbFile, 0600, nil)
if err != nil {
log.Panic(err)
}

err = db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))

if b == nil {
genesis := NewGenesisBlock()
b, err := tx.CreateBucket([]byte(blocksBucket))
if err != nil {
log.Panic(err)
}

err = b.Put(genesis.Hash, genesis.Serialize())
if err != nil {
log.Panic(err)
}

err = b.Put([]byte("l"), genesis.Hash)
if err != nil {
log.Panic(err)
}

tip = genesis.Hash
} else {
tip = b.Get([]byte("l"))
}

return nil
})

bc := Blockchain_in_db{tip, db}

return &bc
}

func blockchain_db_test() {
bc := NewBlockchain_in_db()
bc.AddBlock("Send 1 BTC to Ivan")
bc.AddBlock("Send 2 more BTC to Ivan")
bci := bc.Iterator()
for {
block := bci.Next()
fmt.Printf("Prev. hash: %x\n", block.PrevHash)
fmt.Printf("Data: %s\n", block.Data)
fmt.Printf("Hash: %x\n", block.Hash)
pow := NewProofOfWork(block)
fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate()))
fmt.Println()
if len(block.PrevHash) == 0 {
break
}
}
}

func list_blocks() {
bc := NewBlockchain_in_db()
bci := bc.Iterator()
for {
block := bci.Next()
fmt.Printf("Prev. hash: %x\n", block.PrevHash)
fmt.Printf("Data: %s\n", block.Data)
fmt.Printf("Hash: %x\n", block.Hash)
pow := NewProofOfWork(block)
fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate()))
fmt.Println()
if len(block.PrevHash) == 0 {
break
}
}
}

func Add(data string) {
bc := NewBlockchain_in_db()
bc.AddBlock(data)
fmt.Print("Success!")
}

func cli_mode() {

foostr := flag.NewFlagSet("newblock",flag.ExitOnError)
strValue := foostr.String("data","string","打印字符串")

switch os.Args[1] {
case "newblock":
foostr.Parse(os.Args[2:])
Add(*strValue)
case "listblocks":
list_blocks()

default:
fmt.Println("expected 'str' subcommands")
os.Exit(1)
}
}

实验截图:

image-20211102111924062

添加区块。

image-20211102112149554

可以看到区块已经被成功添加进来。

image-20211102112214484

Week3

本周开始好像就和GO语言没啥关系了,开始realworld。

但也因为个人原因,开始摆烂了,懒得做选做了。

实验一 熟悉 Bitcoin Core 的基本配置方法

在Linux上安装Bitcoincore15版本,这里不推荐最新的版本,据说删除了很多对新手友好的指令。

虽然指导书上给的windows安装包,但我怕把windows装炸了,于是装在虚拟机里了。

1
2
3
4
5
6
mkdir /wallet
cd /wallet
wget https://bitcoincore.org/bin/bitcoin-core-0.15.2/bitcoin-0.15.2-x86_64-linux-gnu.tar.gz
cd bitcoin-0.15.2/bin
ln -s /wallet/bitcoin-0.15.2/bin/bitcoind /usr/bin/bitcoind
ln -s /wallet/bitcoin-0.15.2/bin/bitcoin-cli /usr/bin/bitcoin-cli

成功安装好。

image-20211102165202133

/root/.bitcoin为默认存储数据位置,我们查看文件夹如下。

image-20211102173125963
bitcoin.conf内容如下:

1
2
dir=/wallet/datadir
regtest=1

image-20211102193032479

最终配置的三个.conf:

1
2
3
4
5
6
7
8
9
10
regtest=1
port=22222
rpcport=18332
addnode=127.0.0.1:22224
addnode=127.0.0.1:22226

rpcuser=alice

# rpc访问的password
rpcpassword=8PPL3gL3gAM967mies3E= #设置rpc接口的访问密码
1
2
3
4
5
6
7
8
9
10
regtest=1
port=22224
rpcport=18334
addnode=127.0.0.1:22222
addnode=127.0.0.1:22226

rpcuser=bob

# rpc访问的password
rpcpassword=8PPL3gL3gAM967mies3E= #设置rpc接口的访问密码
1
2
3
4
5
6
7
8
9
regtest=1
port=22226
rpcport=18336
addnode=127.0.0.1:22222
addnode=127.0.0.1:22224

rpcuser=network

rpcpassword=8PPL3gL3gAM967mies3E= #设置rpc接口的访问密码

同时运行三个终端后,我们查看debug.log,可以看到一些信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
2021-11-02 09:14:04 * Using 2.0MiB for block index database
2021-11-02 09:14:04 * Using 8.0MiB for chain state database
2021-11-02 09:14:04 * Using 440.0MiB for in-memory UTXO set (plus up to 286.1MiB of unused mempool space)
2021-11-02 09:14:04 init message: Loading block index...
2021-11-02 09:14:04 Opening LevelDB in /root/.bitcoin/regtest/blocks/index
2021-11-02 09:14:04 Opened LevelDB successfully
2021-11-02 09:14:04 Using obfuscation key for /root/.bitcoin/regtest/blocks/index: 0000000000000000
2021-11-02 09:14:04 LoadBlockIndexDB: last block file = 0
2021-11-02 09:14:04 LoadBlockIndexDB: last block file info: CBlockFileInfo(blocks=1, size=293, heights=0...0, time=2011-02-02...2011-02-02)
2021-11-02 09:14:04 Checking all blk files are present...
2021-11-02 09:14:04 LoadBlockIndexDB: transaction index disabled
2021-11-02 09:14:04 Opening LevelDB in /root/.bitcoin/regtest/chainstate
2021-11-02 09:14:04 Opened LevelDB successfully
2021-11-02 09:14:04 Using obfuscation key for /root/.bitcoin/regtest/chainstate: 3ee9ed6c7b1f986a
2021-11-02 09:14:04 Loaded best chain: hashBestChain=0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206 height=0 date=2011-02-02 23:16:42 progress=1.000000
2021-11-02 09:14:04 init message: Rewinding blocks...
2021-11-02 09:14:04 init message: Verifying blocks...
2021-11-02 09:14:04 block index 14ms
2021-11-02 09:14:04 init message: Loading wallet...
2021-11-02 09:14:04 nFileVersion = 150200
2021-11-02 09:14:04 Keys: 2002 plaintext, 0 encrypted, 2002 w/ metadata, 2002 total
2021-11-02 09:14:04 wallet 25ms
2021-11-02 09:14:04 setKeyPool.size() = 2000
2021-11-02 09:14:04 mapWallet.size() = 0
2021-11-02 09:14:04 mapAddressBook.size() = 1
2021-11-02 09:14:04 mapBlockIndex.size() = 1
2021-11-02 09:14:04 nBestHeight = 0
2021-11-02 09:14:04 Bound to [::]:18444
2021-11-02 09:14:04 Bound to 0.0.0.0:18444
2021-11-02 09:14:04 init message: Loading P2P addresses...
2021-11-02 09:14:04 Loaded 0 addresses from peers.dat 0ms
2021-11-02 09:14:04 init message: Loading banlist...
2021-11-02 09:14:04 init message: Starting network threads...
2021-11-02 09:14:04 init message: Done loading
2021-11-02 09:14:04 opencon thread start
2021-11-02 09:14:04 msghand thread start
2021-11-02 09:14:04 dnsseed thread start
2021-11-02 09:14:04 Loading addresses from DNS seeds (could take a while)
2021-11-02 09:14:04 0 addresses found from DNS seeds
2021-11-02 09:14:04 dnsseed thread exit
2021-11-02 09:14:04 torcontrol thread start
2021-11-02 09:14:04 addcon thread start
2021-11-02 09:14:04 net thread start
2021-11-02 09:14:04 Imported mempool transactions from disk: 0 successes, 0 failed, 0 expired
2021-11-02 09:15:05 Adding fixed seed nodes as DNS doesn't seem to be available.

我们可以看到:

存储链上交易状态初始化的数据空间为8Mib。

在初始化过程中,节点钱包密钥池最终保存了2000对密钥?

程序添加P2P的流程:在区块链中增加新的区块并与ip端口绑定,之后加载P2P地址,重建peers.dat和banlist.dat;启动网线程,完成加载,然后开启其他线程,寻找新鲜的更新信息,最后接受版本信息。

实验二 掌握常用 RPC 指令,利用回归测试网络实现挖矿与交易

利用 bitcoin-cli 指令实现简单的回归测试与网络挖矿及交易

由于使用的是linux,于是在.bashrc进行了alias配置,完成了命令的简化。

1
2
3
4
5
6
7
8
9
10
11
alias alice-d='bitcoind -regtest -conf=/root/.bitcoin/alice.conf -datadir=/root/.bitcoin/alice $*'
alias bob-d='bitcoind -regtest -datadir=/root/.bitcoin/bob $*'
alias network-d='bitcoind -regtest -datadir=/root/.bitcoin/network $*'

alias alice-cli='bitcoin-cli -regtest -conf=/root/.bitcoin/alice.conf -datadir=/root/.bitcoin/alice $*'
alias bob-cli='bitcoin-cli -regtest -conf=/root/.bitcoin/bob.conf -datadir=/root/.bitcoin/bob $*'
alias network-cli='bitcoin-cli -regtest -conf=/root/.bitcoin/network.conf -datadir=/root/.bitcoin/network $*'

alias alice-qt=bitcoin-qt -regtest -datadir=/root/.bitcoin/alice $*
alias bob-qt=bitcoin-qt -regtest -datadir=/root/.bitcoin/bob $*
alias network-qt=bitcoin-qt -regtest -datadir=/root/.bitcoin/network $*

同时运行4个终端。

image-20211108203059756

右边三个分别是alice,bob,network,要保证全程一直开启。

左侧的进程使用bitcoin-cli,先通过挖100次矿得到一笔巨款,然后分别查询network和bob的地址,然后支付给他俩,bob再挖一块获得确认,最后查看余额。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
root@ubuntu:~# alice-cli listaccounts
{
"": 8750.00000000
}
root@ubuntu:~# alice-cli getnewaddress
mvkdP7rdB2U45R5nNJApQxfrmDrVq5HBs6
root@ubuntu:~# bob-cli getnewaddress
mwQ53d8PBuVTY1PQdXznnh5E8wrL2waG4o
root@ubuntu:~# network-cli getnewaddress
mnJVRxFTdb9dLciYDtnQMyVTQdyfEjRU5C
root@ubuntu:~# alice-cli sendtoaddress "mnJVRxFTdb9dLciYDtnQMyVTQdyfEjRU5C" 1.5
5e5a42260cd71c9113a14e7264664b3149bfd49754694a68ab4f4cc82eeab05a
root@ubuntu:~# alice-cli sendtoaddress "mwQ53d8PBuVTY1PQdXznnh5E8wrL2waG4o" 2.5
0eaeb0b8c50dbdbf305b9966a255034856e4c8cf99ca435bc1a0ac579c90582a
root@ubuntu:~# bob-cli generate 1
[
"33c426060b1858bc9f96852c7898a9a7716d0548465439543d8634bca20c5e6a"
]
root@ubuntu:~# bob-cli listaccounts
{
"": 2.50000000
}
root@ubuntu:~# network-cli listaccounts
{
"": 1.50000000
}

image-20211102201622520

上述过程便是描述的全过程。

实验三 通过控制台与测试链进行更加丰富的交互

bitcoin-qt就是把刚才的操作全部变成图形化操作,这里显示的是既不是alice的,也不是bob的,所以没有显示余额。

image-20211108201423610

在help的debug窗口里能对json进行一个解析。

1
2
3
4
5
decoderawtransaction
"010000000156211389e5410a9fd1fc684ea3a852b8cee07fd15398689d99441b
98bfa76e290000000000ffffffff0280969800000000001976a914fdc79909566
42433ea75cabdcc0a9447c5d2b4ee88acd0e89600000000001976a914d6c49205
6f3f99692b56967a42b8ad44ce76b67a88ac00000000"

image-20211108201816009

可以看到,该交易的输入输出情况,输入包含交易id,交易序号,输出包含交易值,scrippubkey,有两个交易,第一个数据量大小为0.100000,另一个是0.09890000。

Week4

实验一 区块链浏览器的基本操作与功能

前往网站https://blockstream.info/block/000000000000000003dd2fdbb484d6d9c349d644d8bbb3cbfa5e67f639a465fe,获得了如下信息:

image-20211116232329805

image-20211116232339490

值得我们注意的是,第二个交易里每次都是0.00001BTC,一共进行了5569次,可以发现这里面存在着很多无用的垃圾交易,算是对服务器的一个DDOS攻击(类似),体现出了区块链系统在设计方面没有考虑这种价格极小,但是靠数量堆叠上去的垃圾订单,很影响服务器运行。

前往https://btc.com/stats/diff ,查看比特币挖矿难度变化的可视化实时结果。

image-20211116232738221

难度调整的间隔是:11天5小时

难度变化趋势:整体上难度是在增加,但是中间会有波动。

带来的影响:会使计算区块越来越困难

平均算力计算:用难度除以平均出块时间

调用api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PS C:\Users\FYHSSGSS> curl https://blockstream.info/api/mempool

StatusCode : 200
StatusDescription : OK
Content : {"count":10071,"vsize":3470658,"total_fee":19003372,"fee_histogram":[[26.302326,50134],[12.983334,5
0077],[10.573426,50207],[9.985863,79877],[8.196078,50075],[7.5089393,50009],[6.0842695,50052],[5.44
14...
RawContent : HTTP/1.1 200 OK
Vary: Accept-Encoding
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: x-total-results
Alt-Svc: clear
Content-Length: 1159
Cache-Control: public, max-age=10
Content...
Forms : {}
Headers : {[Vary, Accept-Encoding], [Access-Control-Allow-Origin, *], [Access-Control-Expose-Headers, x-total
-results], [Alt-Svc, clear]...}
Images : {}
InputFields : {}
Links : {}
ParsedHtml : mshtml.HTMLDocumentClass
RawContentLength : 1159

分析这个json的前一小部分即可,交易数目为10071,数据量为3470658,大约3470658/1024/1024=3个区块能处理这么多交易。

image-20211116233912303

高度在9991-10000间区块内包含的总交易数目为10个。

实验二

逐步分析:

初始占内有x,y,z,

第一步复制栈,得到x,y,z,x,y,z
第二步把两个栈顶加起来y+z,然后pushnum9然后equal
得到了等式y+z=9
然后类似的,每次弹两个,分别是z+x=7x+y=8
然后根据这三个方程解出来x,y,z

即为:

解出来便可以写出解锁脚本。

1
op_pushnum_3 op_pushnum_5 op_pushnum_4

实验三

实验截图如下:

查看指定区块详情

image-20211109203257688

探究ERC20代币合约;

image-20211109203331016

image-20211109203425860

image-20211109203645931

image-20211109203658797

image-20211109203714922

智能合约定义为一段部署在evm虚拟机中的代码,无法自动执行,需要人为的 触发才能执行,每执行一次需要发起这次执行的账户扣除对应的gas作为手续费, 智能合约与平时的代码其实没有什么区别,只是运行于一个以太坊这样的分布式 平台上而已。我们将通过下周的学习了解到如何写出自己的智能合约。

实验四 体验比特币靓号地址

简单的正则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PS C:\Users\FYHSSGSS\Documents\University\2021autumn\blockchain\区块链实验课\实验四\实验材料\vanitygen-0.22-win> .\vanitygen.exe -r 'ccc'
Pattern: ccc
Address: 1PHtXF1YL9vTVN4FcccorAYny4q7W9Q2mT
Privkey: 5KhRJHBQLNUjh4XGGMncydN9PaWECrMoRVFLvgmQonybbL6PFFc
PS C:\Users\FYHSSGSS\Documents\University\2021autumn\blockchain\区块链实验课\实验四\实验材料\vanitygen-0.22-win> .\vanitygen.exe -r '^11.*77$'
Pattern: ^11.*77$
Address: 11AXtLdPWRacsCqWbFwwUkBeuJF9DL477
Privkey: 5JeiqauTWKDjScF5w8ycFQnULKsxhQr2wWEUmDCKKrRij4NPWGZ
PS C:\Users\FYHSSGSS\Documents\University\2021autumn\blockchain\区块链实验课\实验四\实验材料\vanitygen-0.22-win> .\vanitygen.exe -r '\d\d\d$'
Pattern: \d\d\d$
Address: 18ykHEtYHquacgtM2TjxPTg3MvAwebF164
Privkey: 5KAQmGGH5xFVHvXmf8f8uKk7nmcXicqVPMie3fpD7z9VQLk2kxZ
PS C:\Users\FYHSSGSS\Documents\University\2021autumn\blockchain\区块链实验课\实验四\实验材料\vanitygen-0.22-win> .\vanitygen.exe -r '\d\d\d88$'
Pattern: \d\d\d88$
Address: 1KLPqwyvwuqiY7erifjjyUBrvaB1b81688
Privkey: 5JDQjKxQ56HKkLttr4BRoDq7PdmkALGxhjKNMB4i6nyLU7wjA54

实验五 体验在线生成不同种类的钱包地址

这里放一下实验截图就好。

普通钱包

image-20211116230455377

纸钱包

image-20211116230430698

虚荣钱包

image-20211116230527645

扩展3 藏头诗:

简单的正则。

1
2
3
4
5
6
7
8
import os
import re
target = 'experience'
for i, c in enumerate(target):
pattern = '^.{%d}%c.*$' % (i + 1, c)
p = os.popen('.\\vanitygen.exe -r "%s"' % (pattern))
res = re.search(r'Address: (.*)', p.read())
print(res[1])

image-20211116184235243

Week5

本周的实验内容是通过构建宠物游戏来进行智能合约编写,所用的语言是Solidity。

(搬一段教程的话)Solidity是一种静态类型的编程语言,用于开发在EVM上运行的智能合约。 Solidity被编译为可在EVM上运行的字节码。借由Solidity,开发人员能够编写出可自我运行其欲实现之商业逻辑的应用程序,该程序可被视为一份具权威性且永不可悔改的交易合约。对已具备程序编辑能力的人而言,编写Solidity的难易度就如同编写一般的编程语言。

课程团队太猛了,花了一年时间写了一个平台,非常好看!

在本实验中,我们选取的编译器版本是0.4.26+

实验一 solidity基础

按照教程一步一步写的,语法很简单,基本不用学。

下面的代码实现的是一个叫做Animallncubators的合约,动物有名字,还有一个独立的DNA。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
pragma solidity >=0.4.12 <0.6.0;

contract Animallncubators {
uint dnaDigits = 16;
uint dnaLength = 10**16;
struct Animal {
uint dna;
string name;
}
Animal[] public animals;
event NewAnimal(uint AnimalId, string name, uint dna);
function _createAnimal(string _name, uint _dna) private {
animals.push(Animal(_dna, _name));
uint _animalId = animals.length - 1;
NewAnimal(_animalId, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint){
uint rand = uint(keccak256(_str));
return rand % dnaLength;
}
function createRandomAnimal(string _name) public {
uint randDna = _generateRandomDna(_name);
_createAnimal(_name, randDna);
}
}

合约实现完成后,我们往后依次添加名为DrogonRheagalViserion的动物,call他们的ID就会显示他们的信息,比如名字,DNA什么的,如截图所示。

image-20211116200456986

image-20211116200516468

image-20211116200534109

实验二 Solidity进阶——宠物成长系统

在这个实验里,我们在上面的基础上继续扩展合约,新增了每个动物的主人,新建了一个动物ID与主人地址的映射,以及主人动物数量的映射,同时,在createRandomAnimal里限制了必须主人在没有动物的时候才能调用这个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
pragma solidity >=0.4.12 <0.6.0;

contract Animallncubators {
uint dnaDigits = 16;
uint dnaLength = 10**16;
struct Animal {
uint dna;
string name;
}
Animal[] public animals;
mapping(address=>uint) ownerAnimalCount;
mapping(uint=>address) AnimalToOwner;
event NewAnimal(uint AnimalId, string name, uint dna);
function _createAnimal(string _name, uint _dna) internal {
animals.push(Animal(_dna, _name));
uint _animalId = animals.length - 1;
AnimalToOwner[_animalId] = msg.sender;
ownerAnimalCount[msg.sender] += 1;
NewAnimal(_animalId, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint){
uint rand = uint(keccak256(_str));
return rand % dnaLength;
}
function createRandomAnimal(string _name) public {
require( ownerAnimalCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
_createAnimal(_name, randDna);
}
}

同时又新建了一个继承于Animallncubators的合约AnimalFeeding,文件名是AnimalFeeding.sol,这里面的feedAndGrow函数很神奇,吃了食物之后DNA融合,进化出一个新的动物,名字也改了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pragma solidity >=0.4.12 <0.6.0;
import "./Animallncubators.sol";

contract AnimalFeeding is Animallncubators{
function feedAndGrow(uint _AnimalId, uint _targetDna) internal {
require(keccak256(AnimalToOwner[_AnimalId]) == keccak256(msg.sender));
Animal storage myAnimal = animals[_AnimalId];
uint _petDna = _targetDna % dnaLength;
uint _newDna = uint((myAnimal.dna + _petDna) / 2);
_newDna = (_newDna / 100) * 100 + 99;
_createAnimal("No-one", _newDna);
}
function _catchFood(uint _name) internal pure returns (uint) {
uint rand = uint(keccak256(_name));
return rand;
}
function feedOnFood(uint _AnimalId, uint _FoodId) public{
uint _FoodDna = _catchFood(_FoodId);
feedAndGrow(_AnimalId, _FoodDna);
}

}

image-20211123141157370

执行第二只宠物的时候废了,因为一个地址只能有一只精灵。

image-20211123141245108

我们选取三个地址分别生成三只精灵,然后换回第一个地址,准备投喂。

image-20211123141905953

投喂成功,新诞生了一只No-one

image-20211116212721326

实验三 Solidity高阶理论

新建ownable.sol,这段代码的意义主要有三:

  • 创建合约,先写构造函数,将msg.sender(其部署者)设置为owner

  • 为它加上一个修饰符 onlyOwner,只有合约的所有者owner才能进行访问。

  • 允许将合约所有权转让给他人。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* @title Ownable
* @dev The Ownable contract has an owner address, and provides basic authorization control
* functions, this simplifies the implementation of "user permissions".
*/
contract Ownable {
address public owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

/**
* @dev The Ownable constructor sets the original `owner` of the contract to the sender
* account.
*/
function Ownable() public {
owner = msg.sender;
}

/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(msg.sender == owner);
_;
}

/**
* @dev Allows the current owner to transfer control of the contract to a newOwner.
* @param newOwner The address to transfer ownership to.
*/
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0));
OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
}

在此基础上,我们新增Animallncubators的一些功能,增加了投喂食物的冷却功能,两次食物间隔必须超过一分钟。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
pragma solidity >=0.4.12 <0.6.0;
import "./ownable.sol";

contract Animallncubators is Ownable{
uint dnaDigits = 16;
uint dnaLength = 10**16;
uint32 cooldownTime = 60;
struct Animal {
uint32 level;
uint32 readyTime;
uint dna;
string name;
}
Animal[] public animals;
mapping(address=>uint) ownerAnimalCount;
mapping(uint=>address) AnimalToOwner;
event NewAnimal(uint AnimalId, string name, uint dna);
function _createAnimal(string _name, uint _dna) internal {
animals.push(Animal(0, uint32(now), _dna, _name));
uint _animalId = animals.length - 1;
AnimalToOwner[_animalId] = msg.sender;
ownerAnimalCount[msg.sender] += 1;
NewAnimal(_animalId, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint){
uint rand = uint(keccak256(_str));
return rand % dnaLength;
}
function createRandomAnimal(string _name) public {
require( ownerAnimalCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
_createAnimal(_name, randDna);
}
}

在这里增加一分钟的限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pragma solidity >=0.4.12 <0.6.0;
import "./Animallncubators.sol";

contract AnimalFeeding is Animallncubators{
function feedAndGrow(uint _AnimalId, uint _targetDna) internal {
require(keccak256(AnimalToOwner[_AnimalId]) == keccak256(msg.sender));
Animal storage myAnimal = animals[_AnimalId];
require(now >= myAnimal.readyTime);
uint _petDna = _targetDna % dnaLength;
uint _newDna = uint((myAnimal.dna + _petDna) / 2);
_newDna = (_newDna / 100) * 100 + 99;
_createAnimal("No-one", _newDna);
myAnimal.readyTime = uint32(now) + cooldownTime;
}
function _catchFood(uint _name) internal pure returns (uint) {
uint rand = uint(keccak256(_name));
return rand;
}
function feedOnFood(uint _AnimalId, uint _FoodId) public{
uint _FoodDna = _catchFood(_FoodId);
feedAndGrow(_AnimalId, _FoodDna);
}
}

最后我们再写一个合约叫做AnimalHelper,实现一些辅助升级功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
pragma solidity >=0.4.12 <0.6.0;
import "./AnimalFeeding.sol";
contract AnimalHelper is AnimalFeeding{
modifier aboveLevel (uint _level, uint _AnimalId) {
require(animals[_AnimalId].level >= _level);
_;
}
function changeName(uint _AnimalId, string _newName) external aboveLevel(2, _AnimalId){
require(keccak256(msg.sender) == keccak256(AnimalToOwner[_AnimalId]));
animals[_AnimalId].name = _newName;
}
function changeDna(uint _AnimalId, uint _newDna) external aboveLevel(20, _AnimalId){
require(keccak256(msg.sender) == keccak256(AnimalToOwner[_AnimalId]));
animals[_AnimalId].dna = _newDna;
}
function getAnimalsByOwner(address _owner)external view returns(uint[]){
uint[] memory result = new uint[](ownerAnimalCount[_owner]);
uint8 count = 0;
for (uint i = 0; i < animals.length; i++) {
if (keccak256(AnimalToOwner[i]) == keccak256(_owner)) {
result[count] = i;
count += 1;
}
}
return result;
}
}

新建一个小动物,然后投喂。

image-20211123143304320

抓紧时间连续投喂两波

image-20211123144244104

发现第二波执行失败了。

image-20211123144251746

最后看一下getAnimalsByOwner这个函数。

image-20211123144428245

实验四 拓展实验4:solidity高阶篇

更新一版的Animallncubators,新增了攻击力,赢的次数,输的次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
pragma solidity >=0.4.12 <0.6.0;
import "./ownable.sol";

contract Animallncubators is Ownable{
uint dnaDigits = 16;
uint dnaLength = 10**16;
uint32 cooldownTime = 60;
struct Animal {
uint32 level;
uint32 readyTime;
uint dna;
uint ATK;
uint winCount;
uint lossCount;
string name;
}
Animal[] public animals;
mapping(address=>uint) ownerAnimalCount;
mapping(uint=>address) AnimalToOwner;
event NewAnimal(uint AnimalId, string name, uint dna);
function _createAnimal(string _name, uint _dna) internal {
animals.push(Animal(0, uint32(now), _dna, 1, 0, 0, _name));
uint _animalId = animals.length - 1;
AnimalToOwner[_animalId] = msg.sender;
ownerAnimalCount[msg.sender] += 1;
NewAnimal(_animalId, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint){
uint rand = uint(keccak256(_str));
return rand % dnaLength;
}
function createRandomAnimal(string _name) public {
require( ownerAnimalCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
_createAnimal(_name, randDna);
}
}

模拟对战环节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
pragma solidity >=0.4.12 <0.6.0;
import "./AnimalHelper.sol";

contract AnimalAttack is AnimalHelper{
uint randNonce = 0;
function winPro() returns(bool){
uint random = uint(keccak256(now, msg.sender, randNonce)) % 100;
randNonce++;
return (random < 75);
}

function fight(uint _attackAnimalId, uint _defenceAnimalId) external {
if (winPro()) {
animals[_attackAnimalId].ATK ++;
animals[_attackAnimalId].winCount ++;
animals[_defenceAnimalId].lossCount ++;
}
else {
animals[_attackAnimalId].lossCount ++;
animals[_defenceAnimalId].winCount ++;
}
animals[_attackAnimalId].readyTime = uint32(now);
animals[_defenceAnimalId].readyTime = uint32(now);
}
}

Week6

实验 1 会议报名登记系统的基本功能与实现

实现一个会议报名的智能合约,已经给的差不多了,只需要实现delegate和enrollfor函数即可。

其中,trustees是受托人到其委托人的一个映射,是一个不定长数组,每次被委托的人都往后push即可。

enrollFor替人报名,报名逻辑与正常enroll一样,唯一不同的是我们需要在trustees数组中找到委托人,然后以后都报名他。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function delegate(address addr) public{
trustees[addr].push(participants[msg.sender]);
}
function enrollFor(string memory username,string memory title) public returns(string memory){
uint index = 0;
for (uint i = 0; i < trustees[msg.sender].length; i++) {
if (keccak256(bytes(trustees[msg.sender][i].name)) == keccak256(bytes(username))) {
index = i;
break;
}
}
for (uint i = 0; i < conferences.length; i++){
if (keccak256(bytes(conferences[i].title)) == keccak256(bytes(title))){
require(conferences[i].current<conferences[i].max,"Enrolled full");
conferences[i].current = conferences[i].current+1;
if(conferences[i].current==conferences[i].max){
emit ConferenceExpire(title);
}
trustees[msg.sender][index].confs.push(title);
}
}
uint len = trustees[msg.sender][index].confs.length;
require(len>0,"Conference does not exist");
return trustees[msg.sender][index].confs[len-1];
}

问题

应在合约的哪个函数指定管理员身份?如何指定?

需要在合约的构造函数处制定,根据实际情景,应该是创建这个会议体制的人为管理员,所以指定admin为msg.sender。

在发起新会议时,如何确定发起者是否为管理员?简述 require()、assert()、 revert()的区别。

require(msg.sender==admin,"permission denied");

require和assert都是条件声明,这三种方式的执行逻辑是相同的:

require(msg.sender==admin,"permission denied"),assert(msg.sender==admin,"permission denied"),if(msg.sender!=admin) {revert("permission denied");}

区别在于revert和require在执行失败后会返还gas,但是assert就算执行失败了也会照样扣除gas。

简述合约中用 memory 和 storage 声明变量的区别

Storage 变量是指永久存储在区块链中的变量。 Memory 变量则是临时的,当外部函数对某合约调用完成时,内存型变量即被移除。 你可以把它想象成存储在你电脑的硬盘或是RAM中数据的关系。

一般情况下不用管这俩关键字, 默认情况下Solidity 会自动处理它们。 状态变量(在函数之外声明的变量)默认为“存储”形式,并永久写入区块链;而在函数内部声明的变量是“内存”型的,它们函数调用结束后消失。

然而也有一些情况下,你需要手动声明存储类型,主要用于处理函数内的 结构体和 数组 时。这两个属性主要用于优化代码以节省合约的gas消耗,目前只需知道有时需要显式地声明 storage 或 memory 即可。

实验二 学习用 Truffle 组件部署和测试合约

用npm安装truffle。

1
npm install truffle -g

之后写测试合约来鉴定功能是否正常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
pragma solidity >=0.4.25 <0.7.0;

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/Enrollment.sol";

contract TestEnrollment{
//测试注册后,是否返回正确注册信息(仅测试name即可)
function testSignUp() public {
Enrollment en = new Enrollment();
(string memory name, ) = en.signUp("alice","male");
(string memory expected_name, ) = ("alice","male");
Assert.equal(name,expected_name,"signup failed");
}

//测试管理员添加新会议是否成功
function testNewConf() public{
Enrollment enroll = new Enrollment();
string memory expected = "conf1";
Assert.equal(enroll.newConference("conf1","beijing",30),expected,"new conference failed");
}

//请测试enroll函数,确保当用户报名后,其已报名会议列表中有该会议。提示:不要忘记先由管理员创建会议。
function testEnroll() public{
Enrollment en = new Enrollment();
string memory expected = "conf1";
en.newConference("conf1","beijing",30);
Assert.equal(en.enroll("conf1"), expected,"Enrolled full");
}
}

代码写完后,进行测试。

1
truffle test

可以看到测试成功。

image-20211123200646354

1
truffle migrate

image-20211123200809853

合约部署成功。

部署之前会先进行链的初始化,会在本地保存在一个json文件中。之后需要进行合约 的编写,进行详细的合约规定。第三步需要对合约进行编译,将之前的json文件中的 ABI和EVM code进行获取编译。第四步实现合约的部署,将已经实现好的合约部署 到网络,如下截图:

image-20211130200446359

image-20211130201037162

image-20211130201133207

实验三 利用 Web3.js 实现合约与前端的结合

我们需要在:src/contracts/contract.js补充合约信息,以及src/component/xx组件/index.js 补充web3交互代码,其中conflistsignup中有简单的样例。

delegate部分:

1
2
3
4
5
6
7
8
9
10
11
const mapDispatchToProps = (dispatch) => {
return {
submit(address) {
contract.methods.delegate(address) //调用合约signUp方法
.send({from:window.web3.eth.accounts[0]},function(err,res){console.log(res)}) //function中的res为方法返回值
.then((res)=>console.log(res)); //该res为交易执行完后的具体交易信息,如TxHash等

dispatch({
type: 'submit_delegate'
})
},

signup部分:

1
2
3
4
5
6
7
8
9
10
11
12
const mapDispatchToProps = (dispatch) => {
return {
submit(username,extra) {
contract.methods.signUp(username, extra) //调用合约signUp方法
.send({from:window.web3.eth.accounts[0]},function(err,res){console.log(res)}) //function中的res为方法返回值
.then((res)=>console.log(res)); //该res为交易执行完后的具体交易信息,如TxHash等

dispatch({
type: 'submit_signup'
})
},

对conflist部分做如下修改,特判了res为空的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
componentDidMount(){
//先执行一遍查询操作
contract.methods.queryConfList()
.call({from:window.web3.eth.accounts[0]},(err,res)=>{
//将返回的数组依次压入data中
this.setState({loading: true});
if(res != null){
for(var i=0;i<res.length;i=i+2){
data.push({title:res[i],detail:res[i+1]});
}
}
else{
data.push({title:'no',detail:'no'})
}
})
.then(()=>{
//更新状态,使页面数据重新渲染
this.setState({loading: false});
});

同理,myconf也是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
componentDidMount(){
//学习conflist/index.js该位置代码进行实现。
//先执行一遍查询操作
contract.methods.queryMyConf()
.call({from:window.web3.eth.accounts[0]},(err,res)=>{
//将返回的数组依次压入data中
this.setState({loading: true});
if(res != null){
for(var i=0;i<res.length;i=i+1){
data.push({'title': res[i]});
}
}
else{
data.push({'title': 'no'});
}
})
.then(()=>{
//更新状态,使页面数据重新渲染
this.setState({loading: false});
});

之后再在contract.js部分填上abi和合约地址,这里不再赘述。

完善前端过后npm build,然后npm start运行此框架。

image-20211130203429995

运行成功,并且能打开metamask。

image-20211130203616747

下面测试功能,先导入两个账户,私钥从ganache上找到。

image-20211130204128153

注册用户:

image-20211130204255730

新建title:

image-20211130204337550

为我自己报名:

image-20211130204422267

支付后:

image-20211130204505509

可以看到My Conferences里添加了本次会议。

之后我们用account3注册一个新账号叫1234,截图重复不再展示,委托他报名。

image-20211130204652665

执行成功。