ERC20 代币作为 Hyperledger Fabric Golang 链码

与以太坊区块链一样,Hyperledger Fabric平台(HLF)可用于创建代币,实现为智能合约(HLF术语中的链码),用于保存用户余额。与以太坊不同,HLF链码不能将用户地址用作持有者密钥,因此我们将使用成员资格服务提供程序(MSP)标识符和用户证书标识符的组合。下面是一个简单的示例,说明如何使用CCKit链码库在Hyperledger Fabric平台上创建代币作为Golang链码。

什么是ERC20 Token标准

ERC20 Token标准是为了在以太坊中标准化通证智能合约,它描述了以太坊Token合约必须实现的功能和事件。以太坊区块链上的大多数主要代币都符合ERC20标准。ERC-20有许多好处,包括统一Token钱包和交换列出更多Token的能力,只提供Token合约的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ----------------------------------------------------------------------------
// ERC Token Standard #20 Interface
// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20-token-standard.md
// ----------------------------------------------------------------------------
contract ERC20Interface {
function totalSupply() public constant returns (uint);
function balanceOf(address tokenOwner) public constant returns (uint balance);
function allowance(address tokenOwner, address spender) public constant returns (uint remaining);
function transfer(address to, uint tokens) public returns (bool success);
function approve(address spender, uint tokens) public returns (bool success);
function transferFrom(address from, address to, uint tokens) public returns (bool success);

event Transfer(address indexed from, address indexed to, uint tokens);
event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}

ERC20实施基础知识

从本质上讲,以太坊Token合约是一份智能合约,其中包含帐户地址及其余额的地址。余额是由合约创建者定义的值,它可以是可替代的物理对象,另一种货币价值。此余额的单位通常称为Token。

ERC20功能:

  • balanceOf:返回所有者标识符的标记余额(以太坊的帐户地址)。
  • transfer:将金额转移到我们选择的所有者标识符。
  • approve:设置一个Token数量,允许指定的所有者标识符代表我们支出。
  • allowance:检查允许所有者标识符代表我们花多少钱。
  • transferFrom:如果我们被该所有者标识符允许花费一些Token,则指定要转移的所有者标识符。

Hyperledger Fabric的所有者标识符

在Hyperledger Fabric网络中,所有参与者都具有其他参与者已知的身份。默认的成员资格服务提供程序Membership Service Provider实现使用X.509证书作为身份,采用传统的公钥基础结构(PKI)分层模型。

使用有关提案和资产所有权的创建者的信息,链码应该能够实现链代码级访问控制机制,检查参与者是否可以启动更新资产的交易。相应的链码逻辑必须能够存储与资产相关联的“所有权”信息,并根据提议对其进行评估。

作为HLF网络中的唯一所有者标识符(Token余额持有者),我们可以使用MSP标识符和用户身份标识符的组合。身份标识符,是X.509证书的SubjectIssuer部分的串联。此ID在MSP中保证是唯一的。

1
2
3
4
5
6
7
func (c *clientIdentityImpl) GetID() (string, error) {
// The leading "x509::" distinquishes this as an X509 certificate, and
// the subject and issuer DNs uniquely identify the X509 certificate.
// The resulting ID will remain the same if the certificate is renewed.
id := fmt.Sprintf("x509::%s::%s", getDN(&c.cert.Subject), getDN(&c.cert.Issuer))
return base64.StdEncoding.EncodeToString([]byte(id)), nil
}

客户端身份链代码库Client identity chaincode library允许编写链代码,该代码根据客户端的身份(即链代码的调用者)做出访问控制决策。

特别是,你可以根据与客户端关联的以下任一或两者来做出访问控制决策:

  • 客户身份的MSP(成员资格服务提供程序)ID。
  • 与客户端标识关联的属性。

CCkit包含具有结构和功能的标识包,可用于实现链代码中的访问控制。

开始使用该示例

在我们的示例中,我们使用CCKit Router来管理智能合约功能。在开始之前,请务必获取CCkit

1
git clone git@github.com:s7techlab/cckit.git

并使用dep命令获取依赖项:

1
dep ensure -vendor-only

ERC20示例位于examples/erc20目录中。

定义令牌智能合约功能

首先,我们需要定义链代码函数。在我们的示例中,我们使用来自CCKitRouter包,它允许我们以一致的方式定义链代码方法及其参数。有关链代码方法路由,中间件,链代码调用上下文的详细信息在前一篇文章中有所描述。

首先,我们使用参数symbolnametotalSupply定义init函数(智能合约构造函数)。之后我们定义链码方法,实现ERC20接口,采用HLF所有者标识符(MSP Id和证书ID对)。从链代码状态查询的方法以query为前缀,写入链代码状态的方法以invoke为前缀。

因此,我们使用默认的链代码结构,它将InitInvoke处理委托给router。

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
func NewErc20FixedSupply() *router.Chaincode {

r := router.New(`erc20fixedSupply`).Use(p.StrictKnown).
// Chaincode init function, initiates token smart contract with token symbol, name and totalSupply
Init(invokeInitFixedSupply, p.String(`symbol`), p.String(`name`), p.Int(`totalSupply`)).
// Get token symbol
Query(`symbol`, querySymbol).
// Get token name
Query(`name`, queryName).
// Get the total token supply
Query(`totalSupply`, queryTotalSupply).
// get account balance
Query(`balanceOf`, queryBalanceOf, p.String(`mspId`), p.String(`certId`)).
//Send value amount of tokens
Invoke(`transfer`, invokeTransfer, p.String(`toMspId`), p.String(`toCertId`), p.Int(`amount`)).
// Allow spender to withdraw from your account, multiple times, up to the _value amount.
// If this function is called again it overwrites the current allowance with _valu
Invoke(`approve`, invokeApprove, p.String(`spenderMspId`), p.String(`spenderCertId`), p.Int(`amount`)).
// Returns the amount which _spender is still allowed to withdraw from _owner]
Query(`allowance`, queryAllowance, p.String(`ownerMspId`), p.String(`ownerCertId`),
p.String(`spenderMspId`), p.String(`spenderCertId`)).
// Send amount of tokens from owner account to another
Invoke(`transferFrom`, invokeTransferFrom, p.String(`fromMspId`), p.String(`fromCertId`),
p.String(`toMspId`), p.String(`toCertId`), p.Int(`amount`))

return router.NewChaincode(r)
}

Chaincode初始化(构造函数)

Chaincode init函数(标记构造函数)执行以下操作:

  • 使用来自CCKit的所有者扩展来链接关于链代码所有者的链接代码状态信息。
  • 放入链码状态token配置:代币符号,名称和总供应量。
  • 设置chaincode所有者余额与总供应量。
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
const SymbolKey = `symbol`
const NameKey = `name`
const TotalSupplyKey = `totalSupply`


func invokeInitFixedSupply(c router.Context) (interface{}, error) {
ownerIdentity, err := owner.SetFromCreator(c)
if err != nil {
return nil, errors.Wrap(err, `set chaincode owner`)
}

// save token configuration in state
if err := c.State().Insert(SymbolKey, c.ArgString(`symbol`)); err != nil {
return nil, err
}

if err := c.State().Insert(NameKey, c.ArgString(`name`)); err != nil {
return nil, err
}

if err := c.State().Insert(TotalSupplyKey, c.ArgInt(`totalSupply`)); err != nil {
return nil, err
}

// set token owner initial balance
if err := setBalance(c, ownerIdentity.GetMSPID(), ownerIdentity.GetID(), c.ArgInt(`totalSupply`)); err != nil {
return nil, errors.Wrap(err, `set owner initial balance`)
}

return ownerIdentity, nil
}

定义事件结构类型

我们使用identity身份包中的Id结构并定义TransferApprove事件的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type (
Id struct {
MSP string
Cert string
}

Transfer struct {
From identity.Id
To identity.Id
Amount int
}

Approve struct {
From identity.Id
Spender identity.Id
Amount int
}
)

实现token智能合约功能

查询函数非常简单,它只是从链码状态读取值:

1
2
3
4
5
const SymbolKey = `symbol`

func querySymbol(c r.Context) (interface{}, error) {
return c.State().Get(SymbolKey)
}

一些改变状态函数更复杂。例如,在函数invokeTransfer中,我们执行:

  • 接收函数调用者证书(通过tx GetCreator()函数)。
  • 检查转移目的地。
  • 获得当前的调用者(付款人)余额。
  • 检查余额以转移代币金额amount
  • 获得收件人余额。
  • 以链码状态更新付款人和收款人余额。
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
func invokeTransfer(c r.Context) (interface{}, error) {
// transfer target
toMspId := c.ArgString(`toMspId`)
toCertId := c.ArgString(`toCertId`)

//transfer amount
amount := c.ArgInt(`amount`)

// get informartion about tx creator
invoker, err := identity.FromStub(c.Stub())
if err != nil {
return nil, err
}

// Disallow to transfer token to same account
if invoker.GetMSPID() == toMspId && invoker.GetID() == toCertId {
return nil, ErrForbiddenToTransferToSameAccount
}

// get information about invoker balance from state
invokerBalance, err := getBalance(c, invoker.GetMSPID(), invoker.GetID())
if err != nil {
return nil, err
}

// Check the funds sufficiency
if invokerBalance-amount < 0 {
return nil, ErrNotEnoughFunds
}

// Get information about recipient balance from state
recipientBalance, err := getBalance(c, toMspId, toCertId)
if err != nil {
return nil, err
}

// Update payer and recipient balance
setBalance(c, invoker.GetMSPID(), invoker.GetID(), invokerBalance-amount)
setBalance(c, toMspId, toCertId, recipientBalance+amount)

// Trigger event with name "transfer" and payload - serialized to json Transfer structure
c.SetEvent(`transfer`, &Transfer{
From: identity.Id{
MSP: invoker.GetMSPID(),
Cert: invoker.GetID(),
},
To: identity.Id{
MSP: toMspId,
Cert: toCertId,
},
Amount: amount,
})

// return current invoker balance
return invokerBalance - amount, nil
}

// setBalance puts balance value to state
func setBalance(c r.Context, mspId, certId string, balance int) error {
return c.State().Put(balanceKey(mspId, certId), balance)
}

// balanceKey creates composite key for store balance value in state
func balanceKey(ownerMspId, ownerCertId string) []string {
return []string{BalancePrefix, ownerMspId, ownerCertId}
}

测试

此外,我们可以通过CCKit MockStub快速测试我们的链代码。

要开始测试,我们通过MockStub使用测试参数初始化链码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var _ = Describe(`ERC-20`, func() {

const TokenSymbol = `HLF`
const TokenName = `HLFCoin`
const TotalSupply = 10000
const Decimals = 3

//Create chaincode mock
erc20fs := testcc.NewMockStub(`erc20`, NewErc20FixedSupply())

// load actor certificates
actors, err := identity.ActorsFromPemFile(`SOME_MSP`, map[string]string{
`token_owner`: `s7techlab.pem`,
`account_holder1`: `victor-nosov.pem`,
//`accoubt_holder2`: `victor-nosov.pem`
}, examplecert.Content)
if err != nil {
panic(err)
}

BeforeSuite(func() {
// init token haincode
expectcc.ResponseOk(erc20fs.From(actors[`token_owner`]).Init(TokenSymbol, TokenName, TotalSupply, Decimals))
})

我们可以检查所有代币操作:

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
Describe("ERC-20 transfer", func() {
It("Disallow to transfer token to same account", func() {
expectcc.ResponseError(
erc20fs.From(actors[`token_owner`]).Invoke(
`transfer`, actors[`token_owner`].GetMSPID(), actors[`token_owner`].GetID(), 100),
ErrForbiddenToTransferToSameAccount)
})

It("Disallow token holder with zero balance to transfer tokens", func() {
expectcc.ResponseError(
erc20fs.From(actors[`account_holder1`]).Invoke(
`transfer`, actors[`token_owner`].GetMSPID(), actors[`token_owner`].GetID(), 100),
ErrNotEnoughFunds)
})

It("Allow token holder with non zero balance to transfer tokens", func() {
expectcc.PayloadInt(
erc20fs.From(actors[`token_owner`]).Invoke(
`transfer`, actors[`account_holder1`].GetMSPID(), actors[`account_holder1`].GetID(), 100),
TotalSupply-100)

expectcc.PayloadInt(
erc20fs.Query(
`balanceOf`, actors[`token_owner`].GetMSPID(), actors[`token_owner`].GetID()), TotalSupply-100)

expectcc.PayloadInt(
erc20fs.Query(
`balanceOf`, actors[`account_holder1`].GetMSPID(), actors[`account_holder1`].GetID()), 100)
})
})

完整的例子可以在这里找到:https://github.com/s7techlab/cckit/tree/master/examples/erc20

希望尽快学习Hyperroger fabric课程的请访问Fabric区块链开发详解,本课程面向初学者,内容即包含Hyperledger Fabric的身份证书与MSP服务、权限策略、通道配置与启动、链码通信接口等核心概念,也包含Fabric网络设计、nodejs链码与应用开发的操作实践,是Nodejs工程师学习Fabric区块链开发的最佳选择。

======================================================================

分享一些以太坊、EOS、比特币等区块链相关的交互式在线编程实战教程:

  • java以太坊开发教程,主要是针对java和android程序员进行区块链以太坊开发的web3j详解。
  • php以太坊,主要是介绍使用php进行智能合约开发交互,进行账号创建、交易、转账、代币开发以及过滤器和交易等内容。
  • python以太坊,主要是针对python工程师使用web3.py进行区块链以太坊开发的详解。
  • 以太坊入门教程,主要介绍智能合约与dapp应用开发,适合入门。
  • 以太坊开发进阶教程,主要是介绍使用node.js、mongodb、区块链、ipfs实现去中心化电商DApp实战,适合进阶。
  • ERC721以太坊通证实战,课程以一个数字艺术品创作与分享DApp的实战开发为主线,深入讲解以太坊非同质化通证的概念、标准与开发方案。内容包含ERC-721标准的自主实现,讲解OpenZeppelin合约代码库二次开发,实战项目采用Truffle,IPFS,实现了通证以及去中心化的通证交易所。
  • C#以太坊,主要讲解如何使用C#开发基于.Net的以太坊应用,包括账户管理、状态与交易、智能合约开发与交互、过滤器和交易等。
  • EOS教程,本课程帮助你快速入门EOS区块链去中心化应用的开发,内容涵盖EOS工具链、账户与钱包、发行代币、智能合约开发与部署、使用代码与智能合约交互等核心知识点,最后综合运用各知识点完成一个便签DApp的开发。
  • 深入浅出玩转EOS钱包开发,本课程以手机EOS钱包的完整开发过程为主线,深入学习EOS区块链应用开发,课程内容即涵盖账户、计算资源、智能合约、动作与交易等EOS区块链的核心概念,同时也讲解如何使用eosjs和eosjs-ecc开发包访问EOS区块链,以及如何在React前端应用中集成对EOS区块链的支持。课程内容深入浅出,非常适合前端工程师深入学习EOS区块链应用开发。
  • java比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Java代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Java工程师不可多得的比特币开发学习课程。
  • php比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Php代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Php工程师不可多得的比特币开发学习课程。
  • c#比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在C#代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是C#工程师不可多得的比特币开发学习课程。
  • tendermint区块链开发详解,本课程适合希望使用tendermint进行区块链开发的工程师,课程内容即包括tendermint应用开发模型中的核心概念,例如ABCI接口、默克尔树、多版本状态库等,也包括代币发行等丰富的实操代码,是go语言工程师快速入门区块链开发的最佳选择。

汇智网原创翻译,转载请标明出处。这里是ERC20 代币作为 Hyperledger Fabric Golang 链码