Skip to content

Commit d83a209

Browse files
committedMay 10, 2019
meta-txs: optimize volatile sender using calldata
1 parent a576413 commit d83a209

File tree

12 files changed

+153
-59
lines changed

12 files changed

+153
-59
lines changed
 

‎contracts/apps/AppStorage.sol

-10
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,9 @@ contract AppStorage {
1515
* Hardcoded constants to save gas
1616
* bytes32 internal constant KERNEL_POSITION = keccak256("aragonOS.appStorage.kernel");
1717
* bytes32 internal constant APP_ID_POSITION = keccak256("aragonOS.appStorage.appId");
18-
* bytes32 internal constant VOLATILE_SENDER_POSITION = keccak256("aragonOS.appStorage.volatile.sender");
1918
*/
2019
bytes32 internal constant KERNEL_POSITION = 0x4172f0f7d2289153072b0a6ca36959e0cbe2efc3afe50fc81636caa96338137b;
2120
bytes32 internal constant APP_ID_POSITION = 0xd625496217aa6a3453eecb9c3489dc5a53e6c67b444329ea2b2cbc9ff547639b;
22-
bytes32 internal constant VOLATILE_SENDER_POSITION = 0xd6486d5aa3dac4242db35dd7559408452252cf8050988dbc66956eaa315379ce;
2321

2422
function kernel() public view returns (IKernel) {
2523
return IKernel(KERNEL_POSITION.getStorageAddress());
@@ -29,19 +27,11 @@ contract AppStorage {
2927
return APP_ID_POSITION.getStorageBytes32();
3028
}
3129

32-
function volatileStorageSender() public view returns (address) {
33-
return VOLATILE_SENDER_POSITION.getStorageAddress();
34-
}
35-
3630
function setKernel(IKernel _kernel) internal {
3731
KERNEL_POSITION.setStorageAddress(address(_kernel));
3832
}
3933

4034
function setAppId(bytes32 _appId) internal {
4135
APP_ID_POSITION.setStorageBytes32(_appId);
4236
}
43-
44-
function setVolatileStorageSender(address _sender) internal {
45-
VOLATILE_SENDER_POSITION.setStorageAddress(_sender);
46-
}
4737
}

‎contracts/common/MemoryHelpers.sol

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
pragma solidity ^0.4.24;
2+
3+
4+
library MemoryHelpers {
5+
6+
function append(bytes memory self, address addr) internal pure returns (bytes memory) {
7+
// alloc required encoded data size
8+
uint256 dataSize = self.length;
9+
uint256 appendedDataSize = dataSize + 32;
10+
bytes memory appendedData = new bytes(appendedDataSize);
11+
12+
// copy data
13+
uint256 inputPointer;
14+
uint256 outputPointer;
15+
assembly {
16+
inputPointer := add(self, 0x20)
17+
outputPointer := add(appendedData, 0x20)
18+
}
19+
memcpy(outputPointer, inputPointer, dataSize);
20+
21+
// append address
22+
assembly {
23+
let signerPointer := add(add(appendedData, 0x20), dataSize)
24+
mstore(signerPointer, addr)
25+
}
26+
27+
return appendedData;
28+
}
29+
30+
// From https://github.com/Arachnid/solidity-stringutils/blob/master/src/strings.sol
31+
function memcpy(uint256 dest, uint256 src, uint256 len) private pure {
32+
// Copy word-length chunks while possible
33+
for(; len >= 32; len -= 32) {
34+
assembly {
35+
mstore(dest, mload(src))
36+
}
37+
dest += 32;
38+
src += 32;
39+
}
40+
41+
// Copy remaining bytes
42+
uint256 mask = 256 ** (32 - len) - 1;
43+
assembly {
44+
let srcpart := and(mload(src), not(mask))
45+
let destpart := and(mload(dest), mask)
46+
mstore(dest, or(destpart, srcpart))
47+
}
48+
}
49+
}

‎contracts/kernel/IKernel.sol

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
pragma solidity ^0.4.24;
66

77
import "../acl/IACL.sol";
8+
import "../relayer/IRelayer.sol";
89
import "../common/IVaultRecoverable.sol";
910

1011

@@ -16,6 +17,7 @@ interface IKernelEvents {
1617
// This should be an interface, but interfaces can't inherit yet :(
1718
contract IKernel is IKernelEvents, IVaultRecoverable {
1819
function acl() public view returns (IACL);
20+
function relayer() public view returns (IRelayer);
1921
function hasPermission(address who, address where, bytes32 what, bytes how) public view returns (bool);
2022

2123
function setApp(bytes32 namespace, bytes32 appId, address app) public;

‎contracts/kernel/Kernel.sol

+10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import "./KernelConstants.sol";
55
import "./KernelStorage.sol";
66
import "../acl/IACL.sol";
77
import "../acl/ACLSyntaxSugar.sol";
8+
import "../relayer/IRelayer.sol";
89
import "../common/ConversionHelpers.sol";
910
import "../common/IsContract.sol";
1011
import "../common/Petrifiable.sol";
@@ -169,6 +170,7 @@ contract Kernel is IKernel, KernelStorage, KernelAppIds, KernelNamespaceConstant
169170
function APP_ADDR_NAMESPACE() external pure returns (bytes32) { return KERNEL_APP_ADDR_NAMESPACE; }
170171
function KERNEL_APP_ID() external pure returns (bytes32) { return KERNEL_CORE_APP_ID; }
171172
function DEFAULT_ACL_APP_ID() external pure returns (bytes32) { return KERNEL_DEFAULT_ACL_APP_ID; }
173+
function DEFAULT_RELAYER_APP_ID() external pure returns (bytes32) { return KERNEL_DEFAULT_RELAYER_APP_ID; }
172174
/* solium-enable function-order, mixedcase */
173175

174176
/**
@@ -197,6 +199,14 @@ contract Kernel is IKernel, KernelStorage, KernelAppIds, KernelNamespaceConstant
197199
return IACL(getApp(KERNEL_APP_ADDR_NAMESPACE, KERNEL_DEFAULT_ACL_APP_ID));
198200
}
199201

202+
/**
203+
* @dev Get the installed Relayer app
204+
* @return Relayer app
205+
*/
206+
function relayer() public view returns (IRelayer) {
207+
return IRelayer(getApp(KERNEL_APP_ADDR_NAMESPACE, KERNEL_DEFAULT_RELAYER_APP_ID));
208+
}
209+
200210
/**
201211
* @dev Function called by apps to check ACL on kernel or to check permission status
202212
* @param _who Sender of the original call

‎contracts/kernel/KernelConstants.sol

+2
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ contract KernelAppIds {
1010
bytes32 internal constant KERNEL_CORE_APP_ID = apmNamehash("kernel");
1111
bytes32 internal constant KERNEL_DEFAULT_ACL_APP_ID = apmNamehash("acl");
1212
bytes32 internal constant KERNEL_DEFAULT_VAULT_APP_ID = apmNamehash("vault");
13+
bytes32 internal constant KERNEL_DEFAULT_VAULT_APP_ID = apmNamehash("relayer");
1314
*/
1415
bytes32 internal constant KERNEL_CORE_APP_ID = 0x3b4bf6bf3ad5000ecf0f989d5befde585c6860fea3e574a4fab4c49d1c177d9c;
1516
bytes32 internal constant KERNEL_DEFAULT_ACL_APP_ID = 0xe3262375f45a6e2026b7e7b18c2b807434f2508fe1a2a3dfb493c7df8f4aad6a;
1617
bytes32 internal constant KERNEL_DEFAULT_VAULT_APP_ID = 0x7e852e0fcfce6551c13800f1e7476f982525c2b5277ba14b24339c68416336d1;
18+
bytes32 internal constant KERNEL_DEFAULT_RELAYER_APP_ID = 0x7641595d1a2007abf0fe95c31d0b7a822954acbf6fb0cbe3bd1161d9dec9e1d3;
1719
}
1820

1921

‎contracts/relayer/IRelayer.sol

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pragma solidity ^0.4.24;
2+
3+
4+
contract IRelayer {
5+
function relay(address from, address to, uint256 nonce, bytes calldata, bytes signature) external;
6+
}
+12-21
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,25 @@
11
pragma solidity ^0.4.24;
22

3-
3+
import "./IRelayer.sol";
44
import "../apps/AragonApp.sol";
55

66

7-
interface IRelayedAragonApp {
8-
function exec(address from, bytes calldata) external;
9-
}
7+
contract RelayedAragonApp is AragonApp {
108

11-
contract RelayedAragonApp is IRelayedAragonApp, AragonApp {
12-
bytes32 public constant RELAYER_ROLE = keccak256("RELAYER_ROLE");
9+
function sender() internal view returns (address) {
10+
address relayer = address(_relayer());
11+
if (msg.sender != relayer) return msg.sender;
1312

14-
function exec(address from, bytes calldata) external auth(RELAYER_ROLE) {
15-
setVolatileStorageSender(from);
16-
bool success = address(this).call(calldata);
17-
if (!success) revertForwardingError();
18-
setVolatileStorageSender(address(0));
13+
address signer = _decodeSigner();
14+
return signer != address(0) ? signer : relayer;
1915
}
2016

21-
function sender() internal view returns (address) {
22-
if (msg.sender != address(this)) return msg.sender;
23-
address volatileSender = volatileStorageSender();
24-
return volatileSender != address(0) ? volatileSender : address(this);
17+
function _decodeSigner() internal returns (address signer) {
18+
bytes memory calldata = msg.data;
19+
assembly { signer := mload(add(calldata, calldatasize)) }
2520
}
2621

27-
function revertForwardingError() private {
28-
assembly {
29-
let ptr := mload(0x40)
30-
returndatacopy(ptr, 0, returndatasize)
31-
revert(ptr, returndatasize)
32-
}
22+
function _relayer() internal returns (IRelayer) {
23+
return kernel().relayer();
3324
}
3425
}

‎contracts/relayer/Relayer.sol

+27-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
pragma solidity ^0.4.24;
22

3+
import "./IRelayer.sol";
34
import "./RelayedAragonApp.sol";
45
import "../lib/sig/ECDSA.sol";
56
import "../apps/AragonApp.sol";
7+
import "../common/MemoryHelpers.sol";
68
import "../common/DepositableStorage.sol";
79

810

9-
contract Relayer is AragonApp, DepositableStorage {
11+
contract Relayer is IRelayer, AragonApp, DepositableStorage {
1012
using ECDSA for bytes32;
13+
using MemoryHelpers for bytes;
1114

1215
bytes32 public constant ALLOW_OFF_CHAIN_SERVICE_ROLE = keccak256("ALLOW_OFF_CHAIN_SERVICE_ROLE");
1316
bytes32 public constant DISALLOW_OFF_CHAIN_SERVICE_ROLE = keccak256("DISALLOW_OFF_CHAIN_SERVICE_ROLE");
@@ -21,7 +24,7 @@ contract Relayer is AragonApp, DepositableStorage {
2124

2225
event ServiceAllowed(address indexed service);
2326
event ServiceDisallowed(address indexed service);
24-
event TransactionRelayed(address indexed from, address indexed to, uint256 nonce, bytes calldata);
27+
event TransactionRelayed(address from, address to, uint256 nonce, bytes calldata);
2528

2629
mapping (address => bool) internal allowedServices;
2730
mapping (address => uint256) internal lastUsedNonce;
@@ -48,8 +51,9 @@ contract Relayer is AragonApp, DepositableStorage {
4851
assertValidTransaction(from, nonce, calldata, signature);
4952

5053
lastUsedNonce[from] = nonce;
51-
IRelayedAragonApp(to).exec(from, calldata);
54+
relayCall(from, to, calldata);
5255
emit TransactionRelayed(from, to, nonce, calldata);
56+
forwardReturnedData();
5357
}
5458

5559
function allowService(address service) external authP(ALLOW_OFF_CHAIN_SERVICE_ROLE, arr(service)) {
@@ -84,4 +88,24 @@ contract Relayer is AragonApp, DepositableStorage {
8488
function messageHash(bytes calldata, uint256 nonce) internal pure returns (bytes32) {
8589
return keccak256(abi.encodePacked(keccak256(calldata), nonce));
8690
}
91+
92+
function relayCall(address from, address to, bytes calldata) private {
93+
bytes memory encodedSignerCalldata = calldata.append(from);
94+
assembly {
95+
let success := call(gas, to, 0, add(encodedSignerCalldata, 0x20), mload(encodedSignerCalldata), 0, 0)
96+
switch success case 0 {
97+
let ptr := mload(0x40)
98+
returndatacopy(ptr, 0, returndatasize)
99+
revert(ptr, returndatasize)
100+
}
101+
}
102+
}
103+
104+
function forwardReturnedData() private {
105+
assembly {
106+
let ptr := mload(0x40)
107+
returndatacopy(ptr, 0, returndatasize)
108+
return(ptr, returndatasize)
109+
}
110+
}
87111
}

‎contracts/test/mocks/common/KeccakConstants.sol

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ contract KeccakConstants {
2222
bytes32 public constant KERNEL_APP_ID = keccak256(abi.encodePacked(APM_NODE, keccak256("kernel")));
2323
bytes32 public constant DEFAULT_ACL_APP_ID = keccak256(abi.encodePacked(APM_NODE, keccak256("acl")));
2424
bytes32 public constant DEFAULT_VAULT_APP_ID = keccak256(abi.encodePacked(APM_NODE, keccak256("vault")));
25+
bytes32 public constant DEFAULT_RELAYER_APP_ID = keccak256(abi.encodePacked(APM_NODE, keccak256("relayer")));
2526

2627
// ACL
2728
bytes32 public constant CREATE_PERMISSIONS_ROLE = keccak256(abi.encodePacked("CREATE_PERMISSIONS_ROLE"));

‎contracts/test/mocks/kernel/KernelConstantsMock.sol

+1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ contract KernelConstantsMock is Kernel {
1212
function getKernelAppId() external pure returns (bytes32) { return KERNEL_CORE_APP_ID; }
1313
function getDefaultACLAppId() external pure returns (bytes32) { return KERNEL_DEFAULT_ACL_APP_ID; }
1414
function getDefaultVaultAppId() external pure returns (bytes32) { return KERNEL_DEFAULT_VAULT_APP_ID; }
15+
function getDefaultRelayerAppId() external pure returns (bytes32) { return KERNEL_DEFAULT_RELAYER_APP_ID; }
1516
}

‎test/contracts/common/keccak_constants.js

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ contract('Constants', () => {
2727
assert.equal(await kernelConstants.getKernelAppId(), await keccakConstants.KERNEL_APP_ID(), "kernel app id doesn't match")
2828
assert.equal(await kernelConstants.getDefaultACLAppId(), await keccakConstants.DEFAULT_ACL_APP_ID(), "default ACL id doesn't match")
2929
assert.equal(await kernelConstants.getDefaultVaultAppId(), await keccakConstants.DEFAULT_VAULT_APP_ID(), "default vault id doesn't match")
30+
assert.equal(await kernelConstants.getDefaultRelayerAppId(), await keccakConstants.DEFAULT_RELAYER_APP_ID(), "default relayer id doesn't match")
3031
assert.equal(await kernelConstants.getKernelCoreNamespace(), await keccakConstants.KERNEL_CORE_NAMESPACE(), "core namespace doesn't match")
3132
assert.equal(await kernelConstants.getKernelAppBasesNamespace(), await keccakConstants.KERNEL_APP_BASES_NAMESPACE(), "base namespace doesn't match")
3233
assert.equal(await kernelConstants.getKernelAppAddrNamespace(), await keccakConstants.KERNEL_APP_ADDR_NAMESPACE(), "app namespace doesn't match")

‎test/contracts/relayer/relayer.js

+42-25
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const { sha3, soliditySha3 } = require('web3-utils')
2+
const { assertRevert } = require('../../helpers/assertThrow')
23
const { getEventArgument, getNewProxyAddress } = require('../../helpers/events')
34

45
const ACL = artifacts.require('ACL')
@@ -7,10 +8,10 @@ const Relayer = artifacts.require('Relayer')
78
const DAOFactory = artifacts.require('DAOFactory')
89
const SampleApp = artifacts.require('RelayedAppMock')
910

10-
contract('VolatileRelayedApp', ([_, root, sender, vault, offChainRelayerService]) => {
11-
let daoFactory, dao, acl, app, relayer, relayedTx, nonce = 1
11+
contract('RelayedApp', ([_, root, member, anyone, vault, offChainRelayerService]) => {
12+
let daoFactory, dao, acl, app, relayer, relayedTx, nonce = 0
1213
let kernelBase, aclBase, sampleAppBase, relayerBase
13-
let WRITING_ROLE, APP_MANAGER_ROLE, RELAYER_ROLE, ALLOW_OFF_CHAIN_SERVICE_ROLE
14+
let WRITING_ROLE, APP_MANAGER_ROLE, ALLOW_OFF_CHAIN_SERVICE_ROLE, RELAYER_APP_ID
1415

1516
before('deploy base implementations', async () => {
1617
aclBase = await ACL.new()
@@ -20,9 +21,9 @@ contract('VolatileRelayedApp', ([_, root, sender, vault, offChainRelayerService]
2021
daoFactory = await DAOFactory.new(kernelBase.address, aclBase.address, '0x0')
2122
})
2223

23-
before('load roles', async () => {
24+
before('load constants', async () => {
25+
RELAYER_APP_ID = await kernelBase.DEFAULT_RELAYER_APP_ID()
2426
WRITING_ROLE = await sampleAppBase.WRITING_ROLE()
25-
RELAYER_ROLE = await sampleAppBase.RELAYER_ROLE()
2627
APP_MANAGER_ROLE = await kernelBase.APP_MANAGER_ROLE()
2728
ALLOW_OFF_CHAIN_SERVICE_ROLE = await relayerBase.ALLOW_OFF_CHAIN_SERVICE_ROLE()
2829
})
@@ -36,7 +37,7 @@ contract('VolatileRelayedApp', ([_, root, sender, vault, offChainRelayerService]
3637
})
3738

3839
before('create relayer instance', async () => {
39-
const receipt = await dao.newAppInstance('0x11111', relayerBase.address, '0x', false, { from: root })
40+
const receipt = await dao.newAppInstance(RELAYER_APP_ID, relayerBase.address, '0x', true, { from: root })
4041
relayer = Relayer.at(getNewProxyAddress(receipt))
4142
await relayer.initialize()
4243

@@ -51,32 +52,48 @@ contract('VolatileRelayedApp', ([_, root, sender, vault, offChainRelayerService]
5152
app = SampleApp.at(getNewProxyAddress(receipt))
5253
await app.initialize()
5354

54-
await acl.createPermission(sender, app.address, WRITING_ROLE, root, { from: root })
55-
await acl.createPermission(relayer.address, app.address, RELAYER_ROLE, root, { from: root })
55+
await acl.createPermission(member, app.address, WRITING_ROLE, root, { from: root })
5656
})
5757

58-
beforeEach('relay transaction', async () => {
59-
const calldata = app.contract.write.getData(10)
60-
const messageHash = soliditySha3(sha3(calldata), nonce)
61-
const signature = web3.eth.sign(sender, messageHash)
58+
beforeEach('increment nonce', () => nonce++)
6259

63-
relayedTx = await relayer.relay(sender, app.address, nonce, calldata, signature, { from: offChainRelayerService })
64-
nonce++
65-
})
60+
context('when the sender is authorized', () => {
61+
const sender = member
62+
63+
beforeEach('relay transaction', async () => {
64+
const calldata = app.contract.write.getData(10)
65+
const messageHash = soliditySha3(sha3(calldata), nonce)
66+
const signature = web3.eth.sign(sender, messageHash)
67+
68+
relayedTx = await relayer.relay(sender, app.address, nonce, calldata, signature, { from: offChainRelayerService })
69+
})
70+
71+
it('relays transactions to app', async () => {
72+
assert.equal((await app.read()).toString(), 10, 'app value does not match')
73+
})
74+
75+
it('overloads a transaction with ~34k of gas', async () => {
76+
const { receipt: { cumulativeGasUsed: relayedGasUsed } } = relayedTx
77+
const { receipt: { cumulativeGasUsed: nonRelayerGasUsed } } = await app.write(10, { from: sender })
78+
79+
const gasOverload = relayedGasUsed - nonRelayerGasUsed
80+
console.log('relayedGasUsed:', relayedGasUsed)
81+
console.log('nonRelayerGasUsed:', nonRelayerGasUsed)
82+
console.log('gasOverload:', gasOverload)
6683

67-
it('relays transactions to app', async () => {
68-
assert.equal((await app.read()).toString(), 10, 'app value does not match')
84+
assert.isBelow(gasOverload, 34000, 'relayed txs gas overload is higher than 34k')
85+
})
6986
})
7087

71-
it('overloads a transaction with ~78k of gas', async () => {
72-
const { receipt: { cumulativeGasUsed: relayedGasUsed } } = relayedTx
73-
const { receipt: { cumulativeGasUsed: nonRelayerGasUsed } } = await app.write(10, { from: sender })
88+
context('when the sender is not authorized', () => {
89+
const sender = anyone
7490

75-
const gasOverload = relayedGasUsed - nonRelayerGasUsed
76-
console.log('relayedGasUsed:', relayedGasUsed)
77-
console.log('nonRelayerGasUsed:', nonRelayerGasUsed)
78-
console.log('gasOverload:', gasOverload)
91+
it('reverts', async () => {
92+
const calldata = app.contract.write.getData(10)
93+
const messageHash = soliditySha3(sha3(calldata), nonce)
94+
const signature = web3.eth.sign(sender, messageHash)
7995

80-
assert.isBelow(gasOverload, 78000, 'relayed txs gas overload is higher than 78k')
96+
await assertRevert(relayer.relay(sender, app.address, nonce, calldata, signature, { from: offChainRelayerService }), 'APP_AUTH_FAILED')
97+
})
8198
})
8299
})

0 commit comments

Comments
 (0)
Please sign in to comment.