Skip to content

AMB Home-to-Foreign async calls #570

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 6, 2021
Merged
1 change: 1 addition & 0 deletions contracts/interfaces/IAMB.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface IAMB {
function failedMessageSender(bytes32 _messageId) external view returns (address);
function requireToPassMessage(address _contract, bytes _data, uint256 _gas) external returns (bytes32);
function requireToConfirmMessage(address _contract, bytes _data, uint256 _gas) external returns (bytes32);
function requireToGetInformation(bytes32 _requestSelector, bytes _data) external returns (bytes32);
function sourceChainId() external view returns (uint256);
function destinationChainId() external view returns (uint256);
}
5 changes: 5 additions & 0 deletions contracts/interfaces/IAMBInformationReceiver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pragma solidity 0.4.24;

interface IAMBInformationReceiver {
function onInformationReceived(bytes32 messageId, bool status, bytes result) external;
}
15 changes: 14 additions & 1 deletion contracts/mocks/Box.sol
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
pragma solidity 0.4.24;

import "../interfaces/IAMB.sol";
import "../interfaces/IAMBInformationReceiver.sol";

contract Box {
contract Box is IAMBInformationReceiver {
uint256 public value;
address public lastSender;
bytes32 public messageId;
bytes32 public txHash;
uint256 public messageSourceChainId;
bool public status;
bytes public data;

function setValue(uint256 _value) public {
value = _value;
Expand Down Expand Up @@ -51,4 +54,14 @@ contract Box {
bytes memory encodedData = abi.encodeWithSelector(methodSelector, _i);
IAMB(_bridge).requireToConfirmMessage(_executor, encodedData, 141647);
}

function makeAsyncCall(address _bridge, bytes32 _selector, bytes _data) external {
IAMB(_bridge).requireToGetInformation(_selector, _data);
}

function onInformationReceived(bytes32 _messageId, bool _status, bytes _data) external {
messageId = _messageId;
data = _data;
status = _status;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
pragma solidity 0.4.24;

import "../../interfaces/IAMBInformationReceiver.sol";
import "./BasicHomeAMB.sol";

/**
* @title AsyncInformationProcessor
* @dev Functionality for making and processing async calls on Home side of the AMB.
*/
contract AsyncInformationProcessor is BasicHomeAMB {
event UserRequestForInformation(
bytes32 indexed messageId,
bytes32 indexed requestSelector,
address indexed sender,
bytes data
);
event SignedForInformation(address indexed signer, bytes32 indexed messageId);
event InformationRetrieved(bytes32 indexed messageId, bool status, bool callbackStatus);
event EnabledAsyncRequestSelector(bytes32 indexed requestSelector, bool enable);

/**
* @dev Makes an asynchronous request to get information from the opposite network.
* Call result will be returned later to the callee, by using the onInformationReceived(bytes) callback function.
* @param _requestSelector selector for the async request.
* @param _data payload for the given selector
*/
function requireToGetInformation(bytes32 _requestSelector, bytes _data) external returns (bytes32) {
// it is not allowed to pass messages while other messages are processed
// if other is not explicitly configured
require(messageId() == bytes32(0) || allowReentrantRequests());
// only contracts are allowed to call this method, since EOA won't be able to receive a callback.
require(AddressUtils.isContract(msg.sender));

require(isAsyncRequestSelectorEnabled(_requestSelector));

bytes32 _messageId = _getNewMessageId(sourceChainId());

_setAsyncRequestSender(_messageId, msg.sender);

emit UserRequestForInformation(_messageId, _requestSelector, msg.sender, _data);
return _messageId;
}

/**
* Tells if the specific async request selector is allowed to be used and supported by the bridge oracles.
* @param _requestSelector selector for the async request.
* @return true, if selector is allowed to be used.
*/
function isAsyncRequestSelectorEnabled(bytes32 _requestSelector) public view returns (bool) {
return boolStorage[keccak256(abi.encodePacked("enableRequestSelector", _requestSelector))];
}

/**
* Enables or disables the specific async request selector.
* Only owner can call this method.
* @param _requestSelector selector for the async request.
* @param _enable true, if the selector should be allowed.
*/
function enableAsyncRequestSelector(bytes32 _requestSelector, bool _enable) external onlyOwner {
boolStorage[keccak256(abi.encodePacked("enableRequestSelector", _requestSelector))] = _enable;

emit EnabledAsyncRequestSelector(_requestSelector, _enable);
}

/**
* @dev Submits result of the async call.
* Only validators are allowed to call this method.
* Once enough confirmations are collected, callback function is called.
* @param _messageId unique id of the request that was previously made.
* @param _status true, if JSON-RPC request succeeded, false otherwise.
* @param _result call result returned by the other side of the bridge.
*/
function confirmInformation(bytes32 _messageId, bool _status, bytes _result) external onlyValidator {
bytes32 hashMsg = keccak256(abi.encodePacked(_messageId, _status, _result));
bytes32 hashSender = keccak256(abi.encodePacked(msg.sender, hashMsg));
// Duplicated confirmations
require(!affirmationsSigned(hashSender));
setAffirmationsSigned(hashSender, true);

uint256 signed = numAffirmationsSigned(hashMsg);
require(!isAlreadyProcessed(signed));
// the check above assumes that the case when the value could be overflew will not happen in the addition operation below
signed = signed + 1;

setNumAffirmationsSigned(hashMsg, signed);

emit SignedForInformation(msg.sender, _messageId);

if (signed >= requiredSignatures()) {
setNumAffirmationsSigned(hashMsg, markAsProcessed(signed));
address sender = _restoreAsyncRequestSender(_messageId);
bytes memory data = abi.encodeWithSelector(
IAMBInformationReceiver(address(0)).onInformationReceived.selector,
_messageId,
_status,
_result
);
uint256 gas = maxGasPerTx();
require((gasleft() * 63) / 64 > gas);

bool callbackStatus = sender.call.gas(gas)(data);

emit InformationRetrieved(_messageId, _status, callbackStatus);
}
}

/**
* Internal function for saving async request sender for future use.
* @param _messageId id of the sent async request.
* @param _sender address of the request sender, receiver of the callback.
*/
function _setAsyncRequestSender(bytes32 _messageId, address _sender) internal {
addressStorage[keccak256(abi.encodePacked("asyncSender", _messageId))] = _sender;
}

/**
* Internal function for restoring async request sender information.
* @param _messageId id of the sent async request.
* @return address of async request sender and callback receiver.
*/
function _restoreAsyncRequestSender(bytes32 _messageId) internal returns (address) {
bytes32 hash = keccak256(abi.encodePacked("asyncSender", _messageId));
address sender = addressStorage[hash];

require(sender != address(0));

delete addressStorage[hash];
return sender;
}
}
4 changes: 2 additions & 2 deletions contracts/upgradeable_contracts/arbitrary_message/HomeAMB.sol
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
pragma solidity 0.4.24;

import "./BasicHomeAMB.sol";
import "./AsyncInformationProcessor.sol";

contract HomeAMB is BasicHomeAMB {
contract HomeAMB is AsyncInformationProcessor {
event UserRequestForSignature(bytes32 indexed messageId, bytes encodedData);
event AffirmationCompleted(
address indexed sender,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,7 @@ contract MessageDelivery is BasicAMB, MessageProcessor {
require(messageId() == bytes32(0) || allowReentrantRequests());
require(_gas >= getMinimumGasUsage(_data) && _gas <= maxGasPerTx());

bytes32 _messageId;
bytes memory header = _packHeader(_contract, _gas, _dataType);
_setNonce(_nonce() + 1);

assembly {
_messageId := mload(add(header, 32))
}
(bytes32 _messageId, bytes memory header) = _packHeader(_contract, _gas, _dataType);

bytes memory eventData = abi.encodePacked(header, _data);

Expand Down Expand Up @@ -68,19 +62,15 @@ contract MessageDelivery is BasicAMB, MessageProcessor {
function _packHeader(address _contract, uint256 _gas, uint256 _dataType)
internal
view
returns (bytes memory header)
returns (bytes32 _messageId, bytes memory header)
{
uint256 srcChainId = sourceChainId();
uint256 srcChainIdLength = _sourceChainIdLength();
uint256 dstChainId = destinationChainId();
uint256 dstChainIdLength = _destinationChainIdLength();

bytes32 mVer = MESSAGE_PACKING_VERSION;
uint256 nonce = _nonce();
_messageId = _getNewMessageId(srcChainId);

// Bridge id is recalculated every time again and again, since it is still cheaper than using SLOAD opcode (800 gas)
bytes32 bridgeId = keccak256(abi.encodePacked(srcChainId, address(this))) &
0x00000000ffffffffffffffffffffffffffffffffffffffff0000000000000000;
// 79 = 4 + 20 + 8 + 20 + 20 + 4 + 1 + 1 + 1
header = new bytes(79 + srcChainIdLength + dstChainIdLength);

Expand All @@ -97,11 +87,27 @@ contract MessageDelivery is BasicAMB, MessageProcessor {
mstore(add(header, 76), _gas)
mstore(add(header, 72), _contract)
mstore(add(header, 52), caller)

mstore(add(header, 32), or(mVer, or(bridgeId, nonce)))
mstore(add(header, 32), _messageId)
}
}

/**
* @dev Generates a new messageId for the passed request/message.
* Increments the nonce accordingly.
* @param _srcChainId source chain id of the newly created message. Should be a chain id of the current network.
* @return unique message id to use for the new request/message.
*/
function _getNewMessageId(uint256 _srcChainId) internal returns (bytes32) {
uint64 nonce = _nonce();
_setNonce(nonce + 1);

// Bridge id is recalculated every time again and again, since it is still cheaper than using SLOAD opcode (800 gas)
bytes32 bridgeId = keccak256(abi.encodePacked(_srcChainId, address(this))) &
0x00000000ffffffffffffffffffffffffffffffffffffffff0000000000000000;

return MESSAGE_PACKING_VERSION | bridgeId | bytes32(nonce);
}

/* solcov ignore next */
function emitEventOnMessageRequest(bytes32 messageId, bytes encodedData) internal;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ contract VersionableAMB is VersionableBridge {
* @return (major, minor, patch) version triple
*/
function getBridgeInterfacesVersion() external pure returns (uint64 major, uint64 minor, uint64 patch) {
return (5, 7, 0);
return (6, 0, 0);
}
}
122 changes: 121 additions & 1 deletion test/arbitrary_message/home_bridge.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const EternalStorageProxy = artifacts.require('EternalStorageProxy.sol')

const { expect } = require('chai')
const { ERROR_MSG, ZERO_ADDRESS, toBN } = require('../setup')
const { sign, ether, expectEventInLogs } = require('../helpers/helpers')
const { sign, ether, expectEventInLogs, getEvents } = require('../helpers/helpers')

const requiredBlockConfirmations = 8
const gasPrice = web3.utils.toWei('1', 'gwei')
Expand Down Expand Up @@ -915,4 +915,124 @@ contract('HomeAMB', async accounts => {
await homeContract.setChainIds('0x112233', '0x4455', { from: accounts[1] }).should.be.rejected
})
})
describe('async information retrieval', () => {
let homeContract
let box
let data

const ethCallSelector = web3.utils.soliditySha3('eth_call(address,bytes)')

beforeEach(async () => {
homeContract = await HomeAMB.new()
await homeContract.initialize(
HOME_CHAIN_ID_HEX,
FOREIGN_CHAIN_ID_HEX,
validatorContract.address,
1000000,
gasPrice,
requiredBlockConfirmations,
owner
).should.be.fulfilled
box = await Box.new()
data = web3.eth.abi.encodeParameters(
['address', 'bytes'],
[accounts[1], box.contract.methods.value().encodeABI()]
)
})

it('should enable new selector', async () => {
await homeContract.enableAsyncRequestSelector(ethCallSelector, true, { from: accounts[1] }).should.be.rejected
await homeContract.enableAsyncRequestSelector(ethCallSelector, true, { from: owner }).should.be.fulfilled

expect(await homeContract.isAsyncRequestSelectorEnabled(ethCallSelector)).to.be.equal(true)
})

it('should allow to request information from the other chain', async () => {
await homeContract.requireToGetInformation(ethCallSelector, data).should.be.rejected
await box.makeAsyncCall(homeContract.address, ethCallSelector, data).should.be.rejected
await homeContract.enableAsyncRequestSelector(ethCallSelector, true).should.be.fulfilled
await box.makeAsyncCall(homeContract.address, ethCallSelector, data).should.be.fulfilled

const events = await getEvents(homeContract, { event: 'UserRequestForInformation' })
expect(events.length).to.be.equal(1)
expect(events[0].returnValues.sender).to.be.equal(box.address)
expect(events[0].returnValues.requestSelector).to.be.equal(ethCallSelector)
expect(events[0].returnValues.data).to.be.equal(data)
})

it('should accept confirmations from a single validator', async () => {
await homeContract.enableAsyncRequestSelector(ethCallSelector, true).should.be.fulfilled
await box.makeAsyncCall(homeContract.address, ethCallSelector, data).should.be.fulfilled
const events = await getEvents(homeContract, { event: 'UserRequestForInformation' })
expect(events.length).to.be.equal(1)
const { messageId } = events[0].returnValues
const result = '0x0000000000000000000000000000000000000000000000000000000000000005'

await homeContract.confirmInformation(messageId, true, result, { from: owner }).should.be.rejected
const { logs } = await homeContract.confirmInformation(messageId, true, result, { from: authorities[0] }).should
.be.fulfilled
await homeContract.confirmInformation(messageId, true, result, { from: authorities[0] }).should.be.rejected
await homeContract.confirmInformation(messageId, true, result, { from: authorities[1] }).should.be.rejected
logs[0].event.should.be.equal('SignedForInformation')
expectEventInLogs(logs, 'InformationRetrieved', {
messageId,
status: true,
callbackStatus: true
})

expect(await box.data()).to.be.equal(result)
expect(await box.messageId()).to.be.equal(messageId)
expect(await box.status()).to.be.equal(true)
})
it('should accept confirmations from 2-of-3 validators', async () => {
await homeContract.enableAsyncRequestSelector(ethCallSelector, true).should.be.fulfilled
await validatorContract.setRequiredSignatures(2).should.be.fulfilled

await box.makeAsyncCall(homeContract.address, ethCallSelector, data).should.be.fulfilled
const events = await getEvents(homeContract, { event: 'UserRequestForInformation' })
expect(events.length).to.be.equal(1)
const { messageId } = events[0].returnValues
const result = '0x0000000000000000000000000000000000000000000000000000000000000005'

const { logs: logs1 } = await homeContract.confirmInformation(messageId, true, result, { from: authorities[0] })
.should.be.fulfilled
const { logs: logs2 } = await homeContract.confirmInformation(messageId, true, result, { from: authorities[1] })
.should.be.fulfilled
logs1[0].event.should.be.equal('SignedForInformation')
logs2[0].event.should.be.equal('SignedForInformation')

expectEventInLogs(logs2, 'InformationRetrieved', {
messageId,
status: true,
callbackStatus: true
})

expect(await box.data()).to.be.equal(result)
expect(await box.messageId()).to.be.equal(messageId)
expect(await box.status()).to.be.equal(true)
})
it('should process failed calls', async () => {
await homeContract.enableAsyncRequestSelector(ethCallSelector, true).should.be.fulfilled
await validatorContract.setRequiredSignatures(1).should.be.fulfilled

await box.makeAsyncCall(homeContract.address, ethCallSelector, data).should.be.fulfilled
const events = await getEvents(homeContract, { event: 'UserRequestForInformation' })
expect(events.length).to.be.equal(1)
const { messageId } = events[0].returnValues
const result = '0x0000000000000000000000000000000000000000000000000000000000000005'

const { logs } = await homeContract.confirmInformation(messageId, false, result, { from: authorities[0] }).should
.be.fulfilled
logs[0].event.should.be.equal('SignedForInformation')
expectEventInLogs(logs, 'InformationRetrieved', {
messageId,
status: false,
callbackStatus: true
})

expect(await box.data()).to.be.equal(result)
expect(await box.messageId()).to.be.equal(messageId)
expect(await box.status()).to.be.equal(false)
})
})
})