bigchaindb源码分析(五)——写事务(上)

之前在部署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_transactionwrite_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_validself.inputsoutput_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如何完成这项任务

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值