pseudoyu

pseudoyu

Blockchain | Programming | Photography | Boyi
github
twitter
telegram
mastodon
bilibili
jike

BitXHub Cross-Chain Plugin (Fabric) Source Code Interpretation

Introduction#

Previously, it was mentioned that the BitXHub cross-chain platform developed by Qulian Technology is one of the more comprehensive open-source solutions in the industry, mainly optimizing functionality, security, and flexibility in the cross-chain process through relay chains, gateways, and plugin mechanisms.

Currently, the company team is working on a cross-chain module for a BaaS platform, where I am responsible for the cross-chain adapter part, corresponding to the BitXHub platform's listening module and application chain plugin module. The adapter will listen for cross-chain events on the application chain and pass the corresponding parameters to the gateway for cross-chain related business logic requirements.

Therefore, I plan to conduct an in-depth interpretation of the BitXHub's meshplus/pier-client-fabric plugin source code to learn its excellent code structure and functional modules, in order to better implement my own adapter functionality.

Cross-Chain Transaction Process#

cross_chain_plugin

According to the cross-chain business requirements, a typical cross-chain calling process is shown in the above diagram.

  1. The sub-chain that needs to perform cross-chain transactions must install the adapter and deploy the provided cross-chain contract and business contract.
  2. When a user calls the business contract through the SDK, the contract will call the cross-chain contract and throw a cross-chain event.
  3. The corresponding adapter of the sub-chain will poll or subscribe to the cross-chain events thrown by the cross-chain contract and send them to the listening module of the cross-chain gateway.
  4. The cross-chain gateway will convert the response methods and parameters extracted from the cross-chain events into transactions recognizable by the target sub-chain.
  5. The cross-chain gateway will submit the converted transactions to the target sub-chain and execute them.

Adapter Mechanism#

Interface Design#

The adapter is mainly responsible for interaction with the sub-chain and participates in cross-chain interaction through interface calls. It mainly provides the following interfaces.

Call Chaincode#

The adapter receives transaction parameters sent by the cross-chain gateway, encapsulates them into a data structure accepted by the adapted sub-chain, and calls the chaincode.

Query Cross-Chain Transactions#

The sub-chain will store cross-chain related details in the payload field, such as contracts, users, etc. The adapter will parse and encapsulate this information, providing the corresponding interface for the cross-chain gateway to query.

Query Historical Transaction Information#

The adapter needs to provide a historical transaction query interface to facilitate proactive querying when cross-chain events are not received due to network transmission or other reasons.

Query Application Chain Basic Information#

The adapter needs to provide a query interface for the relevant information of the adapted sub-chain to facilitate queries by the cross-chain gateway, such as name, type, etc.

Source Code Interpretation#

Next, we will interpret the core functional module source code of the BitXHub cross-chain plugin (Fabric).

Design Pattern#

The plugin project adopts a typical "producer-consumer" model, which is very suitable for concurrent scenarios that require polling/subscribing to receive data. This model utilizes the feature that at any given time, only one goroutine accesses a certain piece of data in the channel.

Subscribe/Poll Cross-Chain Events#

The plugin needs to construct a producer object to subscribe to the cross-chain events of its corresponding sub-chain.

// Construct producer
ec, err := event.New(c.channelProvider, event.WithBlockEvents())
if err != nil {
    return fmt.Errorf("failed to create fabcli, error: %v", err)
}

c.eventClient = ec

// Subscribe to cross-chain events
registration, notifier, err := ec.RegisterChaincodeEvent(c.meta.CCID, c.meta.EventFilter)
if err != nil {
    return fmt.Errorf("failed to register chaincode event, error: %v", err)
}
c.registration = registration

The method for subscribing to events calls the RegisterChaincodeEvent() method of fabric-sdk-go. It is important to note that when you no longer need to listen to events, you need to call the Unregister() method to cancel the subscription.

The ccID in the method is the chaincode ID that needs to be listened to, and eventFilter is the chaincode event that needs to be listened to. This method will return a channel to receive data (when unsubscribed, the channel will close).

func (c *Client) RegisterChaincodeEvent(ccID, eventFilter string) (fab.Registration, <-chan *fab.CCEvent, error) {
	return c.eventService.RegisterChaincodeEvent(ccID, eventFilter)
}

Both the subscribed cross-chain contract object (i.e., the producer) and the consumer are placed in an infinite loop. When a cross-chain event is thrown, the producer will continuously put data into the channel, while the consumer will continuously take data out of the channel.

go func() {
    for {
        select {
        // Producer writes cross-chain event to channel
        case ccEvent := <-notifier:
            if ccEvent != nil {
                c.handle(ccEvent)
            }
        // Consumer takes cross-chain event data from channel
        case <-c.ctx:
            return
        }
    }
}()

Since both the producer and consumer are in an infinite loop, the producer's goroutine will not exit, and the channel will continue to write data. When there are no new events, the consumer will block, waiting for the producer to receive new data and write it to the channel.

Plugin Initialization, Running, and Stopping#

Having looked at the overall design pattern, let's examine the mechanism of the entire plugin project running from the main entry of the program.

Initialization#

In the client program initialization, the consumer object is first constructed based on a custom structure.

// Construct consumer
mgh, err := newFabricHandler(contractmeta.EventFilter, eventC, appchainID)
if err != nil {
    return err
}

done := make(chan bool)
csm, err := NewConsumer(configPath, contractmeta, mgh, done)
if err != nil {
    return err
}

Running#

The entry point of the program is very simple, which is to poll the cross-chain contract and start the consumer object.

func (c *Client) Start() error {
	logger.Info("Fabric consumer started")
	go c.polling()
	return c.consumer.Start()
}

Stopping#

Stopping the plugin is also very simple, which is to stop the program and cancel the subscription to events.

// Stop plugin
func (c *Client) Stop() error {
	c.ticker.Stop()
	c.done <- true
	return c.consumer.Shutdown()
}

In the consumer package, cancel the subscription to events.

func (c *Consumer) Shutdown() error {
	c.eventClient.Unregister(c.registration)
	return nil
}

Looking deeper, canceling the subscription to events calls the Unregister() method of fabric-sdk-go, which will cancel the subscription to the event and close the corresponding channel.

func (c *Client) Unregister(reg fab.Registration) {
	c.eventService.Unregister(reg)
}

Interface Implementation#

In addition to subscribing to events, the plugin also provides a series of query interfaces for the gateway to call, in order to complete the corresponding cross-chain operations.

getProof()#

For example, to obtain Proof information, etc.

func (c *Client) getProof(response channel.Response) ([]byte, error) {
	var proof []byte
	var handle = func(response channel.Response) ([]byte, error) {
		// query proof from fabric
		l, err := ledger.New(c.consumer.channelProvider)
		if err != nil {
			return nil, err
		}

		t, err := l.QueryTransaction(response.TransactionID)
		if err != nil {
			return nil, err
		}
		pd := &common.Payload{}
		if err := proto.Unmarshal(t.TransactionEnvelope.Payload, pd); err != nil {
			return nil, err
		}

		pt := &peer.Transaction{}
		if err := proto.Unmarshal(pd.Data, pt); err != nil {
			return nil, err
		}

		return pt.Actions[0].Payload, nil
	}

	if err := retry.Retry(func(attempt uint) error {
		var err error
		proof, err = handle(response)
		if err != nil {
			logger.Error("Can't get proof", "error", err.Error())
			return err
		}
		return nil
	}, strategy.Wait(2*time.Second)); err != nil {
		logger.Error("Can't get proof", "error", err.Error())
	}

	return proof, nil
}

getChainID()#

This interface is used to obtain the chain ID.

func (c *Client) GetChainID() (string, string) {
	request := channel.Request{
		ChaincodeID: c.meta.CCID,
		Fcn:         GetChainId,
	}

	response, err := c.consumer.ChannelClient.Execute(request)
	if err != nil || response.Payload == nil {
		return "", ""
	}
	chainIds := strings.Split(string(response.Payload), "-")
	if len(chainIds) != 2 {
		return "", ""
	}
	return chainIds[0], chainIds[1]
}

Other Interfaces#

For more implementation details of other interfaces, see meshplus/pier-client-fabric/client.go.

Cross-Chain Contract#

The cross-chain contract is an important part of implementing the plugin's listening function. When the business requires cross-chain, it will uniformly call the cross-chain contract and interact with the cross-chain gateway.

The cross-chain contract provides a series of interfaces for business contracts to implement. Therefore, writing business contracts according to certain specifications can simplify the development and maintenance of cross-chain business. The specifications for writing cross-chain contracts can be found in the <Cross-Chain Contract Writing Documentation>.

Event Implementation#

How does the cross-chain contract throw cross-chain events to the plugin?

In the Invoke() method of the cross-chain contract, the cross-chain contract first obtains the calling method and corresponding parameters of the contract caller (which is the business contract) through the GetFunctionAndParameters() method, and then calls different contracts based on the method name.

func (broker *Broker) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
	function, args := stub.GetFunctionAndParameters()
    // ...
    	switch function {
            // ...
            case "getChainId":
                return broker.getChainId(stub)
            case "getInMessage":
                return broker.getInMessage(stub, args)
            case "getOutMessage":
                return broker.getOutMessage(stub, args)
            // ...
            case "EmitInterchainEvent":
                return broker.EmitInterchainEvent(stub, args)
            default:
                return shim.Error("invalid function: " + function + ", args: " + strings.Join(args, ","))
	}
}

Let's focus on what happens when EmitInterchainEvent() is called, as explained in the comments.

func (broker *Broker) EmitInterchainEvent(stub shim.ChaincodeStubInterface, args []string) pb.Response {
    // Check if the number of input parameters is correct
    // The cross-chain contract requires many parameters, as failures in the chain can easily lead to security issues
	if len(args) != 5 {
		return shim.Error("incorrect number of arguments, expecting 7")
	}

	// Read parameters and store them in corresponding variables

	// Target chain ID
	dstServiceID := args[0]

	// Own chaincode ID
	cid, err := getChaincodeID(stub)
	if err != nil {
		return shim.Error(err.Error())
	}

	// Get bxhID and appchainID
	curFullID, err := broker.genFullServiceID(stub, cid)
	if err != nil {
		return shim.Error(err.Error())
	}

	// Combine the current chain ID and target chain ID into an output cross-chain service pair
	outServicePair := genServicePair(curFullID, dstServiceID)

	// Get the key-value pairs of the output values
	outMeta, err := broker.getMap(stub, outterMeta)
	if err != nil {
		return shim.Error(err.Error())
	}

	// Query whether the output cross-chain service pair exists in the key-value pairs, otherwise set it to 0
	if _, ok := outMeta[outServicePair]; !ok {
		outMeta[outServicePair] = 0
	}

	// Encapsulate transaction information
	tx := &Event{
		Index:     outMeta[outServicePair] + 1,
		DstFullID: dstServiceID,
		SrcFullID: curFullID,
		Func:      args[1],
		Args:      args[2],
		Argscb:    args[3],
		Argsrb:    args[4],
	}

	// Increment the output service
	outMeta[outServicePair]++

	// Convert transaction information to JSON format
	txValue, err := json.Marshal(tx)
	if err != nil {
		return shim.Error(err.Error())
	}

	// Format the output event message
	key := broker.outMsgKey(outServicePair, strconv.FormatUint(tx.Index, 10))

	// Write the message and transaction information to the ledger (persist)
	if err := stub.PutState(key, txValue); err != nil {
		return shim.Error(fmt.Errorf("persist event: %w", err).Error())
	}

	// Set the corresponding cross-chain transaction event name and store the transaction information in the payload
	if err := stub.SetEvent(interchainEventName, txValue); err != nil {
		return shim.Error(fmt.Errorf("set event: %w", err).Error())
	}

	// Write the metadata state to the ledger
	if err := broker.putMap(stub, outterMeta, outMeta); err != nil {
		return shim.Error(err.Error())
	}

	return shim.Success(nil)
}

The above is what happens when calling the cross-chain contract. Essentially, it just sets a trigger event in the cross-chain contract using SetEvent(), and then subscribes to it in the plugin using RegisterChaincodeEvent().

SetEvent(name string, payload []byte) error

SetEvent() is an interface under the shim package, mainly passing in the name and payload array. For details on the principles and details of chaincode event listening, see <Hyperledger Fabric Go SDK Event Analysis>.

Business Contract#

After analyzing the cross-chain contract, let's see how the business contract calls the cross-chain contract, taking the data_swapper.go data exchange contract in the example as an example.

func (s *DataSwapper) get(stub shim.ChaincodeStubInterface, args []string) pb.Response {
	switch len(args) {
	case 1:
		// args[0]: key
		value, err := stub.GetState(args[0])
		if err != nil {
			return shim.Error(err.Error())
		}

		return shim.Success(value)
	case 2:
		// args[0]: destination service id
		// args[1]: key
		b := util.ToChaincodeArgs(emitInterchainEventFunc, args[0], "interchainGet,interchainSet,", args[1], args[1], "")
		response := stub.InvokeChaincode(brokerContractName, b, channelID)
		if response.Status != shim.OK {
			return shim.Error(fmt.Errorf("invoke broker chaincode %s error: %s", brokerContractName, response.Message).Error())
		}

		return shim.Success(nil)
	default:
		return shim.Error("incorrect number of arguments")
	}
}

To obtain information from another chain in the data_swapper.go business contract, the switch...case... first checks the length of the input parameter array args []string when calling the get method. When the length is 1, it normally calls its own contract for querying, while when the length is 2, it first converts the parameters from string to chaincode argument array format using the ToChaincodeArgs() method provided by Fabric.

func ToChaincodeArgs(args ...string) [][]byte {
	bargs := make([][]byte, len(args))
	for i, arg := range args {
		bargs[i] = []byte(arg)
	}
	return bargs
}

Then, it directly calls the cross-chain contract in the business chaincode using the InvokeChaincode() method, passing in the parameters and channel ID, thus completing a cross-chain data query chaincode call.

Conclusion#

The above is an interpretation of the cross-chain transaction process and the BitXHub cross-chain plugin (Fabric) source code. It is also hoped that this process deepens the understanding of the cross-chain mechanism and related platforms, enabling better participation in its open-source construction in the future.

References#

  1. Cross-Chain Technology Platform BitXHub
  2. BitXHub Document
  3. meshplus/pier-client-fabric
  4. Ten Questions about BitXHub: Discussing the Architectural Design of Cross-Chain Platforms
  5. Cross-Chain Contract Writing Documentation
  6. Hyperledger Fabric Go SDK Event Analysis
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.