fabric-sdk-go解析交易数据详解

fabric-sdk-go不如Node.js SDK和Java SDK完善,提供的接口功能有限。例如虽然它提供了一个通过交易ID查询交易的方法,但直接返回的信息不易读,在使用中带来了不便 。为此,笔者研究了一下怎样解析交易详情并做相关记录。

本文参考了 亚楠老猎人的《令人懊恼的阉割版fabric sdk功能缺失》, 在此对其表示感谢!

本文环境为Fabric-1.4.6版本,fabric-go-sdk为v1.0.0-rc1版本。

一、QueryTransaction接口

和以太坊类似,fabric提交一笔写交易后会得到一个交易ID,我们可以根据这个交易ID去查询交易相关信息,然而fabric-sdk-go中提供的查询接口QueryTransaction返回的信息很有限(或者说不易读)。

这里是GoDoc中的该接口的相关文档,有兴趣的读者可以看一下,QueryTransaction,如果没有兴趣,接着往下看。

通过交易ID查询交易的接口如下:

func (c *Client) QueryTransaction(transactionID fab.TransactionID, options ...RequestOption) (*pb.ProcessedTransaction, error)

该函数的返回值对应的结构体为:

type ProcessedTransaction struct {
    
    
    // An Envelope which includes a processed transaction
    TransactionEnvelope *common.Envelope `protobuf:"bytes,1,opt,name=transactionEnvelope,proto3" json:"transactionEnvelope,omitempty"`
    // An indication of whether the transaction was validated or invalidated by committing peer
    ValidationCode       int32    `protobuf:"varint,2,opt,name=validationCode,proto3" json:"validationCode,omitempty"`
    XXX_NoUnkeyedLiteral struct{
    
    } `json:"-"`
    XXX_unrecognized     []byte   `json:"-"`
    XXX_sizecache        int32    `json:"-"`
}

可以看到,有一个ValidationCode字段代表验证状态码,我们再查看一下TransactionEnvelope对应的common.Envelope结构。

type Envelope struct {
    
    
    // A marshaled Payload
    Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"`
    // A signature by the creator specified in the Payload header
    Signature            []byte   `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"`
    XXX_NoUnkeyedLiteral struct{
    
    } `json:"-"`
    XXX_unrecognized     []byte   `json:"-"`
    XXX_sizecache        int32    `json:"-"`
}

可以看到,它有一个Signature,代表签名,可以直接转化成字符串。另外一个有用字段为Payload,也就是交易数据了,但是为[]byte类型,无法直接获取其内容,需要自己解析了。

这里可以看到,fabric-sdk-go提供的QueryTransaction接口返回的直接有用的数据只有验证状态码和签名,其Payload需要解析,但其并没有提供直接解析的方法,这个就需要自己动手解决了。

二、一些有用的内部接口

经过研究,发现fabric-sdk-go其实自带了相应的一些解析接口,只是都是些内部接口,需要自己根据实际需求来组合。话不多说,直接切重点,上代码。

所有用到的接口均位于:fabric-sdk-go/internal/github.com/hyperledger/fabric/protoutil/unmarshalers.go中,例如如下代码片断:

// UnmarshalPayload unmarshals bytes to a Payload
func UnmarshalPayload(encoded []byte) (*cb.Payload, error) {
    
    
	payload := &cb.Payload{
    
    }
	err := proto.Unmarshal(encoded, payload)
	return payload, errors.Wrap(err, "error unmarshaling Payload")
}

这个方法就是解析上面提到的Payload的。

本文中所有解析都是基于unmarshalers.go提供的方法。

三、解析基本交易信息

有眼尖的读者可能看到了,上面提到的unmarshalers.go位于internal下面的子目录下,也就是对应的包protoutil为一个内部包,在外部无法直接引用。为此,我们需要稍微修改一下fabric-sdk-go,在fabric-sdk-go目录下添加一个自定义的包来引用这些内部包。

在该目录下建立一个myutils包,并在该包下建立一个myutils.go文件,代码如下:

package myutils

import (
	"encoding/pem"
	"fmt"
	"time"

	"github.com/hyperledger/fabric-sdk-go/internal/github.com/hyperledger/fabric/protoutil"
	"github.com/tjfoc/gmsm/sm2"
)

//TransactionInfo 解析后的交易信息
type TransactionInfo struct {
    
    
	CreateTime       string   //交易创建时间
	ChaincodeID      string   //交易调用链码ID
	ChaincodeVersion string   //交易调用链码版本
	Nonce            []byte   //随机数
	Mspid            string   //交易发起者MSPID
	Name             string   //交易发起者名称
	OUTypes          string   //交易发起者OU分组
	Args             []string //输入参数
	Type             int32    //交易类型
	TxID             string   //交易ID
}

// UnmarshalTransaction 从某个交易的payload来解析它
func UnmarshalTransaction(payloadRaw []byte) (*TransactionInfo, error) {
    
    
	result := &TransactionInfo{
    
    }
	//解析成payload
	payload, err := protoutil.UnmarshalPayload(payloadRaw)
	if err != nil {
    
    
		return nil, err
	}
	//解析成ChannelHeader(包含通道ID、交易ID及交易创建时间等)
	channelHeader, err := protoutil.UnmarshalChannelHeader(payload.Header.ChannelHeader)
	if err != nil {
    
    
		return nil, err
	}
	//解析成SignatureHeader(包含创建者和nonce)
	signHeader, err := protoutil.UnmarshalSignatureHeader(payload.Header.SignatureHeader)
	if err != nil {
    
    
		return nil, err
	}
	//解析成SerializedIdentity(包含证书和MSPID)
	identity, err := protoutil.UnmarshalSerializedIdentity(signHeader.GetCreator())
	if err != nil {
    
    
		return nil, err
	}
	//下面为解析证书
	block, _ := pem.Decode(identity.GetIdBytes())
	if block == nil {
    
    
		return nil, fmt.Errorf("identity could not be decoded from credential")
	}
	cert, err := sm2.ParseCertificate(block.Bytes)
	if err != nil {
    
    
		return nil, fmt.Errorf("failed to parse certificate: %s", err)
	}
	//解析用户名和OU分组
	uname := cert.Subject.CommonName
	outypes := cert.Subject.OrganizationalUnit
	//解析成transaction
	tx, err := protoutil.UnmarshalTransaction(payload.Data)
	if err != nil {
    
    
		return nil, err
	}
	//进一步从transaction中解析成ChaincodeActionPayload
	chaincodeActionPayload, err := protoutil.UnmarshalChaincodeActionPayload(tx.Actions[0].Payload)
	if err != nil {
    
    
		return nil, err
	}
	//进一步解析成proposalPayload
	proposalPayload, err := protoutil.UnmarshalChaincodeProposalPayload(chaincodeActionPayload.ChaincodeProposalPayload)
	if err != nil {
    
    
		return nil, err
	}
	//得到交易调用的链码信息
	chaincodeInvocationSpec, err := protoutil.UnmarshalChaincodeInvocationSpec(proposalPayload.Input)
	if err != nil {
    
    
		return nil, err
	}
	//得到调用的链码的ID,版本和PATH(这里PATH省略了)
	result.ChaincodeID = chaincodeInvocationSpec.ChaincodeSpec.ChaincodeId.Name
	result.ChaincodeVersion = chaincodeInvocationSpec.ChaincodeSpec.ChaincodeId.Version
	//得到输入参数
	var args []string
	for _, v := range chaincodeInvocationSpec.ChaincodeSpec.Input.Args {
    
    
		args = append(args, string(v))
	}
	result.Args = args
	result.Nonce = signHeader.GetNonce()
	result.Type = channelHeader.GetType()
	result.TxID = channelHeader.GetTxId()
	result.Mspid = identity.GetMspid()
	result.Name = uname
	result.OUTypes = outypes[0]
	result.CreateTime = time.Unix(channelHeader.Timestamp.Seconds, 0).Format("2006-01-02 15:04:05")
	return result, nil
}

需要说明的是,因为使用了国密版本,所以这里导入了sm2包。

从这里可以看出,我们已经解析了几乎所有关键的交易信息。这里面也还有一些其它属性笔者并未获取或者解析,有兴趣的读者可以自己试着获取一下。

四、解析区块编号与区块哈希

从上面的解析我们可以看出,它只能解析出交易本身的信息。如果有人问,这个交易在哪个区块呢?显然,上面的解析是无法直接提供答案的,我们还需要手动去获取它。

4.1、获得交易所在区块

具体接口为QueryBlockByTxID,它在fabric-sdk-go/pkg/client/ledger/ledger.go文件中。该方法定义为:

func (c *Client) QueryBlockByTxID(txID fab.TransactionID, options ...RequestOption) (*common.Block, error)

它返回的区块基本信息(不包含区块数据)在BlockHeader结构体中。该结构体定义如下:

type BlockHeader struct {
    
    
	Number               uint64   `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"`
	PreviousHash         []byte   `protobuf:"bytes,2,opt,name=previous_hash,json=previousHash,proto3" json:"previous_hash,omitempty"`
	DataHash             []byte   `protobuf:"bytes,3,opt,name=data_hash,json=dataHash,proto3" json:"data_hash,omitempty"`
	XXX_NoUnkeyedLiteral struct{
    
    } `json:"-"`
	XXX_unrecognized     []byte   `json:"-"`
	XXX_sizecache        int32    `json:"-"`
}

从上面可以看出,得到的有用信息只有区块编号、前一区块哈希和数据哈希,并不包含区块本身哈希(显然不能包含区块本身哈希,因为这样会形成自嵌套无法计算)。

那么怎么得到本区块哈希呢?注意到fabric是区块链,所有的区块链基本上都是后一个区块通过前一区块的哈希来链接到前一区块(区块链这个名称就是这么来的),所以说我们只要得到后一区块的信息就能得到这个区块的哈希了。

4.2、获取通道信息

然而这里还有些要注意,如果本区块为最后区块,那么就没有后一个区块了。我们首先查询通道信息,得到特定通道的区块高度。具体接口为:QueryInfo,该函数定义为:

func (c *Client) QueryInfo(options ...RequestOption) (*fab.BlockchainInfoResponse, error)

BlockchainInfoResponse的定义为:

type BlockchainInfoResponse struct {
    
    
	BCI      *common.BlockchainInfo
	Endorser string
	Status   int32
}

BlockchainInfo的定义为:

type BlockchainInfo struct {
    
    
	Height               uint64   `protobuf:"varint,1,opt,name=height,proto3" json:"height,omitempty"`
	CurrentBlockHash     []byte   `protobuf:"bytes,2,opt,name=currentBlockHash,proto3" json:"currentBlockHash,omitempty"`
	PreviousBlockHash    []byte   `protobuf:"bytes,3,opt,name=previousBlockHash,proto3" json:"previousBlockHash,omitempty"`
	XXX_NoUnkeyedLiteral struct{
    
    } `json:"-"`
	XXX_unrecognized     []byte   `json:"-"`
	XXX_sizecache        int32    `json:"-"`
}

从上面的定义可以看出,我们可以得到当前区块高度和当前区块哈希。

4.3、比较通道区块高度和查询的区块编号

这里要注意,区块高度和编号是两个概念,编号从0开始,是高度值-1,相当于数组长度和下标的关系。

将通道区块高度和查询的区块编号进行比较:

  1. 如果查询区块为通道最新区块,则通道当前区块哈希就是查询区块哈希

  2. 如果查询区块不为通道最新区块,后面还有区块,则查询后一区块信息来得到本区块的哈希值。具体接口为:QueryBlock

    其定义为(blockNumber为区块编号):

    func (c *Client) QueryBlock(blockNumber uint64, options ...RequestOption) (*common.Block, error) 
    

    返回的内容在上面已经介绍过了。

五、解析结果示例

解析出来的结果类似如下格式(已经转化为json)。

{
    
    
      "validationcode": 0,
      "signature": "MEQCIAexXLnk2dhlxGjVLBcpNSOshb8iJhpC4V6IZ0U8R6IeAiACinGhT+Vy0TYb4G6E1Sutb5MDl0Gv2pm64ctN3BvWgg==",
      "BlockNumber": 14,
      "BlockHash": "T7K7Or3fw1ZR5PUfV/kBVKSLXuDZN1LnPWmxL/1sXF4=",
      "Info": {
    
    
          "CreateTime": "2020-12-09 22:33:01",
          "ChaincodeID": "mycc",
          "ChaincodeVersion": "",
          "Nonce": "kxXeBesXSmxEgkejqMdiTYC/YoBh6pKs",
          "Mspid": "Org1MSP",
          "Name": "user1",
          "OUTypes": "client",
          "Args": [
              "invoke",
              "a",
              "b",
              "10"
          ],
          "Type": 3,
          "TxID": "a7a80a1ac6eb50cf699d21f37bf37d3a9e67d46d12bbe08174e8757816ac7358"
      }
  }

好了,我们需要的交易信息终于解析完了。以后如果有人拿着一个交易ID问,这个交易上链了没有,我们可以从容不迫的给他显示相关信息了。
注意这里链码版本得到的就是为空。

由于笔者接触fabric时间不长,能力有限,如果文章有中错误或者需要改进,欢迎大家留言提出。

猜你喜欢

转载自blog.csdn.net/weixin_39430411/article/details/110942774