ethers.js/tests/test-solidity-coder.js

689 lines
24 KiB
JavaScript

'use strict';
var Wallet = require('../index.js');
var solc = require('solc');
var ethereumVm = require('ethereumjs-vm');
var ethereumUtil = require('ethereumjs-util');
var BN = Wallet.utils.BN;
var utils = require('./utils.js');
var random = utils.random;
// Create the indent given a tabstop
function indent(tabs) {
var indent = new Buffer(tabs * 4);
indent.fill(32);
return indent.toString('utf8')
}
/**
*
*
*/
function createContractOutput(types, values) {
var source = 'contract Test {\n';
source += ' function test() constant returns (' + types.join(', ') + ') {\n';
var returns = [];
for (var i = 0; i < types.length; i++) {
var name = String.fromCharCode(97 + i);
// Array type; do a deep copy
if (types[i].indexOf('[') >= 0) {
// Each count (or optionally empty) array type
var arrays = types[i].match(/\[[0-9]*\]/g);
// Allocate the space (only dynamic arrays require new)
source += indent(2) + types[i] + ' memory ' + name;
if (arrays[arrays.length - 1] === '[]') {
source += ' = new ' + types[i] + '(' + values[i].length+ ')';
}
source +=';\n';
var baseType = types[i].substring(0, types[i].indexOf('['));
function recursiveSet(item, indices) {
if (Array.isArray(item)) {
item.forEach(function(item, index) {
var i = indices.slice();
i.unshift(index);
recursiveSet(item, i);
});
} else {
var loc = '';
indices.forEach(function(index) {
loc = '[' + index + ']' + loc;
})
if (item instanceof BN) { item = item.toString(10); }
source += indent(2) + name + loc + ' = ' + baseType + '(' + item + ');\n';
}
}
recursiveSet(values[i], []);
// Dynamic type: bytes
} else if (types[i] === 'bytes') {
source += indent(2) + 'bytes memory ' + name + ' = new bytes(' + values[i].length + ');\n';
source += indent(2) + 'assembly {\n'
source += indent(3) + 'mstore(' + name + ', ' + values[i].length + ')\n';
for (var j = 0; j < values[i].length; j++) {
source += indent(3) + 'mstore8(add(' + name + ', ' + (32 + j) + '), ' + values[i][j] + ')\n';
}
source += indent(2) + '}\n'
/*
var value = '';
for (var j = 0; j < values[i].length; j++) {
value += '\\' + 'x' + values[i].slice(j, j + 1).toString('hex');
}
source += ' bytes memory ' + name + ' = "' + value + '";\n';
*/
// Dynamic type: string
} else if (types[i] === 'string') {
source += ' string memory ' + name + ' = "' + values[i] + '";\n';
// Static type; just use the stack
} else {
source += ' ' + types[i] + ' ' + name + ' = ' + types[i] + '(' + values[i] + ');\n';
}
// Track the name to return
returns.push(name);
}
// Return the values
source += ' return (' + returns.join(', ') + ');\n';
source += ' }\n';
source += '}\n';
try {
var contract = solc.compile(source, 0);
contract = contract.contracts.Test;
contract.sourceCode= source;
return contract;
} catch (error) {
console.log('Failed to compile ========');
console.log({types: types, values: values, contract: contract});
console.log(source);
console.log('========');
process.exit();
}
}
/**
* createContract(types)
*
* Creates the source code for a function that takes in the types,
* creates a deep copy of them and returns them and compiles the
* contract.
*/
/* I don't need this anymore, I think?
function createContract(types) {
var signature = [];
for (var i = 0; i < types.length; i++) {
var name = String.fromCharCode(97 + i);
signature.push(types[i] + ' _' + name);
}
var returns = [];
var maxCounter = -1;
// Begin a contract and test function
var source = 'contract Test {\n';
source += ' function test(' + signature.join(', ') + ') constant returns (' + types.join(', ') + ') {\n';
for (var i = 0; i < types.length; i++) {
var name = String.fromCharCode(97 + i);
// Array type; do a deep copy
if (types[i].indexOf('[') >= 0) {
// Each count (or optionally empty) array type
var arrays = types[i].match(/\[[0-9]*\]/g);
// Allocate the space
source += indent(2) + types[i] + ' memory ' + name;
if (arrays[arrays.length - 1] === '[]') {
source += ' = new ' + types[i] + '(_' + name + '.length)';
}
source +=';\n';
// Build the for loops to copy
var current = name;
for (var j = 0; j < arrays.length; j++) {
var counter = 'l' + String.fromCharCode(97 + j);
if (j > maxCounter) {
source += indent(j + 2) + 'uint ' + counter + ';\n';
maxCounter = j;
}
source += indent(j + 2) + 'for (' + counter + ' = 0; ' + counter + ' < _' + current + '.length; ' + counter+ '++) {\n';
current += '[' + counter + ']'; //arrays[j];
}
// Do the copy of an individual value
var loc = name;
for (var j = 0; j < arrays.length; j++) {
loc += '[l' + String.fromCharCode(97 + j) + ']';
}
source += indent(2 + arrays.length) + loc + ' = _' + loc + ';\n';
// Close all the for loops
for (var j = arrays.length - 1; j >= 0; j--) {
source += indent(j + 2) + '}\n';
}
// Dynamic type; new memory
} else if (types[i] === 'bytes' || types[i] === 'string') {
source += ' ' + types[i] + ' memory ' + name + ' = _' + name + ';\n';
// Static type; just use the stack
} else {
source += ' ' + types[i] + ' ' + name + ' = _' + name + ';\n';
}
// Track the name to return
returns.push(name);
}
// Return the copied values
source += ' return (' + returns.join(', ') + ');\n';
// Done the function and contract
source += ' }\n';
source += '}';
var contract = solc.compile(source, 0);
contract = contract.contracts.Test;
contract.sourceCode= source;
return contract;
}
*/
module.exports = function(test) {
//var coderWeb3 = require('./node_modules/web3/lib/solidity/coder.js');
//coderWeb3._name = 'web3';
var coderAbi = require('ethereumjs-abi');
var coderEjs = {
encodeParams: function(types, values) {
return coderAbi.rawEncode(types, values).toString('hex');
},
decodeParams: function(types, data) {
return coderAbi.rawDecode(types, data);
},
_name: 'ejs'
}
function dumpHex(data) {
for (var i = 2; i < data.length; i += 64) {
console.log(' ' + data.substring(i, i + 64));
}
}
function recursiveEqual(a, b) {
function fail() {
return false;
}
if (typeof(a) === 'number') { a = new BN(a); }
if (typeof(b) === 'number') { b = new BN(b); }
if (utils.isHexString(a)) { a = utils.hexOrBuffer(a); }
if (utils.isHexString(b)) { b = utils.hexOrBuffer(b); }
if (a.eq) {
if (!b.eq || !a.eq(b)) { return fail(); }
return true;
}
if (Buffer.isBuffer(a)) {
if (!Buffer.isBuffer(b) || Buffer.compare(a, b) !== 0) { return fail(); }
return true;
}
if (Array.isArray(a)) {
if (!Array.isArray(b) || a.length !== b.length) { return fail(); }
for (var i = 0; i < a.length; i++) {
if (!recursiveEqual(a[i], b[i])) { return fail(); }
}
return true;
}
if (a !== b) { return fail(); }
return true;
}
function checkLib(types, values, coder) {
//console.log(types, values, coder._name);
var officialData = '0x' + coder.encodeParams(types, values);
var ethersData = Wallet._Contract.Interface.encodeParams(types, values);
if (officialData !== ethersData) {
test.ok(false, 'encoded value did not match ' + coder._name);
console.log('coder=' + coder._name);
console.log('types=' + JSON.stringify(types, {depth: null}));
console.log('values=' + JSON.stringify(values, {depth: null}));
console.log('officialData=');
dumpHex(officialData);
console.log('ethersData=');
dumpHex(ethersData);
}
var ethersValues = Wallet._Contract.Interface.decodeParams(types, officialData);
if (!recursiveEqual(values, ethersValues)) {
test.ok(false, 'decoded value did not match ' + coder._name);
console.log('coder=' + coder._name);
console.log('types=' + JSON.stringify(types, {depth: null}));
console.log('values=' + JSON.stringify(values, {depth: null}));
console.log('officialData=');
dumpHex(officialData);
console.log('ethersValues=' + JSON.stringify(ethersValues, {depth: null}));
}
}
var checkPromises = [];
var nextTestId = 0;
var remaining = {};
function check(types, values, normalizedValues) {
if (!normalizedValues) { normalizedValues = values; }
// First make sure we agree with ourself
var ethersData = Wallet._Contract.Interface.encodeParams(types, values);
var ethersValues = Wallet._Contract.Interface.decodeParams(types, ethersData);
var okSelf = recursiveEqual(normalizedValues, ethersValues);
test.ok(okSelf, "self encode/decode failed");
if (!okSelf) {
console.log('okSelf', okSelf, types, values, normalizedValues, ethersValues);
}
checkPromises.push(new Promise(function(resolve, reject) {
var testId = (nextTestId++);
remaining[testId] = true;
// Use this when a contracts "hangs" (ie. 0-length arrays seem to hang the VM)
//console.log('a', testId, types, values);
try {
var contract = createContractOutput(types, values);
var contractInterface = new Wallet._Contract.Interface(JSON.parse(contract.interface));
var call = contractInterface.test.apply(contractInterface);
var vm = new ethereumVm();
vm.runCode({
code: new Buffer(contract.runtimeBytecode, 'hex'),
data: new Buffer(call.data.substring(2), 'hex'),
gasLimit: '0x80000000'
}, function(error, result) {
delete remaining[testId];
// Use this when contract hangs (see the above try)
//console.log('b', testId, Object.keys(remaining).join(','));
try {
var vmData = '0x' + result.return.toString('hex');
test.equal(ethersData, vmData, 'Failed to generate same output as VM');
if (ethersData !== vmData) {
console.log('\n\n');
console.log(contract.sourceCode);
console.log({
types: types,
values: values
});
console.log('ethers=');
dumpHex(ethersData);
console.log('vm=');
dumpHex('0x' + result.return.toString('hex'));
}
resolve();
} catch(error) {
reject(error);
}
});
} catch(error) {
console.log(error);
reject(error);
}
}));
return;
// Second, compile and feed to a solidity contract
checkPromises.push(new Promise(function(resolve, reject) {
var contract = createContract(types);
var contractInterface = new Wallet._Contract.Interface(JSON.parse(contract.interface));
var call = contractInterface.test.apply(contractInterface, values);
var vm = new ethereumVm();
vm.runCode({
code: new Buffer(contract.runtimeBytecode, 'hex'),
data: new Buffer(call.data.substring(2), 'hex'),
gasLimit: '0x80000000'
}, function(error, result) {
var vmData = '0x' + result.return.toString('hex');
var okData = (vmData === ethersData);
test.ok(okData, 'Data did not match');
if (!okData) {
console.dir({
types: types,
contract: contract.sourceCode,
values: values
}, {depth: null});
console.log('vm.data=');
dumpHex(vmData)
console.log('ethers.data=');
dumpHex(ethersData)
}
var vmValues = call.parse(vmData)
var okValues = recursiveEqual(normalizedValues, vmValues);
test.ok(okValues, 'Values did not match');
if (!okValues) {
console.dir({
types: types,
contract: contract.sourceCode,
values: values,
normalized: normalizedValues,
vmValues: vmValues
}, {depth: null});
}
resolve();
});
}));
return;
// And thirdly try ocmparing ourselves against ethereumjs-lib (ignoring
// cases which they currently have open bugs/issues)
var checkTypes = types.join(',');
function has(regex) { return checkTypes.match(regex); }
var hasDynamic = (has(/\[\]/) || has(/bytes([^0-9]|$)/) || has(/string/));
var hasDynamicArray = has(/\[\]/);
var hasFixedArray = has(/\[[0-9]+\]/);
var hasNestedArray = has(/\]\[/);
if (!hasFixedArray && !hasDynamicArray) {
try {
checkLib(types, values, coderEjs);
} catch (error) {
// Bugs in coder
if (error.message === "Cannot read property '1' of null") {
return;
} else if (error.message.match(/^invalid /)) {
return;
}
if (error.message.match(/^Number can only safely store up to/)) {
return;
}
throw error;
}
}
}
// Test cases: https://github.com/ethereum/solidity.js/blob/master/test/coder.decodeParam.js
check(['int'], [new BN(1)]);
check(['int'], [new BN(16)]);
check(['int'], [new BN(-1)]);
check(['int256'], [new BN(1)]);
check(['int256'], [new BN(16)]);
check(['int256'], [new BN(-1)]);
check(['int8'], [new BN(16)]);
check(['int32'], [new BN(16)]);
check(['int64'], [new BN(16)]);
check(['int128'], [new BN(16)]);
check(['uint'], [new BN(1)]);
check(['uint'], [new BN(16)]);
check(['uint'], [new BN(-1)], [new BN('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 16)]);
check(['uint256'], [new BN(1)]);
check(['uint256'], [new BN(16)]);
check(['uint256'], [new BN(-1)], [new BN('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 16)]);
check(['uint8'], [new BN(16)]);
check(['uint32'], [new BN(16)]);
check(['uint64'], [new BN(16)]);
check(['uint128'], [new BN(16)]);
check(['int', 'int'], [new BN(1), new BN(2)]);
check(['int', 'int'], [new BN(1), new BN(2)]);
check(['int[2]', 'int'], [[new BN(12), new BN(22)], new BN(3)]);
check(['int[2]', 'int[]'], [[new BN(32), new BN(42)], [new BN(3), new BN(4), new BN(5)]]);
check(
['bytes32'],
['0x6761766f66796f726b0000000000000000000000000000000000000000000000']
);
check(
['bytes'],
[new Buffer('6761766f66796f726b', 'hex')]
);
check(
['string'],
['\uD835\uDF63']
);
check(
['address', 'string', 'bytes6[4]', 'int'],
[
"0x97916ef549947a3e0d321485a31dd2715a97d455",
"foobar2",
["0xa165ab0173c6", "0xf0f37bee9244", "0xc8dc0bf08d2b", "0xc8dc0bf08d2b"],
34
]
);
check(
['bytes32'],
['0x731a3afc00d1b1e3461b955e53fc866dcf303b3eb9f4c16f89e388930f48134b']
);
check(
['bytes'],
[new Buffer('731a3afc00d1b1e3461b955e53fc866dcf303b3eb9f4c16f89e388930f48134b')]
);
check(
['bytes32[2]'],
[['0x731a3afc00d1b1e3461b955e53fc866dcf303b3eb9f4c16f89e388930f48134b',
'0x731a3afc00d1b1e3461b955e53fc866dcf303b3eb9f4c16f89e388930f48134b']]
);
check(
['bytes'],
[new Buffer('131a3afc00d1b1e3461b955e53fc866dcf303b3eb9f4c16f89e388930f48134b' +
'231a3afc00d1b1e3461b955e53fc866dcf303b3eb9f4c16f89e388930f48134b' +
'331a3afc00d1b1e3461b955e53fc866dcf303b3eb9f4c16f89e388930f48134b')]
);
// Some extra checks for width and sign tests
check(['uint32'], [14], [new BN(14)]);
check(['uint32'], [14], [new BN(14)]);
check(['uint32'], [-14], [new BN(0xfffffff2)]);
check(['int32'], [14], [new BN(14)]);
check(['int32'], [-14], [new BN(-14)]);
check(['int8'], [new BN(1)], [new BN(1)]);
check(['int8'], [new BN(-1)], [new BN(-1)]);
check(['int8'], [new BN(189)], [new BN(-67)]);
check(['int8'], [new BN(-189)], [new BN(67)]);
check(['int8'], [new BN(257)], [new BN(1)]);
check(['uint8'], [new BN(343)], [new BN(87)]);
check(['uint8'], [new BN(-1)], [new BN(255)]);
check(['uint56[5]'], [[new BN(639), new BN(227), new BN(727), new BN(325), new BN(146)]]);
function randomTypeValue(onlyStatic) {
switch (random(0, (onlyStatic ? 5: 8))) {
case 0:
var size = random(1, 33);
return {
type: 'bytes' + size,
value: function() {
var value = '0x' + utils.randomBuffer(size).toString('hex');
return {
value: value,
normalized: value
}
}
}
case 1:
var signed = (random(0, 2) === 0);
var type = (!signed ? 'u': '') + 'int';
var size = 32;
if (random(0, 4) > 0) {
size = random(1, 33)
type += (8 * size);
}
return {
type: type,
value: function() {
var mask = '';
for (var i = 0; i < size; i++) { mask += 'ff'; }
var value = random(-500, 1000);
var normalized = (new BN(value)).toTwos(size * 8).and(new BN(mask, 16));
if (signed) {
normalized = normalized.fromTwos(size * 8);
}
/*
if (!signed && value < 0) {
normalized = normalized.toTwos(size * 8);
}
*/
return {
value: value,
normalized: normalized
};
}
}
case 2:
return {
type: 'address',
value: function() {
var value = '0x' + utils.randomBuffer(20).toString('hex');
return {
value: value,
normalized: value
};
}
}
case 3:
return {
type: 'bool',
value: function() {
var value = (random(0, 2) === 0);
return {
value: value,
normalized: value
};
}
}
case 4:
var size = random(1, 6); /// @TODO: Support random(0, 6)... Why is that even possible?
var subTypeValue = randomTypeValue(true);
return {
type: subTypeValue.type + '[' + size + ']',
value: function() {
var values = [];
var normalized = [];
for (var i = 0; i < size; i++) {
var value = subTypeValue.value();
values.push(value.value);
normalized.push(value.normalized);
}
return {
value: values,
normalized: normalized
};
}
}
case 5:
return {
type: 'bytes',
value: function() {
var value = utils.randomBuffer(random(0, 100));
return {
value: value,
normalized: value
};
},
skip: 0
}
case 6:
var text = 'abcdefghijklmnopqrstuvwxyz\u2014ABCDEFGHIJKLMNOPQRSTUVWXYZFOOBARfoobar'
return {
type: 'string',
value: function() {
var value = text.substring(0, random(0, 60));
return {
value: value,
normalized: value
};
}
}
case 7:
var size = random(1, 6); // @TODO: bug in solidity or VM prevents this from being 0
var subTypeValue = randomTypeValue(true);
return {
type: subTypeValue.type + '[]',
value: function() {
var values = [];
var normalized = [];
for (var i = 0; i < size; i++) {
var value = subTypeValue.value();
values.push(value.value);
normalized.push(value.normalized);
}
return {
value: values,
normalized: normalized
};
}
}
}
}
// @TODO: Test 0 arguments
// Create a bunch of random test cases
for (var i = 0; i < 1000; i++) {
var count = random(1, 4);
var types = [], values = [], normalized = [];;
for (var j = 0; j < count; j++) {
var type = randomTypeValue();
types.push(type.type);
var value = type.value();
values.push(value.value);
normalized.push(value.normalized);
}
check(types, values, normalized);
}
/*
check([ 'bool[4]', 'bool[]', 'int8' ],
[ [ true, true, false, true ], [ true, true ], -210 ] [ [ true, true, false, true ], [ true, true ], new BN(-0xd2) ],
[ [ true, true, false, true ], [ true, true ], new BN(2e) ]
);
*/
// Bug in solidity or in the VM, not sure, but this fails
// check(['uint8[4][]'], [ [] ]);
Promise.all(checkPromises).then(function(results) {
test.done();
}, function(error) {
console.log('ERROR', error);
test.done();
});
}