社区负责人Daniel Podaru介绍使用Flow Go SDK的情况
从哪里开始
区块链术语一开始会让人望而生畏,可能会导致在实际编写与区块链互动的代码之前不得不学习许多概念。在这篇文章中,我们将利用Flow Go SDK来编写与Flow互动的代码。
流动的介绍
理解Flow的最简单方法是把它看成是一个具有超能力的数据库。该数据库的多个副本在分布于地球上的机器上运行,所有这些都是相互同步的。
为了与Flow互动,你将与持有数据库副本的机器之一进行通信,通常被称为节点。
为了从Flow中读取数据,你将创建一个称为脚本的程序,并将这个程序提交给节点。节点将运行它并返回一个结果。
为了向Flow读写数据,你将创建一个称为事务的程序,将其提交给节点并等待响应。
如果我们可以用交易做同样的事情,为什么还要用脚本呢?
为了执行一项交易,该节点将需要一笔费用,称为交易费。
由于交易会改变数据,而数据需要在所有节点上进行验证并保持同步,因此在整个网络上知道交易的结果之前会有延迟。
运行脚本不需要任何费用,而且由于数据已经在所有节点上同步,脚本的结果是即时的。
我需要等待多少时间才能得到一个交易结果?
交易是分批执行的,一批交易被称为一个块。把区块看成是对交易进行排序的一种方式。了解交易的执行顺序是很重要的,因为多个交易可能会改变相同的数据。
一批交易的结果之前的延迟被称为块时间,估计为~10秒。在流量仿真器上,这个延迟被设置为5秒。
交易费用是如何支付的?
在内部,Flow有一个账户系统,从概念上讲,可以把它看作一个数据库(或Excel)表。表中的每一行都有账户地址、余额 和 代码等栏目。
账户地址是唯一的,每个账户都有不同的地址,目前看起来是这样的:f4a3472b32eac8d8。
余额是这个账户拥有的Flow代币数量,看起来是这样的10000。假设这是我的账户,我想进行交易,我需要从10000个 Flow代币中拿出一部分来支付交易费用。
代码字段是为了存储一个程序,被称为智能合约。
流动账户是如何控制的?
公钥和私钥被用来控制一个账户。
一个公钥看起来像这样:a5f94b68ed62af51c8aa93752117e9df8f9a41363079a06ca10bef0d9d9aaf4e2785327b6c2b8e92ae4e08fa9df6917d71e34de4cdcddbf59628288d2acc8830
一个私钥看起来像这样。
3cb6545bd541cdd3ecf47001979662ace4dcda93f27179b85be9aad7b488e0d6.
在安全方面,你可以把你的公钥看作是你的用户名,而你的私钥则是经典网络账户场景中的密码。公钥可以与任何人共享,但私钥必须保密。如果你丢失了你的私钥,你就失去了对你的Flow账户的访问。能够访问你的私钥的人可以花费你所有的代币,并修改你所有的智能合约。
我怎样才能获得公钥/私钥?
Flow SDKs为您处理密钥生成和数据签名。密钥的生成取决于所使用的签名算法。Flow目前支持ECDSA(椭圆曲线数字签名算法)签名算法。这种算法在特定的椭圆曲线上是有效的。从这些曲线中,Flow支持 "P256 "和 "secp256k1 "曲线。
上面的公钥和私钥是为了配合P256曲线上的ECDSA签名算法而产生的。上面的私钥实际上是一个特定范围内的大的随机整数,而上面的公钥实际上是P256椭圆曲线上由{x,y}坐标表示的一个点。这也是为什么公钥在视觉上是私钥的两倍。
私钥被用来计算固定长度的数据的签名。
公钥是用来验证一个数据是用其相关的私钥签署的,而不知道私钥本身。
安装Go
我们将使用Flow Go SDK与Flow进行交互。
如果你还没有安装Go编程语言,你需要从Go网站下载并安装它:https://golang.org/dl/
一旦你安装了Go,确保你已经设置了GOPATH环境变量,指向你的工作空间目录--你将保存你要编写的Go源代码的文件夹。
要在macOS和Linux上检查GOPATH,请打开终端并运行这个命令。
echo $GOPATH/Users/daniel/gowork
要在Windows上检查GOPATH,请打开命令提示符并运行此命令。
echo %GOPATH%
C:\Users\daniel\gowork>。
这个工作空间目录被称为gowork,但你的可能有不同的名字。
安装Flow Go SDK
使用终端或命令提示符,并输入。
go get -u github.com/onflow/flow-go-sdk
go get -u github.com/onflow/cadence
项目目录
在你的Go工作空间文件夹中,你会发现一个src 文件夹。在里面导航,为你的项目创建一个新的文件夹。
cd $GOPATH
cd src
mkdir helloflow
注意:如果你的Go工作空间里没有src文件夹,请继续用mkdir src创建它。
流动仿真器
为了开发,我们需要安装Flow模拟器,它可以模拟在我们的机器上本地运行的完整Flow网络。仿真器与Flow CLI工具捆绑在一起。
在macOS和Linux上,打开终端并运行安装脚本以获得适合你系统的二进制文件。
sh -ci "$(curl -fsSLhttps://storage.googleapis.com/flow-cli/install.sh)"
在Windows上,打开PowerShell并运行安装脚本,在你的当前目录中下载一个flow.exe文件。
iex "& { $(irm 'https://storage.googleapis.com/flow-cli/install.ps1') }"
从helloflow目录中启动模拟器。在终端(macOS,Linux)或命令提示符(Windows)中输入:"。
cd helloflow
flow emulator start --init
附加的-init参数将告诉仿真器在当前文件夹中创建一个flow.json文件。一旦该文件被创建,你可以省略这个参数,因为模拟器将从flow.json文件中读取数据。
当仿真器启动时,它将打印类似这样的东西。
INFO[0000] ⚙️使用服务帐户0xf8d6e0586b0a20c7 serviceAddress=f8d6e0586b0a20c7 serviceHashAlgo=SHA3_256 servicePrivKey=0ab0b3c92adf319ab118f6c073003f7029bb6fa8eb986f47f9b139fbb189e655servicePubKey=163f52e9bf4bf0b9855c0302de2892f3bf3e3c78441c18fce342063b8d05313ad91e335c1e47291e5ca3a83ba92ce2c8abf65499abbbfc6804923009bd01c55cserviceSigAlgo=ECDSA_P256
INFO[0002] 🌱 启动3569端口的gRPC服务器。... port=3569
INFO[0002] 🌱 启动 HTTP 服务器,端口为 8080... port=8080
Flow节点由一个服务账户管理,它可以被认为是一个管理账户。在启动时,节点会显示关于服务账户的信息。
- f8d6e0586b0a20c7是这个账户的唯一地址
- 0ab0b3c92adf319ab118f6c073003f7029bb6fa8eb986f47f9b139fbb189e655 是用于控制这个账户的私钥。
- 163f52e9bf4bf0b9855c0302de2892f3bf3c78441c18fce342063b8d05313ad91e335c1e47291e5ca3a83ba92ce2c8abf65499abbbfc6804923009bd01c55c是这个帐户使用的公钥。
- ECDSA_P256告诉我们,这个账户的密钥对是根据曲线P256上的ECDSA签名算法创建的。
- SHA3_256告诉我们,这个公钥在验证签名时将使用SHA3算法的256比特变体。我们将使用同样的散列算法,在签名前使我们的数据固定长度。
开始编码
在这个介绍中,我们将创建一个Flow账户,该账户将持有一个智能合约和一个脚本,该脚本将调用智能合约中声明的一个方法。
我们下面要写的所有代码都可以在这里找到:https://github.com/cybercent/intro-flow-sdk。
所使用的Go SDK版本是v0.8.0。
让我们写一些代码来生成一个公钥和私钥对。
启动你最喜欢的IDE,在helloflow文件夹中创建一个名为account.go的新文件。
在account.go文件中加入以下代码。
package main
// [1]
import (
"crypto/rand"
"encoding/hex"
"fmt"
"github.com/onflow/flow-go-sdk/crypto"
)
// [2]
func GenerateKeys(sigAlgoName string) (string, string) {
seed := make([]byte, crypto.MinSeedLength)
_, err := rand.Read(seed)
if err != nil {
panic(err)
}
// [3]
sigAlgo := crypto.StringToSignatureAlgorithm(sigAlgoName)
privateKey, err := crypto.GeneratePrivateKey(sigAlgo, seed)
if err != nil {
panic(err)
}
// [4]
publicKey := privateKey.PublicKey()
pubKeyHex := hex.EncodeToString(publicKey.Encode())
privKeyHex := hex.EncodeToString(privateKey.Encode())
return pubKeyHex, privKeyHex
}
// [5]
func main() {
pubKey, privKey := GenerateKeys("ECDSA_P256")
fmt.Println(pubKey)
fmt.Println(privKey)
}
从终端/命令提示符运行该程序。
去运行账户。go
92e8566fdd5ee25aae2e08e1d8b5ca56e1fb84c8303e05c9e1fbfc8c36a52a90394353235306937dc9c0e3ef357626d24eba0e15f4e9a0a15bc9b2262c1bce69
3e42822b2a3764beee464cdcc1c03fba1db96e43b41be6e5e9b38c64d2987aae
在输出中,第一行是公钥,第二行是私钥。
我们在该计划中采取的步骤。
[1] 导入我们将在程序中使用的外部库。
[2] 定义一个名为GenerateKeys的新函数。它将接受一个签名算法的名称作为参数,并返回一个公钥和私钥
[3] 生成一个随机的种子,我们将从中生成用于签名算法的私钥。
[4] 获取与该私钥相关的公钥。
[5] 在主函数中,我们调用了密钥生成函数,并告诉它我们希望生成的密钥应该与曲线P256上的ECDSA签名算法一起工作。
接下来,让我们添加一个函数,它将创建一个可以由我们刚刚创建的新密钥对控制的Flow账户。
account.go文件的内容是。
package main
// [1]
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"github.com/onflow/flow-go-sdk"
"github.com/onflow/flow-go-sdk/client"
"github.com/onflow/flow-go-sdk/crypto"
"github.com/onflow/flow-go-sdk/templates"
"google.golang.org/grpc"
"strings"
"time"
)
// [2]
func GenerateKeys(sigAlgoName string) (string, string) {
// Code has not changed here.
}
// [3]
func CreateAccount(node string,
publicKeyHex string,
sigAlgoName string,
hashAlgoName string,
code string,
serviceAddressHex string,
servicePrivKeyHex string,
serviceSigAlgoName string,
gasLimit uint64) string {
ctx := context.Background()
sigAlgo := crypto.StringToSignatureAlgorithm(sigAlgoName)
publicKey, err := crypto.DecodePublicKeyHex(sigAlgo, publicKeyHex)
if err != nil {
panic(err)
}
hashAlgo := crypto.StringToHashAlgorithm(hashAlgoName)
// [4]
accountKey := flow.NewAccountKey().
SetPublicKey(publicKey).
SetSigAlgo(sigAlgo).
SetHashAlgo(hashAlgo).
SetWeight(flow.AccountKeyWeightThreshold)
// [5]
accountCode := []byte(nil)
if strings.TrimSpace(code) != "" {
accountCode = []byte(code)
}
// [6]
c, err := client.New(node, grpc.WithInsecure())
if err != nil {
panic("failed to connect to node")
}
serviceSigAlgo := crypto.StringToSignatureAlgorithm(serviceSigAlgoName)
servicePrivKey, err := crypto.DecodePrivateKeyHex(serviceSigAlgo, servicePrivKeyHex)
if err != nil {
panic(err)
}
serviceAddress := flow.HexToAddress(serviceAddressHex)
serviceAccount, err := c.GetAccountAtLatestBlock(ctx, serviceAddress)
if err != nil {
panic(err)
}
// [7]
serviceAccountKey := serviceAccount.Keys[0]
serviceSigner := crypto.NewInMemorySigner(servicePrivKey, serviceAccountKey.HashAlgo)
// [8]
tx := templates.CreateAccount([]*flow.AccountKey{accountKey}, accountCode, serviceAddress)
tx.SetProposalKey(serviceAddress, serviceAccountKey.ID, serviceAccountKey.SequenceNumber)
tx.SetPayer(serviceAddress)
tx.SetGasLimit(uint64(gasLimit))
err = tx.SignEnvelope(serviceAddress, serviceAccountKey.ID, serviceSigner)
if err != nil {
panic(err)
}
// [9]
err = c.SendTransaction(ctx, *tx)
if err != nil {
panic(err)
}
// [10]
return tx.ID().String()
}
[1] 导入库,包括流量客户端和流量交易模板。
[2] GenerateKeys函数保持不变。
[3] 声明CreateAccount函数,该函数以参数为依据。
- 节点我们将连接到的节点的IP和端口,在我们的例子中,这是仿真器
- publicKeyHex我们的公钥的十六进制表示。
- sigAlgoName 我们生成密钥时使用的签名算法的名称。
- hashAlgoName我们希望这个公钥在验证签名时使用的散列算法的名称。
- 代码的智能合约源代码,我们希望在我们的账户下发布。
- serviceAddressHex 十六进制格式的服务地址
- servicePrivKeyHex 十六进制格式的服务私钥
- serviceSigAlgoName生成服务密钥时使用的签名算法的名称。
- gasLimit创建此账户所需的Flow代币的最大交易费用金额。
为什么我们需要包括与服务账户有关的信息?
为了创建一个新的账户,我们需要向Flow写入新的数据,所以我们将使用一个事务。
账户创建不是免费的,这意味着现有的账户需要付费来创建一个新账户。幸运的是,服务账户可以为我们做这件事。
服务账户将通过签署交易来授权付款,这就是为什么我们包括其私钥。
[4] 创建一个新的账户密钥,用来控制我们即将创建的账户。该账户密钥需要知道公钥、哈希算法以及在使用该账户的私钥签署数据时将使用的签名算法。
另外,请注意,我们为账户钥匙设置了一个权重。权重参数表示这个账户钥匙是可以自己解锁还是需要多个账户钥匙。把它想象成一扇有1把锁的门,或2把锁,或很多--你需要多少把钥匙就有多少把锁来打开它。
[5] 检查我们要添加到我们账户的智能合约的源代码是否只由空位组成,我们将其转化为字节。
[6] 连接到流量节点,在我们的例子中是仿真器,并请求获得服务账户所使用的账户密钥的信息c.GetAccountAtLatestBlock(ctx, serviceAddress)。我们这样做的原因是,我们需要账户密钥的最新序列号。
序列号是一个自增的数字,存储在每个账户钥匙旁边。当用户签署交易时,该账户钥匙的序列号 需要与存储在Flow的序列号相匹配。每次交易被发送到Flow时, 序列号都会递增,与交易结果无关。
[7] 从服务账户的私钥和哈希算法创建一个新的签名者。
[8] 使用一个交易来创建账户。这个事务的Cadence源代码已经在SDK的一个模板中声明,所以我们将使用它。
接下来,我们设置提议的账户密钥--发起这项交易的账户密钥--在我们的案例中,是管理我们将创建的账户的账户密钥。
在我们设置了账户的地址后,交易费用将从该账户的余额中提取。
最后,我们设定付款人愿意在这笔交易中花费的最大数量的福禄代币。
交易信封是由服务账户签署的。信封是由SDK在幕后构建的,它由加入所有需要签名的交易数据组成。
为什么它被称为信封?
用另一把钥匙加密一把钥匙的过程称为包络加密。
在我们的交易中,只有一个签字人,即付款人。在其他使用情况下,有多个签字人,每个人都签署交易数据。在创建将由付款人签名的信封时,他们的签名被包括在内。付款人总是最后签字。
[9] 将交易发送到Flow。
[10] 如上所述,在交易被执行和其结果被知道之前,有一个延迟,称为块时间,所以我们在这里只返回交易ID。
接下来,我们将创建一个函数,将交易ID作为一个参数,并返回在该交易中创建的账户地址。
在 account.go文件中加入这个函数。
// [11]
func GetAddress(node string, txIDHex string) string {
ctx := context.Background()
c, err := client.New(node, grpc.WithInsecure())
if err != nil {
panic("failed to connect to node")
}
// [12]
txID := flow.HexToID(txIDHex)
result, err := c.GetTransactionResult(ctx, txID)
if err != nil {
panic("failed to get transaction result")
}
// [13]
var address flow.Address
if result.Status == flow.TransactionStatusSealed {
for _, event := range result.Events {
if event.Type == flow.EventAccountCreated {
accountCreatedEvent := flow.AccountCreatedEvent(event)
address = accountCreatedEvent.Address()
}
}
}
// [14]
return address.Hex()
}
[11] 声明GetAddress 函数。接下来,我们连接到节点。
[12] 向节点询问具有给定ID的交易的结果。
[13] 一个交易可以通过不同的状态过渡:未知、待定、最终执行和 密封。我们感兴趣的是,我们的交易是否已经被封存--这意味着我们所做的修改已经发生,并且不能被恢复了。接下来,我们遍历交易事件,从账户创建事件中提取账户地址,并以十六进制字符串的形式返回。
接下来,我们将调用我们在主函数中创建的函数。
在account.go中改变主函数。
func main() {
pubKey, privKey := GenerateKeys("ECDSA_P256")
fmt.Println(pubKey)
fmt.Println(privKey)
// [15]
node := "127.0.0.1:3569"
sigAlgoName := "ECDSA_P256"
hashAlgoName := "SHA3_256"
code := `
pub contract HelloWorld {
pub let greeting: String
init() {
self.greeting = "Hello, World!"
}
pub fun hello(): String {
return self.greeting
}
}
`
serviceAddressHex := "f8d6e0586b0a20c7"
servicePrivKeyHex := "0ab0b3c92adf319ab118f6c073003f7029bb6fa8eb986f47f9b139fbb189e655"
serviceSigAlgoHex := "ECDSA_P256"
// [16]
gasLimit := uint64(100)
// [17]
txID := CreateAccount(node, pubKey, sigAlgoName, hashAlgoName, code, serviceAddressHex, servicePrivKeyHex, serviceSigAlgoHex, gasLimit)
fmt.Println(txID)
// [18]
blockTime := 10 * time.Second
time.Sleep(blockTime)
// [19]
address := GetAddress(node, txID)
fmt.Println(address)
}
[15] 设置节点地址以指向仿真器的IP和端口。我们将散列和签名算法的名称设置为我们在生成密钥对时使用的算法。
在我们的账户代码下,我们将存储一个用Cadence编写的hello world智能合约。
我们设置服务地址、私钥和签名算法,以匹配模拟器为服务账户显示的算法--这些也可以在flow.json文件中找到。
[16] 将气体限制(在此交易中花费的福禄代币的最大数量)设置为100。
[17] 调用我们上面创建的CreateAccount函数,希望它能返回一个交易ID。[18] 交易结果不会立即得到,所以我们告诉我们的程序睡眠10秒。
[19] 最后我们调用GetAddress函数,该函数将返回新创建账户的地址。
从终端/命令提示符,运行该程序。
去运行账户。go
a1efeb1ef68f941b4abcb5c69175b37a24b756fdd35af3f1ba9d9fd158840a14e9a3ed77d91cf4138d0c995d327ca0cda2ee973da06f8301df366ed107f12135
2b1ebf34cb2c5070333806e9454d1d461c73deadb8a0d525e9c7be126d983e6e
0aefd29b8b1ca662dbb375d7cdb79130208588579a2950a88c89078e6c672f6d
179b6b1cb6755e31
按照顺序,我们会有一个新的公钥,一个新的私钥,交易ID,以及新创建账户的地址179b6b1cb6755e31
每次你运行该程序时,你都会生成一个新的密钥对。你也可以,生成一次密钥对,用它来控制多个账户。
接下来,我们将通过使用脚本与我们在这个账户下发布的合同进行互动。
我们将创建一个可以执行任何脚本的函数,并将我们想用来与我们的智能合约互动的Candence脚本作为参数传给这个函数。
在同一文件夹中创建一个新文件hello.go
package main
// [1]
import (
"context"
"fmt"
"github.com/onflow/flow-go-sdk/client"
"google.golang.org/grpc"
)
// [2]
func ExecuteScript(node string, script []byte) {
ctx := context.Background()
c, err := client.New(node, grpc.WithInsecure())
if err != nil {
panic("failed to connect to node")
}
// [3]
result, err := c.ExecuteScriptAtLatestBlock(ctx, script, nil)
if err != nil {
panic(err)
}
fmt.Println(result)
}
// [4]
func main() {
node := "127.0.0.1:3569"
script := `
import HelloWorld from 0x179b6b1cb6755e31
pub fun main(): String {
return HelloWorld.hello()
}
`
ExecuteScript(node, []byte(script))
}
[1] 导入我们将在这个程序中使用的外部库。
[2] 声明ExecuteScript函数,该函数将节点的IP和端口以及Cadence脚本作为参数。
[3] 将脚本发送到节点上执行,我们马上就会得到一个返回的结果。
[4] 在主函数中,我们将节点设置为仿真器的IP和端口。
接下来,创建一个脚本,将导入在我们创建的账户中发现的 HelloWorld智能合约。179b6b1cb6755e31 是我在机器上创建的账户的地址。你创建的那个将是不同的,所以你需要用这个地址来代替。另外,注意账户地址在Cadence的导入声明中使用时前缀为0x 0x179b6b1cb6755e31。0x 告诉Cadence这是一个用十六进制(base 16)表示的数字。
最后我们调用ExecuteScript函数。
从终端/命令提示符运行该程序。
go run hello.go
Hello, World!
这个结果来自HelloWorld 合约内的hello函数的执行。
祝贺你!
现在你应该对Flow有了更好的了解。下面是对我们所涉及的内容的回顾。
- 如何使用Flow Go SDK创建一个Flow账户。
- 公钥和私钥是如何在Flow中使用以确保账户安全的。
- 如何创建交易和脚本,它们的区别是什么。