Important: This project is a research prototype. It represents ongoing work conducted within the TAST research project. Use with care.
This project contains Ethereum smart contracts that enable asynchronous smart contract calls across Ethereum-based blockchains.
This means that a smart contract on blockchain A is able to call a smart contract on blockchain B in a fully decentralized manner without having to worry about the underlying cross-blockchain communication.
The ability to verify transactions "across" different blockchains is vital to enable applications such as cross-blockchain token transfers.
Imagine you have the following two contracts as in the picture above.
contract ContractOnA {
...
function callRemoteFunction(address param1, uint param2) public {
bytes memory callData = abi.encodeWithSignature("remoteFunction(address,uint256)", param1, param2);
blockchainB.callContract(contractOnB, callIdentifier, callData, "callback");
}
function callback(uint callIdentifier, bytes memory result, bool success) public {
...
}
}
contract ContractOnB {
...
function remoteFunction(address param1, uint param2) public returns (uint) {
...
}
}
ContractOnA
is a smart contract deployed on blockchain A and ContractOnB
is a smart contract deployed on blockchain B.
When a user calls function callRemoteFunction()
of ContractOnA
it causes a call of function remoteFunction()
of ContractOnB
.
Once the the remote function call has been executed, the execution result of remoteFunction()
will be returned to
ContractOnA
by means of a custom callback function.
The project is based on traditional remote procedure calls (RPC). Accordingly, the project consists of two main contracts:
RPCProxy
RPCServer
Additionally, the following components are required which are not part of this repo.
RelayContract
(e.g., EthRelay)- Off-chain clients
Similar to RPCs, contracts can call a proxy contract (represented by RPCProxy
)
which sends a call request to a specialized contract on the remote blockchain (RPCServer
).
RPCServer
then calls the requested function of the requested contract on the remote blockchain.
After execution, the response is sent back to RPCPRoxy
which forwards it to the calling contract.
As blockchains cannot natively communicate with each other, the actual cross-blockchain communication is handled by a set of off-chain clients that pass transactions from one chain to the other.
When blockchain B (blockchain A) receives a transaction by an off-chain client from blockchain A (blockchain B),
blockchain B (blockchain A) needs to be certain that the transaction has actually been included in blockchain A (blockchain B).
For that, RPCProxy
and RPCServer
leverage blockchain relays, such as EthRelay.
Let us consider the example from above again. The following picture shows what happens under the hood when a remote call is made.
User calls function callRemoteFunction()
of ContractOnA
on blockchain A.
Function callRemoteFunction()
contains a cross-blockchain call, i.e., it wants to call function remoteFunction
of ContractOnB
on blockchain B.
ContractOnA
prepares the cross-blockchain call by calling the RPCProxy
.
RPCProxy blockchainB;
bytes memory callData = abi.encodeWithSignature("remoteFunction(address,uint256)", param1, param2);
blockchainB.callContract(contractOnB, callIdentifier, callData, "callback");
When ContractOnA
makes a cross-chain call using the RPCProxy
,
the RPCProxy
emits a "CallPrepared" event which is caught by a set of off-chain clients.
This step is only necessary as Ethereum transactions do not encode the complete call chain of a transactions. Only the address of the first called contract is contained in the transaction.
We need to make sure that cross-chain calls are only successful if
they are made using the correct RPCProxy
contract.
In Ethereum transactions, we only have access to the first address of the call chain, we do not know which "internal" contract calls have taken place.
The "to" field of a transaction only contains ContractOnA
's address and not the RPCProxy
’s address.
We have no way of knowing whether ContractOnA
subsequently called the right RPCProxy
or just a fake one. Two further steps are thus necessary.
After the off-chain client is informed about a pending cross-chain call via the "CallPrepared" event on chain A,
it can create a cross-chain call request via function RPCProxy.requestCall()
.
This is done by sending a transaction directly to the RPCProxy
.
This time, RPCProxy
is called directly, its address is therefore encoded in the "to" field of the transaction.
Thus, once the transaction is forwarded to blockchain B,
blockchain B can verify that it was the correct RPCProxy
which requested the call and not a fake proxy.
When the RPCProxy
executes the requestCall()
transaction, it emits a "CallRequested" event.
This indicates that the call can now be relayed for execution to blockchain B.
Once the off-chain client receives the "CallRequested" event,
it can relay the call to blockchain B.
That means, the transaction data that called requestCall()
is encoded in a transaction itself and
relayed to blockchain B.
This is done by calling function executeCall()
of contract RPCServer
on blockchain B.
When RPCServer
receives the transaction the following 2 steps are executed.
Before the call can be executed on ContractOnB
,
it needs to be checked that the requestCall()
transaction was actually included and executed successfully on blockchain A.
Further, it needs to be checked that the requestCall()
transaction was submitted to the correct
RPCProxy
contract on blockchain A.
After the RPCServer is sure that the call request actually exists on blockchain A,
it can now forward the actual call to ContractOnB
as indicated by the call request.
After the remote call has been executed by ContractOnB
, RPCServer
emits a "CallExecuted" event
encoding the result value from the remote call.
When the off-chain client receives the "CallExecuted" event, it knows that the application contract call was executed.
The acknowledgeCall()
transaction contains the transaction data of the executeCall()
transaction
that was executed on blockchain B.
Before the call acknowledgement can be returned to ContractOnA
,
it needs to be checked that executeCall()
has indeed been included and executed by RPCServer
on blockchain B.
Before the acknowledgement can be executed on ContractOnA
,
it needs to checked that the executeCall()
transaction was actually included and executed
successfully on blockchain B.
Further, it needs to be checked that the executeCall()
transaction was submitted to the correct RPCServer
contract on blockchain B.
After the RPCProxy
is sure that the executeCall()
transaction actually exists on blockchain B,
it can now execute the acknowledgement.
For that, it looks up the stored callback function for the call and calls this function of ContractOnA
.
After the callback has been executed by ContractOnA
,
the RPCProxy
contract emits a "CallAcknowledged" event.
When the off-chain clients receive the "CallAcknowledged" event, they know that the remote contract call has been completed.
A more detailed explanation can be found here.
This repository contains the RPCProxy
and RPCServer
contracts.
To deploy these contracts on a local blockchain you need to have the following tools installed:
Then execute the following steps:
- Clone the repository:
git clone [email protected]:pantos-io/x-chain-smartcontracts.git
- Change into the project directory:
cd x-chain-smartcontracts/
- Install all dependencies:
npm install
- Start Ganache (make sure it listens to port 7545)
- Deploy contracts:
truffle migrate --reset
- Run the tests with
truffle test
Testimonium is a research prototype. We welcome anyone to contribute. File a bug report or submit feature requests through the issue tracker. Feel free to submit a pull request.
- The development of this prototype was funded by Pantos within the TAST research project.
- The code for the RLPReader contract comes from Hamdi Allam.
This project is licensed under the MIT License.