1.实例演示
这里以官网的XO交易族为例演示,该交易族是一个井字棋游戏,在开始之前,我们需要搭建起来一个单节点的sawtooth环境,详情可以查看上一篇博客:
在确认链接之类的都正常之后,我们可以链接到shell容器中进行游戏:
docker exec -it sawtooth-shell-default bash
该容器中存在sawtooth的相关环境,包括sawtooth
命令,以及交易族相关的客户端xo
命令等。
1.1.创建玩家
井字棋需要两名用户,因此需要创建两名用户,这里的创建是指创建用户的密钥,并将其放在$HOME/.sawtooth/keys下。
root@b872105f1c59:/usr/bin# sawtooth keygen jack
writing file: /root/.sawtooth/keys/jack.priv
writing file: /root/.sawtooth/keys/jack.pub
root@b872105f1c59:/usr/bin# sawtooth keygen jill
writing file: /root/.sawtooth/keys/jill.priv
writing file: /root/.sawtooth/keys/jill.pub
1.2.创建游戏
利用刚创建的用户jack
创建一局叫做my-game
的井字棋游戏房间。
xo create my-game --username jack
为了查看游戏的创建情况,可以用如下命令查看目前的游戏列表。
xo list
这里我执行时报了如下的错误,没有遇到的话可以忽略:
Error: Failed to connect to http://127.0.0.1:8008/state?address=5b7349: HTTPConnectionPool(host='127.0.0.1', port=8008): Max retries exceeded with url: /state?address=5b7349 (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7f410b4537b8>: Failed to establish a new connection: [Errno 111] Connection refused',))
这里应该是docker的连接出现了问题,但是如果刚才测试容器连接性没问题的话,可以通过手动指定url的方式来进行参数的提交:
xo list --url rest-api:8008
之后的命令如果遇到类似问题都加这个url参数,没遇到的话就不用管了,之后的说明不再加该参数
打印出如下的内容,说明游戏创建成功:
GAME PLAYER 1 PLAYER 2 BOARD STATE
my-game --------- P1-NEXT
xo list
命令的本质还是REST请求,但是进行了包装,会比使用curl去提交简单许多。
1.3.作为玩家去下棋
交易族的处理逻辑是将第一个下棋的玩家看作PLAYER 1,第二个看作PLAYER 2,并且下的时候需要用--username
参数指定自己的身份。
这里棋盘需要看作数字小键盘的排布:
1 | 2 | 3
---|---|---
4 | 5 | 6
---|---|---
7 | 8 | 9
这里我们使用xo take
命令去让jake和jill分别在5和1处下一枚棋子:
xo take my-game 5 --username jack
xo take my-game 1 --username jill
每次take
命令提交成功之后会打印出如下内容:
Response: {
"link": "http://rest-api:8008/batch_statuses?id=fd61fb0946fc11807d9ae456a8689fad87b71c711a1eb408072b602beaaca36e420bb5ffd3ba2188f6a52354edab084687840f51e6a05de79f5c25c7b6ede627"
}
然后我们可以以下当前的棋盘状态,使用如下的命令:
xo show my-game
可以看出如下的输出:
GAME: : my-game
PLAYER 1 : 035446
PLAYER 2 : 03aa97
STATE : P1-NEXT
O | |
---|---|---
| X |
---|---|---
| |
每一次命令提交并成功更新账本之后,都会更新后台存储的游戏状态,使用如下的格式存储:
<game-name>,<board-state>,<game-state>,<player1-key>,<player2-key>
当前的状态在后台就按如下的格式来存储:
my-game,O---X----,P1-NEXT,02403a...,03729b...
每一次玩家尝试去下棋时,后台逻辑都会检查此时下棋的玩家是否为当前应该下棋的玩家,如果违反了顺序的话,是不会更新账本的,比如当前需要玩家一jack
去下棋了,但是jill
又去尝试下棋。
可以看到虽然交易成功提交了,但是下棋并没有成功:
root@754b02b93ab8:/# xo take my-game 7 --username jill --url rest-api:8008
Response: {
"link": "http://rest-api:8008/batch_statuses?id=f7643aa33d47e1240584965ad8c65bb9116728c2f286c9242839122747265b492352d6e98b9d72b42c4c555112c994fe3c5edf9bbfdbfafaf085126d8ad30532"
}
root@754b02b93ab8:/# xo show my-game --url rest-api:8008
GAME: : my-game
PLAYER 1 : 035446
PLAYER 2 : 03aa97
STATE : P1-NEXT
O | |
---|---|---
| X |
---|---|---
| |
所以另一方面我们也可以知道对于交易这个数据的合法性是由验证者来验证的,而对于交易逻辑的合法性则由交易处理器来进行验证,可以类比为通过编译的程序未必能够正常运行。
然后进行若干次的下棋操作,最终会有一方胜出或者平局,当胜出之后,状态将会改为对应的结果,此时再去下棋仍然不会生效。
1.4.删除游戏
任何一个玩家都可以删除游戏,使用如下命令即可
xo delete my-game
提交成功之后再查看当前游戏列表:
root@754b02b93ab8:/# xo list --url rest-api:8008
GAME PLAYER 1 PLAYER 2 BOARD STATE
可以看到不再有游戏存在了。
此外xo命令还可以指定--auth-user
和--auth-password
用于REST API的简单鉴权。
以上是这个井字棋游戏的全部逻辑,其实看下来,除去区块链的因素,这就是一个普通的小游戏,所以应用只要逻辑写好基本上都可以移植到sawtooth中。
2.开发流程介绍
以下的内容为官网的sawtooth的Python SDK的使用说明Using the Python SDK,主要介绍了交易处理器和客户端的开发流程和术语等,但是并不是一个完整的实践。关于井字棋(XO)的完整内容可以查看https://github.com/hyperledger/sawtooth-sdk-python/tree/main/examples/xo_python
一个交易族包含以下三个组件:
- 交易处理器:定义应用的业务逻辑,他的职责包括注册验证者、处理交易载荷和相关的元数据以及读取设置需要的状态。
- 数据模型:用于记录和存储数据。
- 客户端:处理应用的客户端逻辑,他的职责包括创建和签署交易,把交易压缩成一个批次,然后提交到验证者。客户端可以把批次通过REST API用post方法或者通过ZeroMQ发送给验证者。
客户端和处理器必须使用相同的数据模型,序列化/编码方法以及寻址方式。
在看完整篇文档之后,我发现其实交易处理器和客户端是真实存在的两个组件,而数据模型则是一个逻辑概念,并不需要专门使用sawtoothSDK的API来实现,所以接下来我分别介绍一下交易处理器和客户端的开发流程。
2.1.创建交易处理器
交易处理器有两个顶级组件:
- Processor类,SDK提供一个通用的处理器类。
- Handler类,依赖于应用,包含对于特定的交易族的业务逻辑,多个Handler可以连接到一个处理器类实例上。
这里我个人的理解是Processor相当于使用模板模式提供了一个框架,把Handler作为一个接口让用户去实现,然后再Processor去调用其方法即可。
2.1.1.接入点与Handler
因为交易处理器是一个长期运行的进程,所以需要一个接入点,相当于主函数。
在接入点中,TransactionProcessor
类提供了一个地址来连接验证者和Handler类。
代码sawtooth_xo/processor/main.py
如下:
from sawtooth_sdk.processor.core import TransactionProcessor
from sawtooth_xo.processor.handler import XoTransactionHandler
def main():
# In docker, the url would be the validator's container name with
# port 4004
processor = TransactionProcessor(url='tcp://127.0.0.1:4004')
handler = XoTransactionHandler()
processor.add_handler(handler)
processor.start()
这里可以看到导入了自己实现的一个Handler,然后添加到开放端口的processor的Handler管理器中,然后启动这个processor。
这里实现的具体的XoTransactionHandler class代码sawtooth_xo/processor/handler.py
如下:
class XoTransactionHandler(TransactionHandler):
def __init__(self, namespace_prefix):
self._namespace_prefix = namespace_prefix
@property
def family_name(self):
return 'xo'
@property
def family_versions(self):
return ['1.0']
@property
def namespaces(self):
return [self._namespace_prefix]
def apply(self, transaction, context):
# ...
代码中包含元数据设置的相关方法family_name
、namespaces
等以及一个apply
方法,其中元数据方法用于为processor描述handler,apply
方法则书写具体的业务逻辑,apply
方法调用时包含两个参数,transaction
为由protobuf定义创建的交易类实例,持有需要执行的命令,context
则是Python SDK提供的上下文实例,存储相关的状态等信息。
交易会包含对验证核心透明的负载字节流以及交易族的描述信息,如何处理二进制序列化协议则由实现者去决定,即验证器会保证最后由逻辑处理的数据都是可信的。
2.1.2.apply
方法实现
这里一个apply的实现框架如下,代码仍然位于sawtooth_xo/processor/handler.py
:
def apply(self, transaction, context):
header = transaction.header
signer = header.signer_public_key
xo_payload = XoPayload.from_bytes(transaction.payload)
xo_state = XoState(context)
if xo_payload.action == 'delete':
...
elif xo_payload.action == 'create':
...
elif xo_payload.action == 'take':
...
else:
raise InvalidTransaction('Unhandled action: {}'.format(
xo_payload.action))
这里为了把状态和操作进行分割,XO实例拥有XoState
类和XoPayload
类,前者用于处理交易给出的动作,后者则包含具体的状态,包括当前是谁的回合,棋盘上有哪些棋子等,即通过前者来改变后者存储的内容,这两个类的细节将会在后面给出说明。
这里定义了几个游戏的操作,包括create
创建一场新的游戏,take
在棋盘某个空闲的位置下一个子,delete
删除一场游戏,会根据xo_payload的action字段对应不同的操作进行处理。
这里描述了井字棋游戏的具体实现,包括对我们在apply方法中定义的几个具体操作的实现。
2.1.2.1.create
代码如下:
elif xo_payload.action == 'create':
if xo_state.get_game(xo_payload.name) is not None:
raise InvalidTransaction(
'Invalid action: Game already exists: {}'.format(
xo_payload.name))
game = Game(name=xo_payload.name,
board="-" * 9,
state="P1-NEXT",
player1="",
player2="")
xo_state.set_game(xo_payload.name, game)
_display("Player {} created a game.".format(signer[:6]))
这里首先判断给出的名称对应的游戏是否已经存在,存在的话抛出错误,否则就使用这个名称新建游戏实例,(这个实例由Game
类进行描述,其代码在本文后面可以看到,这里简单理解为结构体即可),然后进行存储,最后打印提示,某位用户创建了游戏。
2.1.2.2.Delete
代码如下:
if xo_payload.action == 'delete':
game = xo_state.get_game(xo_payload.name)
if game is None:
raise InvalidTransaction(
'Invalid action: game does not exist')
xo_state.delete_game(xo_payload.name)
首先判断游戏是否存在,如果不存在的话抛出异常,否则就调用xo_state根据名称删除游戏。
2.1.2.3.Take
代码如下:
elif xo_payload.action == 'take':
game = xo_state.get_game(xo_payload.name)
if game is None:
raise InvalidTransaction(
'Invalid action: Take requires an existing game')
if game.state in ('P1-WIN', 'P2-WIN', 'TIE'):
raise InvalidTransaction('Invalid Action: Game has ended')
if (game.player1 and game.state == 'P1-NEXT' and
game.player1 != signer) or \
(game.player2 and game.state == 'P2-NEXT' and
game.player2 != signer):
raise InvalidTransaction(
"Not this player's turn: {}".format(signer[:6]))
if game.board[xo_payload.space - 1] != '-':
raise InvalidTransaction(
'Invalid Action: space {} already taken'.format(
xo_payload))
if game.player1 == '':
game.player1 = signer
elif game.player2 == '':
game.player2 = signer
upd_board = _update_board(game.board,
xo_payload.space,
game.state)
upd_game_state = _update_game_state(game.state, upd_board)
game.board = upd_board
game.state = upd_game_state
xo_state.set_game(xo_payload.name, game)
_display(
"Player {} takes space: {}\n\n".format(
signer[:6],
xo_payload.space) +
_game_data_to_str(
game.board,
game.state,
game.player1,
game.player2,
xo_payload.name))
这里首先需要根据新下棋的动作去判断该动作是否合法,如果合法就设置新的棋盘位置等,然后设置到当前游戏名称对应的状态,本质上是对当前游戏状态进行更新的操作。
2.1.3.载荷(Payload)XoPayload
交易由头和载荷组成,头包含签名者,用于表示当前的玩家。载荷将包含具体交易信息,在当前的应用中包含游戏的名称name
(不能包含|
,这里是因为存储解构中把|
看作分隔符,如果自己的存储逻辑有办法转义|
就可以让游戏名称包含|
),行为(create
、delete
和take
)和位置(如果行为不是take
的话,这个字段就设置为空)。
那么其实我们可以看出来,载荷的数据结构其实是用户自定义的,相当于web项目中接口的请求参数的设定。
代码sawtooth_xo/processor/xo_payload.py
如下:
class XoPayload:
def __init__(self, payload):
try:
# The payload is csv utf-8 encoded string
name, action, space = payload.decode().split(",")
except ValueError:
raise InvalidTransaction("Invalid payload serialization")
if not name:
raise InvalidTransaction('Name is required')
if '|' in name:
raise InvalidTransaction('Name cannot contain "|"')
if not action:
raise InvalidTransaction('Action is required')
if action not in ('create', 'take', 'delete'):
raise InvalidTransaction('Invalid action: {}'.format(action))
if action == 'take':
try:
if int(space) not in range(1, 10):
raise InvalidTransaction(
"Space must be an integer from 1 to 9")
except ValueError:
raise InvalidTransaction(
'Space must be an integer from 1 to 9')
if action == 'take':
space = int(space)
self._name = name
self._action = action
self._space = space
@staticmethod
def from_bytes(payload):
return XoPayload(payload=payload)
@property
def name(self):
return self._name
@property
def action(self):
return self._action
@property
def space(self):
return self._space
这里负载会是一个,
隔开的字符串,包括name
、action
和space
三个字段,当然开发者也完全可以自定义自己的请求结构,甚至是使用json,这里XoPayload类似于一个实体类,其构造方法接收一个字节数组,然后根据这个字节数组去构造出自己的三个属性,但是如果构造出的属性如果有非法数据则会抛出异常。
2.1.4.状态(state)XoState
XoState类可以把游戏信息转化到字节,然后存在于验证者的Radix-Merkle树,以及把存储在Radix-Merkle树中的字节转化为游戏信息。
Xostate类的状态项目entry
包含由空格隔开的如下内容:
name
,board
,game-state
,player-key-1
,player-key-2
其中:
- name是游戏的名称,不能含有
|
。 - board是一个长度为9的字符串,包含
O
、X
或者-
,代表某方的棋子或者空格。 - game-state为游戏状态,可以为
P1-NEXT
,P2-NEXT
,P1-WIN
,P2-WIN
或者TIE
,即该哪一方下棋或者哪一方已经胜出以及平局等。 - player-key-1和player-key-2则为游戏参与玩家的公钥
这里在实际存储的时候是利用项目entry
哈希之后作为键,项目作为值进行存储的,因此就会存在哈希碰撞问题,这里的处理方式是按照字母顺序对冲突的项进行排序之后使用|分割进行存储,即<a-entry>|<b-entry>|...
。
代码如下sawtooth_xo/processor/xo_state.py
。
XO_NAMESPACE = hashlib.sha512('xo'.encode("utf-8")).hexdigest()[0:6]
class Game:
def __init__(self, name, board, state, player1, player2):
self.name = name
self.board = board
self.state = state
self.player1 = player1
self.player2 = player2
class XoState:
TIMEOUT = 3
def __init__(self, context):
"""Constructor.
Args:
context (sawtooth_sdk.processor.context.Context): Access to
validator state from within the transaction processor.
"""
self._context = context
self._address_cache = {}
def delete_game(self, game_name):
"""Delete the Game named game_name from state.
Args:
game_name (str): The name.
Raises:
KeyError: The Game with game_name does not exist.
"""
games = self._load_games(game_name=game_name)
del games[game_name]
if games:
self._store_game(game_name, games=games)
else:
self._delete_game(game_name)
def set_game(self, game_name, game):
"""Store the game in the validator state.
Args:
game_name (str): The name.
game (Game): The information specifying the current game.
"""
games = self._load_games(game_name=game_name)
games[game_name] = game
self._store_game(game_name, games=games)
def get_game(self, game_name):
"""Get the game associated with game_name.
Args:
game_name (str): The name.
Returns:
(Game): All the information specifying a game.
"""
return self._load_games(game_name=game_name).get(game_name)
def _store_game(self, game_name, games):
address = _make_xo_address(game_name)
state_data = self._serialize(games)
self._address_cache[address] = state_data
self._context.set_state(
{address: state_data},
timeout=self.TIMEOUT)
def _delete_game(self, game_name):
address = _make_xo_address(game_name)
self._context.delete_state(
[address],
timeout=self.TIMEOUT)
self._address_cache[address] = None
def _load_games(self, game_name):
address = _make_xo_address(game_name)
if address in self._address_cache:
if self._address_cache[address]:
serialized_games = self._address_cache[address]
games = self._deserialize(serialized_games)
else:
games = {}
else:
state_entries = self._context.get_state(
[address],
timeout=self.TIMEOUT)
if state_entries:
self._address_cache[address] = state_entries[0].data
games = self._deserialize(data=state_entries[0].data)
else:
self._address_cache[address] = None
games = {}
return games
def _deserialize(self, data):
"""Take bytes stored in state and deserialize them into Python
Game objects.
Args:
data (bytes): The UTF-8 encoded string stored in state.
Returns:
(dict): game name (str) keys, Game values.
"""
games = {}
try:
for game in data.decode().split("|"):
name, board, state, player1, player2 = game.split(",")
games[name] = Game(name, board, state, player1, player2)
except ValueError:
raise InternalError("Failed to deserialize game data")
return games
def _serialize(self, games):
"""Takes a dict of game objects and serializes them into bytes.
Args:
games (dict): game name (str) keys, Game values.
Returns:
(bytes): The UTF-8 encoded string stored in state.
"""
game_strs = []
for name, g in games.items():
game_str = ",".join(
[name, g.board, g.state, g.player1, g.player2])
game_strs.append(game_str)
return "|".join(sorted(game_strs)).encode()
def _make_xo_address(name):
return XO_NAMESPACE + \
hashlib.sha512(name.encode('utf-8')).hexdigest()[:64]
这里首先有一个游戏项目的实体类Game
,即对应我们之前说的entry
,XoState
则是这里实际的存储类,这里的init
中传入的context
参数也是我们之前分析过的由SDK传入的上下文对象,即sawtooth实际进行存储的对象。
另外需要说明一下的是那个XO_NAMESPACE,这里猜测是因为多个交易族共用同一个存储库,所以存储时的key前面需要一个namespace来进行隔离,以免不同的交易族因为产生了相同的key而产生了冲突。
2.2.创建客户端
客户端向分布式账本提交的信息需要进行一系列的加密保护工作用于确认身份和数据有效性验证,但是sawtooth的SDK已经实现了大多数功能并提供了抽象接口来简化使用操作。
2.2.1.创建私钥和签名
为了确认自己的身份并对自己发出的消息进行签名来让验证者进行有效验证,需要一个256位的密钥来作为自己的私钥。Sawtooth使用secp256k1 ECDSA标准来实现签名,也就是说几乎所有32字节的集合都可以作为一个有效的密钥,因此使用SDK的签名模块来生成一个有效的密钥是一项比较简单的工作。
from sawtooth_signing import create_context
from sawtooth_signing import CryptoFactory
context = create_context('secp256k1')
private_key = context.new_random_private_key()
signer = CryptoFactory(context).new_signer(private_key)
这里使用secp256k1来新建了一个私钥,并使用该私钥来新建了一个签名者对象。
私钥十分重要,是证明用户身份的唯一标识,丢了无法恢复,如果私钥被别人获取到,那么对方就可以冒充私钥真正拥有者来进行所有操作,所以需要妥善保管。
2.2.2.构建交易
交易引起Sawtooh的状态发生改变,交易由如下部分组成:
- 被编码后的二进制载荷
- 加密的二进制编码的交易头
- 辅助处理的元数据
- 对交易头的签名
2.2.2.1.载荷编码
交易的载荷由对验证者不需要关心的二进制编码数据组成,编码解码(实质是对字节数组的相互转化)的逻辑完全由特定的交易处理器来实现。因此,采用何种格式进行编码的决定权在交易处理器中,因此编码前需要先搞清楚交易处理器的相关定义。以Integer Key链码族来说,它使用CBOR(简明二进制对象展现,一种数据交换格式)来对其载荷进行编码。
import cbor
payload = {
'Verb': 'set',
'Name': 'foo',
'Value': 42}
payload_bytes = cbor.dumps(payload)
2.2.2.2.创建交易头
交易头包含路由到正确的交易处理器的信息,交易族family_name
和交易族版本family_version
等(可以和交易处理器的元数据对应起来),读或者写的地址(辅助交易处理器更有效率的读写数据?),签名对应的公钥signer_public_key
(用于之后验签),以及使用SHA-512进行摘要计算的载荷字节payload_sha512
。
代码如下:
from hashlib import sha512
from sawtooth_sdk.protobuf.transaction_pb2 import TransactionHeader
txn_header_bytes = TransactionHeader(
family_name='intkey',
family_version='1.0',
inputs=['1cf1266e282c41be5e4254d8820772c5518a2c5a8c0c7f7eda19594a7eb539453e1ed7'],
outputs=['1cf1266e282c41be5e4254d8820772c5518a2c5a8c0c7f7eda19594a7eb539453e1ed7'],
signer_public_key=signer.get_public_key().as_hex(),
# In this example, we're signing the batch with the same private key,
# but the batch can be signed by another party, in which case, the
# public key will need to be associated with that key.
batcher_public_key=signer.get_public_key().as_hex(),
# In this example, there are no dependencies. This list should include
# an previous transaction header signatures that must be applied for
# this transaction to successfully commit.
# For example,
# dependencies=['540a6803971d1880ec73a96cb97815a95d374cbad5d865925e5aa0432fcf1931539afe10310c122c5eaae15df61236079abbf4f258889359c4d175516934484a'],
dependencies=[],
payload_sha512=sha512(payload_bytes).hexdigest()
).SerializeToString()
2.2.2.3.组装交易
交易头创建之后,其字节数组将会用于创建签名,这个签名也会作为交易的ID使用,交易头字节数组、交易头签名和载荷字节数组一起用于构造完整的交易。
from sawtooth_sdk.protobuf.transaction_pb2 import Transaction
signature = signer.sign(txn_header_bytes)
txn = Transaction(
header=txn_header_bytes,
header_signature=signature,
payload=payload_bytes
)
2.2.2.4.编码交易(可选)
如果创建交易和打包成批次的操作都在同一台机器上完成,那么这个编码操作是可以省略的,但是如果打包批次的处理组件在外部,那么就需要先将交易进行序列化传递过去,然后在外部的打包器上进行打包。Python的SDK提供了两种方式,一种是把多个交易存储在TransactionList中,然后一起进行序列化,另一种是把单独的一个交易进行序列化。代码如下:
from sawtooth_sdk.protobuf.transaction_pb2 import TransactionList
txn_list_bytes = TransactionList(
transactions=[txn1, txn2]
).SerializeToString()
txn_bytes = txn.SerializeToString()
2.2.3.构建批次(Batch)
批次是sawtooth中处理交易的最小单位,即向验证者提交若干交易的时候,它必须被包含在一个批次中。批次是一个原子操作,其含有的一个或多个交易要么全部执行,要么全部不执行,这点上有点类似于数据库中事务的概念,但是在批次中的所有事务未必是有依赖关系的。
2.2.3.1.创建批次头
和交易头类似,每个批次都需要拥有一个批次头,但是批次比交易的结构更加简单,一个批次头只需要签名者的公钥和其含有的交易id列表(id也是交易的签名,所以其实验证只用批次头就可以完成了),其顺序和他们在批次中的顺序一致。
from sawtooth_sdk.protobuf.batch_pb2 import BatchHeader
txns = [txn]
batch_header_bytes = BatchHeader(
signer_public_key=signer.get_public_key().as_hex(),
transaction_ids=[txn.header_signature for txn in txns],
).SerializeToString()
2.2.3.2.创建批次
和创建交易流程相似,将其头部签名作为批次id,然后由交易头字节数组、批次id和交易一同构成批次。
from sawtooth_sdk.protobuf.batch_pb2 import Batch
signature = signer.sign(batch_header_bytes)
batch = Batch(
header=batch_header_bytes,
header_signature=signature,
transactions=txns
)
2.2.3.3.编码在批次列表中的批次
为了把若干批次提交到验证者,首先需要将他们收集到BatchList中,多个相关或者不相关的批次可以被提交到同一个BatchList中。但是和批次不同的地方是BatchList不是原子性的,处理时可能会将来自不同客户端的批次插入到同一个BatchList中。代码如下:
from sawtooth_sdk.protobuf.batch_pb2 import BatchList
batch_list_bytes = BatchList(batches=[batch]).SerializeToString()
2.2.4.提交批次到验证者中
客户端和验证者可以并行存在,两者通信使用REST API完成,首先将请求头的"Content-Type"设置为"application/octet-stream",然后把请求体设置为序列化的BatchList。
提交HTTP请求的方式很多,官网的例子中使用了urllib库:
import urllib.request
from urllib.error import HTTPError
try:
request = urllib.request.Request(
'http://rest.api.domain/batches',
batch_list_bytes,
method='POST',
headers={'Content-Type': 'application/octet-stream'})
response = urllib.request.urlopen(request)
except HTTPError as e:
response = e.file
如果是把批次列表存储成了文件,可以使用如下的代码进行读取:
output = open('intkey.batches', 'wb')
output.write(batch_list_bytes)
或者是抛开Python,直接用curl
命令进行提交:
curl --request POST \
--header "Content-Type: application/octet-stream" \
--data-binary @intkey.batches \
"http://rest.api.domain/batches
关于开发的关键步骤就是这些了,我本来计划是按照这个流程去实现一个简单的银行账户应用作为一个实践,但在做的时候发现在docker环境下可能会有网络问题,此外官方的XO的客户端还有一套比较复杂的命令行处理逻辑以及打包的流程,涉及Python的一些高级应用,所以目前暂时凉凉,之后还会通过对XO源码的阅读来尝试照猫画虎吧。