Skip to content
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)
})
})
})