之前在部署bigchaindb时,给出了使用python利用bigchaindb-driver往bigchaindb中写事务的代码,本节根据这段代码,来跟踪分析bigchaindb写事务的整个流程
创建事务
往bigchaindb写事务代码如下
# coding=utf-8
# author: Wu Luo
import bigchaindb_driver
from bigchaindb_driver import BigchainDB
from bigchaindb_driver.crypto import generate_keypair
from time import sleep
import json
from sys import exit
import sys
class BigChainAPI(object):
def __init__(self, host, port=9984, conf='/root/.bigchaindb', timeout=60):
'''
:param host: cluster host of bigchaindb
:param port: port of bigchaindb
:param conf: the configuration file of bigchaindb
:param timeout: waiting vote for `timeout` seconds
:return:
'''
self.host = host
self.port = int(port)
self.bdb = BigchainDB("http://%s:%d" % (self.host, self.port))
self.conf = conf
self.timeout = int(timeout)
self.loadUserKey()
def loadUserKey(self):
'''
load user key from configuration files, as the valid keypairs are
designated in our deployment
:return:
'''
configuration = json.load(open(self.conf, "r"))
self.user = configuration['keypair']
def waitVote(self, txid):
'''
wait the transaction to be voted as valid
:param txid: the transaction id
:return: True/False
'''
print("txid is ", txid)
trials = 0
while trials < self.timeout:
try:
if self.bdb.transactions.status(txid).get('status') == 'valid':
print('Tx valid in:', trials, 'secs')
break
elif self.bdb.transactions.status(txid).get('status') == 'invalid':
print('Tx invalid in:', trials, 'secs')
print(self.bdb.transactions.status(txid))
break
else:
trials += 1
print("trials " + str(trials))
sleep(1)
except bigchaindb_driver.exceptions.NotFoundError:
trials += 1
sleep(1)
if trials == self.timeout:
print('Tx is still being processed... Bye!')
return False
return True
def writeTransaction(self, data, metadata=None):
'''
write data into bigchaindb
:param data: the data to write. it should be a dict
:param metadata: the metadata to write. it should be a dict
:return: the transaction id
'''
_asset = {
'data': data
}
prepared_creation_tx = self.bdb.transactions.prepare(
operation='CREATE',
signers=self.user['public'],
asset=_asset,
metadata=metadata
)
fulfilled_creation_tx = self.bdb.transactions.fulfill(
prepared_creation_tx,
private_keys=self.user['private']
)
sent_creation_tx = self.bdb.transactions.send(fulfilled_creation_tx)
txid = fulfilled_creation_tx['id']
if not self.waitVote(txid):
return False
return txid
def queryTransaction(self, txid):
'''
retrieve a transaction
:param txid: the txid of the transaction to query
:return: the data of the retrieved transaction
'''
data = self.bdb.transactions.retrieve(txid)
return data['asset']
def test():
bigchainAPI = BigChainAPI(host='10.0.0.71', port=9984, timeout=300)
# write an transaction
data = {
'author': 'Wu Luo'
}
txid = bigchainAPI.writeTransaction(data)
if txid == False:
print(">>> waiting for vote")
return False
# query this transaction
query_data = bigchainAPI.queryTransaction(txid)
print(">>> txid is " + txid)
print(">>> data retrieved from bigchainDB")
print(query_data)
test()
API代码在生成一个事务时需要完成三个任务:prepare\fulfill\send
。
prepare
prepare
的调用过程为:bigchaindb_driver.driver.TransactionsEndpoint::prepare
->bigchaindb_driver.offchain::prepare_transaction
->_prepare_transaction
->_prepare_create_transaction_dispatcher
->prepare_create_transaction
->bigchaindb.common.transaction.Transaction::create
。在创建成功之后,返回字典格式的Transaction
{
"inputs": [
{
"owners_before": [
"FUNkUCP8P95RR6bW81j9VyGySwVpVrq191jW2TLHepqF"
],
"fulfills": null,
"fulfillment": {
"public_key": "FUNkUCP8P95RR6bW81j9VyGySwVpVrq191jW2TLHepqF",
"signature": null,
"type": "ed25519-sha-256"
}
}
],
"asset": {
"data": {
"author": "Wu Luo"
}
},
"operation": "CREATE",
"metadata": {
"test": "Wu Luo"
},
"outputs": [
{
"public_keys": [
"FUNkUCP8P95RR6bW81j9VyGySwVpVrq191jW2TLHepqF"
],
"amount": "1",
"condition": {
"details": {
"public_key": "FUNkUCP8P95RR6bW81j9VyGySwVpVrq191jW2TLHepqF",
"signature": null,
"type": "ed25519-sha-256"
},
"uri": "ni:///sha-256;4FmTZj54liUWCSI6XatZKEPm--id9G30Uu9DjnXM3-8?fpt=ed25519-sha-256&cost=131072"
}
}
],
"id": "5516051b6f3c6d1e1608a0ec4b58cd2c5ec79f137554a7f756ae04d4d7b771fa",
"version": "1.0"
}
注意其中fulfillment
项中的signature
的值为null
fulfill
fulfill
的调用过程为:bigchaindb_driver.driver.TransactionsEndpoint::fulfill
->bigchaindb_driver.offchain::fulfill_transaction
def fulfill_transaction(transaction, *, private_keys):
if not isinstance(private_keys, (list, tuple)):
private_keys = [private_keys]
if isinstance(private_keys, tuple):
private_keys = list(private_keys)
transaction_obj = Transaction.from_dict(transaction)
try:
signed_transaction = transaction_obj.sign(private_keys)
except KeypairMismatchException as exc:
raise MissingPrivateKeyError('A private key is missing!') from exc
return signed_transaction.to_dict()
该函数的作用实际上是使用私钥对事务进行签名了,并且返回签名之后的事务。返回值fulfilled_creation_tx
相对于fulfill
(实际上也就是签名)前的内容,发生变化的为inputs[fulfillment]
项:使用私钥对事务进行的签名已经写入到inputs[fulfillment]
里
"inputs": [
{
"owners_before": [
"FUNkUCP8P95RR6bW81j9VyGySwVpVrq191jW2TLHepqF"
],
"fulfillment": "pGSAINcG5BlNY5w9qPTLocoKafLWPxyrsep73N6JxCD5-7YygUAC6NKfhXz--cqEJ4jaRnkDCQIOYv40xy2cAPVdVlHSBuy-24OO1Cdz0p_aPiQ75DHaNh7p866m3JMLtjnPs4MG",
"fulfills": null
}
],
send
语句sent_creation_tx = self.bdb.transactions.send(fulfilled_creation_tx)
的作用即是将事务写入bigchaindb了,我们下一步来分析bigchaindb如何处理创建事务的请求
处理创建事务请求
bigchaindb触发函数
self.bdb.transactions.send
将会调用bigchaindb-driver.driver.TransactionsEndpoint
的send函数。该函数的作用为发起一个http的post请求,此时self.path
的值为/api/v1/transactions/
def send(self, transaction, headers=None):
return self.transport.forward_request(
method='POST', path=self.path, json=transaction, headers=headers)
模块bigchaindb的web部分指定了处理对应http请求的应用(web部分源码分析之后再介绍)。在bigchaindb.web.routes
里设置了http请求的url与被触发的类的关系
ROUTES_API_V1 = [
r('/', info.ApiV1Index),
r('assets/', assets.AssetListApi),
r('blocks/<string:block_id>', blocks.BlockApi),
r('blocks/', blocks.BlockListApi),
r('statuses/', statuses.StatusApi),
r('transactions/<string:tx_id>', tx.TransactionApi),
r('transactions', tx.TransactionListApi),
r('outputs/', outputs.OutputListApi),
r('votes/', votes.VotesApi),
]
如此,可知,对于POST /api/v1/transactions/
请求将会被路由到tx.TransactionListApi
类中,并且执行的函数将是名为post
的函数。该函数首先从request中获取传输过来的json格式的transaction,将其转化为Transaction实例后,依次调用validate_transaction
与write_transaction
来对事务进行验证与写入。完成后将会往driver返回202
class TransactionListApi(Resource):
def post(self):
pool = current_app.config['bigchain_pool']
tx = request.get_json(force=True)
try:
tx_obj = Transaction.from_dict(tx)
except ...
with pool() as bigchain:
bigchain.statsd.incr('web.tx.post')
try:
bigchain.validate_transaction(tx_obj)
except ValidationError as e:
...
else:
bigchain.write_transaction(tx_obj)
response = jsonify(tx)
response.status_code = 202
response.autocorrect_location_header = False
status_monitor = '../statuses?transaction_id={}'.format(tx_obj.id)
response.headers['Location'] = status_monitor
return response
验证事务
首先根据validate_transaction
来分析bigchaindb如何验证事务,该函数最终会调用bigchaindb.models.Transaction
的validate函数,当transaction的operation为CREATE,即为创建资产时,validate
直接调用了inputs_valid
,进而调用_inputs_valid
# bigchaindb.models.Transaction
def validate(self, bigchain):
input_conditions = []
if self.operation == Transaction.TRANSFER:
...
if not self.inputs_valid(input_conditions):
raise InvalidSignature('Transaction signature is invalid.')
return self
# bigchaindb.common.transaction.Transaction
def inputs_valid(self, outputs=None):
if self.operation in (Transaction.CREATE, Transaction.GENESIS):
return self._inputs_valid(['dummyvalue'
for _ in self.inputs])
elif self.operation == Transaction.TRANSFER:
return self._inputs_valid([output.fulfillment.condition_uri
for output in outputs])
else:
allowed_ops = ', '.join(self.__class__.ALLOWED_OPERATIONS)
raise TypeError('`operation` must be one of {}'
.format(allowed_ops))
def _inputs_valid(self, output_condition_uris):
if len(self.inputs) != len(output_condition_uris):
raise ValueError('Inputs and '
'output_condition_uris must have the same count')
tx_dict = self.to_dict()
tx_dict = Transaction._remove_signatures(tx_dict)
tx_serialized = Transaction._to_str(tx_dict)
def validate(i, output_condition_uri=None):
""" Validate input against output condition URI """
return self._input_valid(self.inputs[i], self.operation,
tx_serialized, output_condition_uri)
return all(validate(i, cond)
for i, cond in enumerate(output_condition_uris))
根据我们之前给出的要写入的transaction内容,函数_inputs_valid
中self.inputs
与output_condition_uris
的内容为
# self.inputs
[{
"fulfills": null,
"owners_before": [
"FUNkUCP8P95RR6bW81j9VyGySwVpVrq191jW2TLHepqF"
],
"fulfillment": "pGSAINcG5BlNY5w9qPTLocoKafLWPxyrsep73N6JxCD5-7YygUAC6NKfhXz--cqEJ4jaRnkDCQIOYv40xy2cAPVdVlHSBuy-24OO1Cdz0p_aPiQ75DHaNh7p866m3JMLtjnPs4MG"
}]
# output_condition_uris
['dummyvalue']
语句self.to_dict()
的逻辑在于将transaction中所有inputs的fulfillment的值设置为None,也就是去除事务中的签名项(这一步的函数为Transaction._remove_signatures
),并且根据序列化之后的transaction进行hash计算出事务id——txid。
在return
语句时,将对output_condition_uris
的每一个元素调用_input_valid
进行验证。all()
语句的含义为只有实参中所有元素的bool值都为True时,all()
才返回True。故而,当且仅当所有_input_valid
验证成功时,_inputs_valid
返回True
# bigchaindb/common/transaction.py
def _input_valid(input_, operation, tx_serialized, output_condition_uri=None):
ccffill = input_.fulfillment
try:
parsed_ffill = Fulfillment.from_uri(ccffill.serialize_uri())
except (TypeError, ValueError,
ParsingError, ASN1DecodeError, ASN1EncodeError):
return False
if operation in (Transaction.CREATE, Transaction.GENESIS):
output_valid = True
else:
output_valid = output_condition_uri == ccffill.condition_uri
ffill_valid = parsed_ffill.validate(message=tx_serialized.encode())
return output_valid and ffill_valid
根据之前我们对fulfill
的理解,事务中的fulfillment
对应的是使用私钥对整个事务签名之后的值,故而_input_valid
的逻辑也显而易见了:通过fulfillment
项找到验证签名的类的实例(<cryptoconditions.types.ed25519.Ed25519Sha256 object at 0x7f80a42c8128>
),然后将去除了签名的事务内容当做消息,通过公钥来验证签名值是否正确,若匹配成功,则ffill_valid
将为True。实际上,validate
最后的实现函数我们也可以找到
def validate(self, *, message):
"""
Verify the signature of this Ed25519 fulfillment.
The signature of this Ed25519 fulfillment is verified against
the provided message and public key.
Args:
message (str): Message to validate against.
Return:
boolean: Whether this fulfillment is valid.
"""
try:
returned_message = VerifyKey(self.public_key).verify(
message, signature=self.signature)
except BadSignatureError:
return False
# TODO Check returned message against given message
return True
综上,验证CREATE
事务的逻辑为:去掉所有inputs里的签名值(fulfillment),获取到未签名的事务,同时,对于事务中的每个inputs,利用公钥来解析签名值(fulfillment),将解析后的值与未签名的事务做匹配,来验证签名是否正确
写入事务
在事务验证成功后即可开始写入事务了。write_transaction
为响应创建事务请求时将执行的函数
def write_transaction(self, signed_transaction):
signed_transaction = signed_transaction.to_dict()
if self.nodes_except_me:
assignee = random.choice(self.nodes_except_me)
else:
# I am the only node
assignee = self.me
signed_transaction.update({'assignee': assignee})
signed_transaction.update({'assignment_timestamp': time()})
# write to the backlog
return backend.query.write_transaction(self.connection, signed_transaction)
在需要写入事务时,bigchaindb首先从联盟节点中随机选取一个节点,将该事务分配为这个节点。若为单节点部署,则分配给本节点。之后利用字典的update函数将分配者的公钥信息写入事务中,同时附上分配事务时的时间戳。最后将事务写入到backlog中
# bigchaindb.backend.mongodb.query
@register_query(MongoDBConnection)
def write_transaction(conn, signed_transaction):
try:
return conn.run(
conn.collection('backlog')
.insert_one(signed_transaction))
except DuplicateKeyError:
return
根据之前的分析,在后端存储为mongodb时,write_transaction
的具体实现在bigchaindb.backend.mongodb.query
模块中,而该函数函数体也是将目前被签名的事务写入到backlog表中
值得注意的是,bigchaindb对于事务实际上维护了两个表:backlog与bigchain。事务创建后将放入backlog表中,在事务的分配者完成事务后,才会组合成区块放入到bigchain表中,同时也附带上了其他节点的投票信息
那么这个操作是如何完成的呢?实际上到目前为止,API的send函数的直接触发函数已经执行完成,但结果是将事务验证了签名后、选取分配节点、然后写入backlog表,而对backlog表中的操作就依赖于之前所提到的pipeline了,下篇分析pipeline如何完成这项任务