fuel-core
Version:
Essential javascript architecture for the Fuel side chain
1,595 lines (1,378 loc) • 87.2 kB
JavaScript
// Core Modules
const ganache = require("ganache-core"); // genache is a dumb name..
const { utils, ContractFactory, Contract, providers } = require('ethers');
const ethers = require('ethers');
const EthRPC = require('ethjs-rpc');
const HttpProvider = require('ethjs-provider-http');
const bytecode = require('../contracts/Fuel.code.js');
const utility = require('../contracts/FuelUtility.code.js');
const _utils = require('../utils/utils');
const { test } = require('zora');
const MemoryDB = require('../dbs/MemoryDB');
const Wallet = require('../wallet/wallet');
const post = require('../wallet/post');
const interfaces = require('../interfaces/interfaces');
// Fuel Related Methods
const {
// Interfaces
FuelInterface,
FuelUtilityInterface,
Fuel,
// Low Level
big,
ethereumBlockNumber,
// Structs / Proofs
GenesisBlock,
BlockTransactions,
TransactionRootHeader,
BlockFraudProof,
BlockHeader,
MalformedBlockProof,
BondWithdrawalProof,
DepositProof,
EmptyProof,
UTXOProof,
ByPassError,
FillProof,
TransactionUnsigned,
TransactionMetadata,
TransactionInputUTXO, TransactionInputHTLC, TransactionInputChange, TransactionInputDeposit,
TransactionOutputUTXO, TransactionOutputHTLC, TransactionOutputChange, TransactionOutputWithdrawal,
TransactionLeaf,
TransactionWitness,
EmptyTransactionLeaf,
MalformedTransactionProof,
TransactionProof,
TransactionMerkleProof,
TransactionData,
OverflowingTransactionMerkleProof,
TransactionProofs,
InvalidTransactionProof,
InvalidTransactionInputProof,
InvalidTransactionDoubleSpendProof,
UserWithdrawalProof,
WithdrawalProof,
SummingProof,
FuelInputTypes,
FuelDBKeys,
mysqlIntakeTransaction,
// Construction
constructDepositHashID,
constructWitness,
emptyAddress,
emptyBytes32,
// Types
TypeHex,
TypeAddress,
TypeNumber,
TypeDefined,
TypeArray,
FuelConstants,
TypeObject,
TypeBigNumber,
TypeString,
TypeFunction,
TypeDB,
FuelOutputTypes,
} = require('../lib');
// Generate Deterministic Test Accounts
const accounts = [
new utils.SigningKey('0xf72e1c49ef6662accff15221f2581defd6522248e0aad76b4391bf1140a9dd36'),
new utils.SigningKey('0x6ffd1ce2688b802196b6f8aac601c1e00cd94fd8aab8d64a25c13d3a115e5652'),
new utils.SigningKey('0xa2f349d333d8097ffaa0109847dbde83c08e01cbbd3758ad4eb9a194b56d475d'),
new utils.SigningKey('0x1c9bbaad098c769b09c81917430e819a917e6d521fbdc2e54adf128d745c2327'),
new utils.SigningKey('0xdccea3f8285d22bc27f01e6e67f89960560ac58998996af92a80f852d7b6311c'),
];
// Main acocunt
const address = accounts[0].address;
const providerServer = process.env.providerServer;
if (providerServer) {
console.log('Using provider server');
}
const providerConfig = {
accounts: accounts.map(account => ({
balance: '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
secretKey: account.privateKey,
})),
allowUnlimitedContractSize: true,
vmErrorsOnRPCResponse: true,
gasLimit: '0x1fffffffffffff',
debug: false,
logger: { log: () => {} },
callGasLimit: '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
};
// Provider
const provider = providerServer
? new HttpProvider('http://localhost:3000')
: ganache.provider(providerConfig);
// Wallets
const wallets = [
new ethers.Wallet(accounts[0].privateKey, new providers.Web3Provider(provider)),
new ethers.Wallet(accounts[1].privateKey, new providers.Web3Provider(provider)),
new ethers.Wallet(accounts[2].privateKey, new providers.Web3Provider(provider)),
new ethers.Wallet(accounts[3].privateKey, new providers.Web3Provider(provider)),
new ethers.Wallet(accounts[4].privateKey, new providers.Web3Provider(provider)),
];
// Load the wallet to deploy the contract with
const wallet = new ethers.Wallet(accounts[0].privateKey, new providers.Web3Provider(provider));
// Build raw object
const eth = new EthRPC(provider);
// RPC call
const rpc = (method, ...args) => new Promise((resolve, reject) => eth
.sendAsync({ method, params: args }, (err, result) => {
if (err) return reject(err);
if (result) return resolve(result);
}));
// A send transaction method for testing
const sendTransaction = async params => {
try {
// check params type
TypeObject(params);
// data parsig
let data = params.data || '0x00';
const value = params.value || big('0x00');
// check types
if (typeof params.from !== 'number') {
TypeHex(params.from, 20);
}
TypeHex(data);
TypeBigNumber(value);
// parse params and get nonce and gas price
const from = typeof params.from === 'number' ? (await rpc('eth_accounts'))[params.from] : params.from;
const to = typeof params.to === 'number' ? (await rpc('eth_accounts'))[params.from] : params.to;
const nonce = (await rpc('eth_getTransactionCount', from));
const block = (await rpc('eth_getBlockByNumber', 'latest', false));
const gas = params.gas || big(block.gasLimit);
// method for encoding / decoding
let method = null;
// check gas type
TypeBigNumber(gas);
// Handle custom solidity work
if (typeof params.solidity !== 'undefined') {
TypeString(params.solidity);
const solidity = new utils.Interface([params.solidity]);
const getMethodName = Object.keys(solidity.functions)[0];
method = solidity.functions[getMethodName];
// Enforce type array
TypeArray(params.params);
// handle constructor
if (solidity.abi[0].type === 'constructor') {
data = solidity.deployFunction.encode(params.data, params.params);
} else {
// handle data
data = method.encode(params.params);
}
}
// send transaction
const transactionHash = await rpc('eth_sendTransaction', {
nonce,
from,
gas: gas.toHexString(),
to,
value: value.toHexString(),
data,
});
// get receipt
const receipt = await rpc('eth_getTransactionReceipt', transactionHash);
// return the receipt
return receipt;
} catch (error) {
throw new ByPassError(error);
}
};
// Deploy Fuel Contract
const constructFuel = async (producerAddress, accountIndex, params) => {
try {
// Type Enforcement
TypeAddress(producerAddress);
if (typeof accountIndex !== 'undefined') { TypeNumber(accountIndex); }
if (typeof params !== 'undefined') { TypeObject(accountIndex); }
// Deployment Transaction
const receipt = await sendTransaction(Object.assign(params || {}, {
from: (params || {}).from || accountIndex || 0,
data: bytecode,
solidity: 'constructor(address) public',
params: [producerAddress],
}));
// We connect to the Contract using a Provider, so we will only
// have read-only access to the Contract
const contract = new Contract(receipt.contractAddress, FuelInterface.abi, new providers.Web3Provider(provider));
// Create a new instance of the Contract with a Signer, which allows
return {
contract: contract.connect(wallets[accountIndex || 0]),
blockNumber: big(receipt.blockNumber),
receipt
};
} catch (error) {
throw new ByPassError(error);
}
};
// Deploy Fuel Contract
const constructUtility = async (totalSupply, accountIndex, params) => {
try {
// Enforce Types
TypeBigNumber(totalSupply);
// Weak Type Enforcement
if (typeof accountIndex !== 'undefined') { TypeNumber(accountIndex); }
if (typeof params !== 'undefined') { TypeObject(accountIndex); }
// Deployment Transaction
const receipt = await sendTransaction(Object.assign(params || {}, {
from: (params || {}).from || accountIndex || 0,
data: utility,
solidity: 'constructor(uint256) public',
params: [totalSupply],
}));
// We connect to the Contract using a Provider, so we will only
const contract = new Contract(receipt.contractAddress, FuelUtilityInterface.abi,
new providers.Web3Provider(provider));
// Create a new instance of the Contract with a Signer, which allows
return contract.connect(wallets[accountIndex || 0]);
} catch (error) {
throw new ByPassError(error);
}
};
// Fuel instance
function atUtility(address, accountIndex) {
// Type Enforcement
TypeAddress(address);
// Weak Enforcement
if (typeof accountIndex !== 'undefined') { TypeNumber(accountIndex); }
// We connect to the Contract using a Provider, so we will only
const contract = new Contract(address, FuelUtilityInterface.abi, new providers.Web3Provider(provider));
// Create a new instance of the Contract with a Signer, which allows
return contract.connect(wallets[accountIndex || 0]);
}
// Fuel instance
function atFuel(address, accountIndex) {
TypeAddress(address);
if (typeof accountIndex !== 'undefined') { TypeNumber(accountIndex); }
// We connect to the Contract using a Provider, so we will only
// have read-only access to the Contract
const contract = new Contract(address, FuelInterface.abi, new providers.Web3Provider(provider));
// Create a new instance of the Contract with a Signer, which allows
return contract.connect(wallets[accountIndex || 0]);
}
// Extend zora test methods
function extendTest(t) {
const gt = (v1, v2, message) => {
// Enfore defined
TypeDefined(v1);
TypeDefined(v2);
const pass = big(v1).gt(big(v2));
if (pass) {
t.ok(pass, (message || ''))
} else {
t.fail((message || '') + ` | ${v1} should be GREATER THAN ${v2}`);
}
};
const lt = (v1, v2, message) => {
// Enfore defined
TypeDefined(v1);
TypeDefined(v2);
const pass = big(v1).lt(big(v2));
if (pass) {
t.ok(pass, (message || ''))
} else {
t.fail((message || '') + ` | ${v1} should be LESS THAN ${v2}`);
}
};
const gte = (v1, v2, message) => {
// Enfore defined
TypeDefined(v1);
TypeDefined(v2);
const pass = big(v1).gte(big(v2));
if (pass) {
t.ok(pass, (message || ''))
} else {
t.fail((message || '') + ` | ${v1} should be GREATER THAN OR EQUAL ${v2}`);
}
};
const lte = (v1, v2, message) => {
// Enfore defined
TypeDefined(v1);
TypeDefined(v2);
const pass = big(v1).lte(big(v2));
if (pass) {
t.ok(pass, (message || ''))
} else {
t.fail((message || '') + ` | ${v1} should be LESS THAN OR EQUAL ${v2}`);
}
};
const eq = (v1, v2, message) => {
// Enfore defined
TypeDefined(v1);
TypeDefined(v2);
const pass = big(v1).eq(big(v2));
if (pass) {
t.ok(pass, (message || ''))
} else {
t.fail((message || '') + ` | ${v1} should be EQUAL ${v2}`);
}
};
const throws = async (promiseObject, message) => {
// Enfore defined
TypeDefined(promiseObject);
try {
await promiseObject();
t.fail((message || '') + ` | ${v1} should have Thrown!`);
} catch (error) {
t.ok(1, (message || ''));
}
};
const throwsFraud = async (promiseObject, fraudName, message) => {
// Enfore defined
TypeDefined(promiseObject);
try {
await promiseObject();
t.fail((message || '') + ` | ${fraudName} should have Thrown!`);
} catch (fraudError) {
if (!fraudError.fraudCode) return t.fail('Invalid fuel code error');
t.eq(fraudError.fraudCode, FuelConstants[fraudName],
(message + `${fraudError.fraudCode} should equal ${FuelConstants[fraudName]}` || ''));
}
};
const reverts = async (promiseObject, code, message, logLogs = false) => {
// Enfore defined
TypeDefined(promiseObject);
let obj = null;
try {
obj = await promiseObject();
eq((await getReceipt(obj.hash)).status, 0, (message || '') + ` | ${promiseObject} should have reverted!`);
if (logLogs) {
console.log((await getReceipt(obj.hash)).logs);
}
} catch (error) {
if (typeof error.hashes === 'undefined') {
if (logLogs && obj) {
getReceipt(obj.hash).then(receipt => console.log(receipt.log)).catch(console.log);
}
t.fail((message || '') + ` | ${promiseObject} should have reverted!`);
return;
}
const result = error.results[error.hashes[0]];
const errorCode = big(result.return);
eq(errorCode, code, (message || ''));
}
};
// Basic gte
return {
gt,
lt,
gte,
lte,
eq,
throws,
throwsFraud,
reverts,
};
}
// Fuel Params
const params = {
rpc,
};
// One ether
const oneEther = utils.parseEther('1');
const oneDay = 86400;
const loadsOfGas = big('10000000');
// Unixtime
function unixtime() {
return Math.round((new Date).getTime() / 1000);
}
// Increase Block
async function increaseBlocks(amount) {
try {
// Enforce Types
TypeNumber(amount);
// EVM Mine
for (var i = 0; i < (amount || 1); i++) {
await rpc('evm_mine', unixtime() + 13);
}
} catch (error) {
throw new ByPassError(error);
}
}
// simple get receipt
function getReceipt(transactionHash) {
TypeHex(transactionHash, 32);
// get receipt
return rpc('eth_getTransactionReceipt', transactionHash);
}
// simple get receipt
function statusSuccess(receipt) {
TypeObject(receipt);
// get receipt
return big(receipt.status);
}
// Create a Single Fuel contract, with a Single Block (given leafs)
async function singleBlockSingleProofFuel(transactionLeafs, merkilize, eq, shouldPass = false) {
try {
// Enforce Types
TypeArray(transactionLeafs);
TypeFunction(eq);
// Construct Fuel Contract
const { contract } = await constructFuel(address);
// Genesis Block
const genesis = new GenesisBlock();
//
// Single Block Empty Transaction Root
//
// Transaction Leafs Encoded
const transactions = new BlockTransactions(transactionLeafs);
const merkleRoot = merkilize ? transactions.merkleTreeRoot() : emptyBytes32;
// Submit Transactions
const submit = await contract.submitTransactions(merkleRoot, transactions.encoded, {
gasLimit: loadsOfGas,
});
const submitBlock = big((await getReceipt(submit.hash)).blockNumber);
// Transaction Root Header
const root = new TransactionRootHeader({
producer: address, // - transactionRootCommitter [32 bytes] -- padded address
merkleTreeRoot: merkleRoot, // - transactionRootMerkleTreeRoot [32 bytes] -- padded bytes32
commitmentHash: transactions.hash, // - transactionRootCommitmentHash [32 bytes]
index: big(0), // - transactionRootIndex [32 bytes] -- padded uint16
});
// Block tx roots
eq(await contract.blockTransactionRoots(root.hash), submitBlock, 'root submitted');
// Commit Block
const commit = await contract.commitBlock(1, [root.hash], {
value: await contract.BOND_SIZE(),
gasLimit: loadsOfGas,
});
const commitReceipt = await getReceipt(commit.hash);
// Commit status
eq(commitReceipt.status, 1, 'commit status');
// Block
const block = new BlockHeader({
producer: address, // - blockCommitter [32 bytes] -- padded address
previousBlockHash: genesis.hash, // - previousBlockHash [32 bytes]
blockHeight: big(1), //- blockHeight [32 bytes]
ethereumBlockNumber: big(commitReceipt.blockNumber), // - ethereumBlockNumber [32 bytes]
transactionRoots: [root.hash], // - transactionRoots [64 + dynamic bytes]
});
// Proof
const proof = new MalformedBlockProof(new BlockFraudProof({
block: block,
root: root,
transactions: transactions,
}));
// Block tip adjusted.
eq(await contract.blockTip(), 1, 'tip check');
// Submit Proof
const submitProof = await contract.submitProof(proof.encoded, {
gasLimit: loadsOfGas,
});
// Block tip adjusted.
if (!shouldPass) {
eq(await contract.blockTip(), 0, 'invalid block detected, tip set back');
}
const receipt = await getReceipt(submitProof.hash);
// Return Profile
return {
contract,
block,
root,
transactions,
proof,
receipt,
};
} catch (error) {
throw new ByPassError(error);
}
}
// Random Int
const randomInt = (min = 1, max = 200) => {
TypeNumber(min);
TypeNumber(max);
return Math.floor(Math.random() * (+max - +min)) + +min;
};
// Create a Single Fuel contract, with a Single Block (given leafs)
// Creates a completely valid proof scenario
async function submitMalformedTransactionProof(params) {
try {
const {
merkleProofOverride,
transactionDataOverride,
randomLengthLeafs,
errorCode,
fraudCode,
witnesses,
inputs,
outputs,
metadata,
proofType,
inputIndex,
outputIndex,
token,
witnessIndex,
witnessReference,
summingToken,
includeSecondWitness,
withdrawalAccount,
submitProofTwice,
leafs,
merkleProofOverflow,
proofsOverride,
otherCaller,
otherRootProducer,
includeProofs,
eq,
reverts,
validMerkleRoot,
summingTokenIsERC20,
secondIsRightmost,
secondInputIndex,
secondOutputIndex,
secondWitnessIndex,
secondValidProof,
secondaryMetadata,
secondaryInputs,
secondTransactionIsZero,
constructInputAsSecondChange,
constructInputAsSecondUTXO,
constructInvalidHTLCWitness,
constructInvalidChangeWitness,
leafCount,
blockIncrease,
erc20Enabled,
logReceipts,
produceSecondTransactionRoot,
secondInputIsDeposit,
message,
constructExpiredOutput,
constructInputAsSecondHTLC,
shouldPass,
} = Object.assign({
merkleProofOverride: {},
transactionDataOverride: {},
randomLengthLeafs: false,
errorCode: null,
secondaryMetadata: null,
secondaryInputs: null,
fraudCode: null,
witnesses: null,
metadata: null,
proofType: 'malformedTransaction',
token: null,
inputIndex: null,
outputIndex: null,
witnessIndex: null,
otherRootProducer: false,
secondTransactionIsZero: false,
witnessReference: null,
summingToken: emptyAddress, // Ether
includeSecondWitness: false,
withdrawalAccount: null,
summingTokenIsERC20: false,
submitProofTwice: false,
constructInputAsSecondHTLC: false,
constructInvalidHTLCWitness: false,
secondIsRightmost: false,
secondValidProof: false,
secondInputIndex: null,
constructExpiredOutput: false,
secondOutputIndex: null,
constructInvalidChangeWitness: false,
secondWitnessIndex: null,
constructInputAsSecondChange: false,
erc20Enabled: false,
otherCaller: false,
includeProofs: false,
inputs: null,
outputs: null,
produceSecondTransactionRoot: false,
leafs: null,
merkleProofOverflow: false,
secondInputIsDeposit: false,
proofsOverride: null,
constructInputAsSecondUTXO: false,
validMerkleRoot: true,
logReceipts: false,
leafCount: 1,
blockIncrease: big(0),
message: '',
shouldPass: false,
}, params || {});
// Enforce Types
if (errorCode !== null) { TypeNumber(errorCode); }
if (fraudCode !== null) { TypeNumber(fraudCode); }
if (merkleProofOverride !== null) { TypeObject(merkleProofOverride); }
if (transactionDataOverride !== null) { TypeObject(transactionDataOverride); }
TypeFunction(eq);
TypeFunction(reverts);
TypeString(message);
TypeBigNumber(blockIncrease);
// Construct Fuel Contract
const { contract } = await constructFuel(address);
// Genesis Block
const genesis = new GenesisBlock();
// Second Proof for Comparison proofs
let secondProof = null;
let secondBlockHeader = null;
let inputsOverride = inputs;
// Token
let erc20Token = null;
// Deploy second token
if (erc20Enabled) {
// Construct Token
erc20Token = await constructUtility(big(1000), 0);
// Mint user tokens
await erc20Token.mint(address, 500);
// Mint user tokens
await erc20Token.approve(contract.address, 30);
// Make Deposit and Register Token
await contract.deposit(address, erc20Token.address, big(30), {
gasLimit: loadsOfGas,
});
}
// Construct Deposit
const depositAmount = big(30);
const deposit = await contract.deposit(address, emptyAddress, depositAmount, {
gasLimit: loadsOfGas,
value: depositAmount,
});
const depositReceipt = await getReceipt(deposit.hash);
const depositProof = new DepositProof({
account: address,
token: emptyAddress,
ethereumBlockNumber: big(depositReceipt.blockNumber),
});
// Block tx roots
eq(depositReceipt.status, 1, 'deposit status good');
// Create a second valid proof before main proof for Comparison
if (secondValidProof) {
// Different Owners, Tokens, Amounts, Types as Outputs for Reference
const secondUnsignedTransaction = new TransactionUnsigned({
inputs: secondInputIsDeposit ? [
new TransactionInputDeposit({
depositHashID: constructDepositHashID({
account: address,
token: emptyAddress, // Ether address
ethereumBlockNumber: big(depositReceipt.blockNumber),
}),
witnessReference: witnessReference ? witnessReference : 0,
}),
] : (secondaryInputs ? secondaryInputs: [
new TransactionInputUTXO({ utxoID: emptyBytes32, witnessReference: 0 }),
]),
outputs: [
new TransactionOutputUTXO({ amount: big(5), tokenID: big(0), owner: address }),
new TransactionOutputUTXO({ amount: big(5), tokenID: big(0), owner: accounts[1].address }),
new TransactionOutputChange({ amount: big(10), tokenID: big(1),
ownerAsWitnessIndex: constructInvalidChangeWitness ? 1 : 0 }),
new TransactionOutputWithdrawal({ amount: big(5), tokenID: big(0), owner: address }),
new TransactionOutputHTLC({
amount: big(5),
tokenID: big(0),
owner: address,
digest: utils.hexZeroPad('0x01', 32), // valid digest
expiry: constructExpiredOutput ? big(3) : big(32),
returnWitnessIndex: constructInvalidHTLCWitness ? 1 : 0,
}),
],
});
if (constructInputAsSecondHTLC) {
const utxoProof = new UTXOProof({
transactionHashId: secondUnsignedTransaction.hash,
outputIndex: 4,
type: FuelInputTypes.HTLC,
amount: big(5),
owner: address,
tokenID: big(0),
digest: utils.hexZeroPad('0x01', 32), // valid digest
expiry: constructExpiredOutput ? big(3) : big(32),
returnWitness: constructInvalidHTLCWitness ? 1 : 0,
preImage: emptyBytes32,
});
inputsOverride = [
new TransactionInputHTLC({
utxoID: utxoProof.hash,
preImage: emptyBytes32,
witnessReference: 0,
}),
];
}
if (constructInputAsSecondUTXO) {
const utxoProof = new UTXOProof({
transactionHashId: secondUnsignedTransaction.hash,
outputIndex: 1,
type: FuelInputTypes.UTXO,
amount: big(5),
owner: accounts[1].address,
tokenID: big(0),
});
inputsOverride = [
new TransactionInputUTXO({
utxoID: utxoProof.hash,
witnessReference: 1,
}),
];
}
if (constructInputAsSecondChange) {
const utxoProof = new UTXOProof({
transactionHashId: secondUnsignedTransaction.hash,
outputIndex: 2,
type: FuelInputTypes.Change,
amount: big(10),
owner: constructInvalidChangeWitness ? 1 : 0,
tokenID: big(1),
});
inputsOverride = [
new TransactionInputChange({
utxoID: utxoProof.hash,
witnessReference: 0,
}),
];
}
const secondTransactions = new BlockTransactions([
secondTransactionIsZero ? (new EmptyTransactionLeaf(0)) : (new TransactionLeaf({
metadata: secondaryMetadata ? secondaryMetadata : [
new TransactionMetadata({
blockHeight: big(1),
transactionRootIndex: 0,
transactionIndex: 0,
outputIndex: 0,
}),
],
witnesses: [
new TransactionWitness(constructWitness(secondUnsignedTransaction, accounts[0])),
new TransactionWitness(constructWitness(secondUnsignedTransaction, accounts[1])),
],
unsignedTransaction: secondUnsignedTransaction,
})),
]);
const secondTxIndex = secondIsRightmost ? secondTransactions.rightMostIndex() : 0;
// Setup second root
const secondRoot = new TransactionRootHeader({
producer: address, // - transactionRootCommitter [32 bytes] -- padded address
merkleTreeRoot: secondTransactions.merkleTreeRoot(), // - transactionRootMerkleTreeRoot [32 bytes] -- padded bytes32
commitmentHash: secondTransactions.hash, // - transactionRootCommitmentHash [32 bytes]
index: big(0), // - transactionRootIndex [32 bytes] -- padded uint16
});
// Fake Txs
const fakeTransactions = new BlockTransactions([new EmptyTransactionLeaf(130)]);
const fakeRoot = new TransactionRootHeader({
producer: address, // - transactionRootCommitter [32 bytes] -- padded address
merkleTreeRoot: emptyBytes32, // - transactionRootMerkleTreeRoot [32 bytes] -- padded bytes32
commitmentHash: fakeTransactions.hash, // - transactionRootCommitmentHash [32 bytes]
index: big(0), // - transactionRootIndex [32 bytes] -- padded uint16
});
// Submit Fake block for height / tx reference checks
const secondSubmit = await contract.submitTransactions(emptyBytes32, fakeTransactions.encoded, {
gasLimit: loadsOfGas,
});
const secondCommitReceiptFake = await contract.commitBlock(1, [fakeRoot.hash], {
value: big(await contract.BOND_SIZE()),
gasLimit: loadsOfGas,
});
const fakeBlockHeader = new BlockHeader({
producer: address, // - blockCommitter [32 bytes] -- padded address
previousBlockHash: genesis.hash, // - previousBlockHash [32 bytes]
blockHeight: await contract.blockTip(), //- blockHeight [32 bytes]
ethereumBlockNumber: big((await getReceipt(secondCommitReceiptFake.hash)).blockNumber), // - ethereumBlockNumber [32 bytes]
transactionRoots: [fakeRoot.hash], // - transactionRoots [64 + dynamic bytes]
});
// Submit txs and blocks
await contract.submitTransactions(secondRoot.merkleTreeRoot, secondTransactions.encoded, {
gasLimit: loadsOfGas,
});
const secondCommit = await contract.commitBlock(2, produceSecondTransactionRoot ?
[secondRoot.hash, secondRoot.hash] : [secondRoot.hash], {
value: await contract.BOND_SIZE(),
gasLimit: loadsOfGas,
});
const secondCommitReceipt = await getReceipt(secondCommit.hash);
// Produce Second Block Header
secondBlockHeader = new BlockHeader({
producer: address, // - blockCommitter [32 bytes] -- padded address
previousBlockHash: fakeBlockHeader.hash, // - previousBlockHash [32 bytes]
blockHeight: await contract.blockTip(), //- blockHeight [32 bytes]
ethereumBlockNumber: big(secondCommitReceipt.blockNumber), // - ethereumBlockNumber [32 bytes]
transactionRoots: produceSecondTransactionRoot ?
[secondRoot.hash, secondRoot.hash] : [secondRoot.hash], // - transactionRoots [64 + dynamic bytes]
});
// Setup Second Proof
secondProof = new TransactionProof({
block: secondBlockHeader,
root: secondRoot,
merkle: secondTransactions.merkleProof(secondTxIndex),
transaction: new TransactionData({
inputIndex: secondInputIndex ? secondInputIndex : 0,
outputIndex: secondOutputIndex ? secondOutputIndex : 0,
witnessIndex: secondWitnessIndex ? secondWitnessIndex : 0,
transactionIndex: secondTxIndex,
transactionLeaf: secondTransactions.get(secondTxIndex),
}),
proofs: 0,
});
}
// Unsigned Transaction
const unsignedTransaction = new TransactionUnsigned({
inputs: inputsOverride ? inputsOverride : [ new TransactionInputDeposit({
depositHashID: constructDepositHashID({
account: address,
token: emptyAddress, // Ether address
ethereumBlockNumber: big(depositReceipt.blockNumber),
}),
witnessReference: witnessReference ? witnessReference : 0,
})],
outputs: outputs ? outputs : [ new TransactionOutputUTXO({ amount: depositAmount, tokenID: big(0), owner: address }) ]
});
// Witness
let defaultWitnesses = [
new TransactionWitness(constructWitness(unsignedTransaction, accounts[0])),
];
// Includes a second witness signer
if (includeSecondWitness) {
defaultWitnesses = defaultWitnesses.concat([
new TransactionWitness(constructWitness(unsignedTransaction, accounts[1])),
]);
};
// Valid transaction leaf (Ether transfer)
const transactionLeaf = new TransactionLeaf({
metadata: metadata ? metadata : [],
witnesses: witnesses ? witnesses : defaultWitnesses,
unsignedTransaction,
});
// Random leafs
const randomLeafCount = randomInt(1, 120);
const randomLeafs = randomLengthLeafs ? (new Array(randomLeafCount)).fill(0)
.map(v => new EmptyTransactionLeaf(randomInt(101, 799))) : null;
// Check random leafs
if (randomLengthLeafs) {
eq(randomLeafs.length > 0 ? 1 : 0, 1, `${randomLeafs.length} random leafs with different lengths generated`);
}
// valid leaf generation
const transactionLeafs = leafs ? leafs : [transactionLeaf];
const transactionIndex = 0;
//
// Single Block Empty Transaction Root
//
// Transaction Leafs Encoded
const transactions = new BlockTransactions(transactionLeafs);
const transactionsProof = new BlockTransactions(transactionLeafs);
const merkleRoot = validMerkleRoot ? transactions.merkleTreeRoot() : emptyBytes32;
// submit
let submit = null;
// use other producer
if (otherRootProducer) {
// setup an instance with other account 1
const otherContract = atFuel(contract.address, 1);
// submit transactions
submit = await otherContract.submitTransactions(merkleRoot, transactions.encoded, {
gasLimit: loadsOfGas,
});
} else {
// submit transactions
submit = await contract.submitTransactions(merkleRoot, transactions.encoded, {
gasLimit: loadsOfGas,
});
}
// Submit Transactions
const submitBlock = big((await getReceipt(submit.hash)).blockNumber);
// Transaction Root Header
const root = new TransactionRootHeader({
producer: otherRootProducer ? accounts[1].address : address, // - transactionRootCommitter [32 bytes] -- padded address
merkleTreeRoot: merkleRoot, // - transactionRootMerkleTreeRoot [32 bytes] -- padded bytes32
commitmentHash: transactions.hash, // - transactionRootCommitmentHash [32 bytes]
index: big(0), // - transactionRootIndex [32 bytes] -- padded uint16
});
// Block tx roots
eq(await contract.blockTransactionRoots(root.hash), submitBlock, 'root submitted');
// Commit Block
const commit = await contract.commitBlock(secondValidProof ? 3 : 1, [root.hash], {
value: await contract.BOND_SIZE(),
gasLimit: loadsOfGas,
});
const commitReceipt = await getReceipt(commit.hash);
// Commit status
eq(commitReceipt.status, 1, 'commit status');
// Overflowing Merkle Proof
const overflowMerkleProof = merkleProofOverflow
? new OverflowingTransactionMerkleProof() : null;
// Block Header
const blockHeader = new BlockHeader({
producer: address, // - blockCommitter [32 bytes] -- padded address
previousBlockHash: secondBlockHeader ? secondBlockHeader.hash : genesis.hash, // - previousBlockHash [32 bytes]
blockHeight: await contract.blockTip(), //- blockHeight [32 bytes]
ethereumBlockNumber: big(commitReceipt.blockNumber), // - ethereumBlockNumber [32 bytes]
transactionRoots: [root.hash], // - transactionRoots [64 + dynamic bytes]
});
// setup proofs
let proofs = 0;
// include proof for deposit input above
if (includeProofs) {
proofs = [depositProof]; // include the single deposit proof
}
// Construct Proofs
if (proofs !== 0) {
// Tx Proofs
proofs = new TransactionProofs([new SummingProof(summingTokenIsERC20
? erc20Token.address : summingToken)]
.concat(proofsOverride ? proofsOverride : proofs));
}
// Handle Special Withdrawal Proof
if (proofType === 'userWithdrawal') {
proofs = new TransactionProofs([new WithdrawalProof(token)]);
}
// transaction proof
const transactionProof = new TransactionProof({
block: blockHeader,
root,
merkle: merkleProofOverflow ? overflowMerkleProof : new TransactionMerkleProof(Object.assign({
transactionLeafs,
transactionIndex,
}, merkleProofOverride)),
transaction: new TransactionData(Object.assign({
inputIndex: inputIndex ? inputIndex : 0,
outputIndex: outputIndex ? outputIndex : 0,
witnessIndex: witnessIndex ? witnessIndex : 0,
transactionIndex,
transactionLeaf,
}, transactionDataOverride)),
proofs: proofs,
});
// let Proof
let proof = null;
// Malformed Transaction Proof
if (proofType === 'malformedTransaction') {
proof = new MalformedTransactionProof(transactionProof);
}
// User Withdrawal Proof
if (proofType === 'userWithdrawal') {
proof = new UserWithdrawalProof(transactionProof);
}
// Bond Withdrawal Proof
if (proofType === 'bondWithdrawal') {
proof = new BondWithdrawalProof(blockHeader);
}
// Bond Withdrawal Proof
if (proofType === 'invalidTransaction') {
proof = new InvalidTransactionProof(transactionProof);
}
// Submit the first proof twice
if (submitProofTwice) {
secondProof = transactionProof;
}
// Bond Withdrawal Proof
if (proofType === 'invalidTransactionInput') {
proof = new InvalidTransactionInputProof(transactionProof, secondProof);
}
// Bond Withdrawal Proof
if (proofType === 'invalidTransactionDoubleSpend') {
proof = new InvalidTransactionDoubleSpendProof(transactionProof, secondProof);
}
// Block tip adjusted.
eq(await contract.blockTip(), secondValidProof ? 3 : 1, 'tip check');
// Increase blocks before proof submission!
if (blockIncrease.gt(0)) {
console.log(`mining ${blockIncrease.toNumber()} blocks, this may take a while..`);
await increaseBlocks(blockIncrease.toNumber());
}
// Submit the exact proof a first time, before actual submission
if (submitProofTwice) {
try {
await contract.submitProof(proof.encoded, { gasLimit: loadsOfGas });
} catch (error) {}
}
// Submission contract instance
let submissionContract = contract;
// if other caller is used
if (otherCaller) {
submissionContract = atFuel(contract.address, 1);
}
// if checking for error
if (errorCode !== null) {
// Submit Proof
await reverts(() => submissionContract.submitProof(proof.encoded, { gasLimit: loadsOfGas }),
errorCode, message, logReceipts);
// Block tip adjusted.
eq(await contract.blockTip(), secondValidProof ? 3 : 1, 'tip check');
}
// if checking for fraud proof
if (fraudCode !== null) {
// Submit Proof
const proofSubmission = await submissionContract.submitProof(proof.encoded, { gasLimit: loadsOfGas });
const resultLogs = (await getReceipt(proofSubmission.hash)).logs;
// Log receipts
if (logReceipts) {
console.log(resultLogs);
}
// Check logs
eq(resultLogs.length, 1, 'should have log length 1');
// Logged fraud code
const loggedFraudCode = FuelInterface.parseLog(resultLogs[0]).values.fraudCode;
// Assert correct fraud code!
eq(loggedFraudCode, fraudCode, message);
// Block tip adjusted.
eq(await contract.blockTip(), secondValidProof ? 2 : 0, 'tip check');
}
// if no fraud or errors, check logs for no logs, success tx
if (fraudCode === null && errorCode === null) {
// Submit Proof
const proofSubmission = await submissionContract.submitProof(proof.encoded, { gasLimit: loadsOfGas });
const proofReceipt = await getReceipt(proofSubmission.hash);
const resultLogs = proofReceipt.logs;
// Log receipts
if (logReceipts) {
console.log(resultLogs);
}
if (proofType === 'userWithdrawal') {
// check withdrawal logs for correctness!
eq(resultLogs.length, 1, message);
const log = FuelInterface.parseLog(resultLogs[0]);
eq(log.name === 'WithdrawalMade' ? 1 : 0, 1, 'log name correct');
eq(log.values.token, token, 'token specified correct');
eq(log.values.account, withdrawalAccount, 'account specified correct');
} else if (proofType == 'bondWithdrawal') {
// check withdrawal logs for correctness!
eq(resultLogs.length, 1, message);
const log = FuelInterface.parseLog(resultLogs[0]);
eq(log.name === 'WithdrawalMade' ? 1 : 0, 1, 'log name correct');
eq(log.values.token, emptyAddress, 'token specified correct');
eq(log.values.account, withdrawalAccount, 'account specified correct');
} else {
// Check logs
eq(resultLogs.length, 0, message);
}
// Check Receipt status
eq(proofReceipt.status, 1, 'proof status good');
// Block tip adjusted.
eq(await contract.blockTip(), secondValidProof ? 3 : 1, 'tip check');
}
// Return contract instance
return contract;
} catch (error) {
throw new ByPassError(error);
}
}
// Submit User Withdrawal Proof
const submitUserWithdrawalProof = opts => submitMalformedTransactionProof(Object.assign(opts || {}, {
proofType: 'userWithdrawal',
}));
const submitBondWithdrawalProof = opts => submitMalformedTransactionProof(Object.assign(opts || {}, {
proofType: 'bondWithdrawal',
}));
const submitInvalidTransactionProof = opts => submitMalformedTransactionProof(Object.assign(opts || {}, {
proofType: 'invalidTransaction',
}));
const submitInvalidTransactionInputProof = opts => submitMalformedTransactionProof(Object.assign(opts || {}, {
proofType: 'invalidTransactionInput',
}));
const submitInvalidTransactionDoubleSpendProof = opts => submitMalformedTransactionProof(Object.assign(opts || {}, {
proofType: 'invalidTransactionDoubleSpend',
}));
// Wrapped RPC with Ethers Wallet
const wrappedRPC = signer => async (...args) => {
try {
const _rpc = interfaces.FuelRPC({ web3Provider: provider });
let wall = new ethers.Wallet(signer.privateKey, new ethers.providers.Web3Provider(provider));
if (args[0] === 'eth_sendTransaction') {
const txArgs = args[1];
const result = await wall.sendTransaction(txArgs);
return result.hash;
}
if (args[0] === 'eth_accounts') {
return [signer.address];
}
return _rpc(...args);
} catch (error) {
throw new ByPassError(error);
}
};
// Simulation Testing
async function simulatedContract(contract, remoteDB, mempoolDB, noIntake, localDB, accountsDB, t, faucetDB) {
test('spend tests', async _t => {
try {
if (remoteDB || mempoolDB) {
TypeDB(remoteDB);
TypeDB(mempoolDB);
}
// Genesis Block
const genesis = new GenesisBlock();
// Construct Fuel Contract
// const { contract } = await constructFuel(address);
// Deposit Amount
const depositAmount = big(40);
// Construct Deposit
const depositReceipt = await getReceipt((await contract.deposit(address, emptyAddress, depositAmount, {
gasLimit: loadsOfGas,
value: depositAmount,
})).hash);
// Unsigned Transaction
const unsignedTransaction = new TransactionUnsigned({
inputs: [ new TransactionInputDeposit({
depositHashID: constructDepositHashID({
account: address,
token: emptyAddress, // Ether address
ethereumBlockNumber: big(depositReceipt.blockNumber),
}),
witnessReference: 0,
}),
],
outputs: [ new TransactionOutputUTXO({
amount: big(10),
tokenID: big(0),
owner: address,
}),
new TransactionOutputUTXO({
amount: big(10),
tokenID: big(0),
owner: address,
}),
new TransactionOutputChange({
amount: big(10),
tokenID: big(0),
ownerAsWitnessIndex: 0,
}),
new TransactionOutputWithdrawal({
amount: big(5),
tokenID: big(0),
owner: address,
}),
new TransactionOutputHTLC({
amount: big(5),
tokenID: big(0),
owner: address,
digest: utils.keccak256(utils.hexZeroPad('0x01', 32)),
expiry: big(2),
returnWitnessIndex: 0,
}) ],
});
const transactions = new BlockTransactions([
new TransactionLeaf({
metadata: [],
witnesses: [
new TransactionWitness(constructWitness(unsignedTransaction, accounts[0])),
],
unsignedTransaction,
}),
]);
await contract.submitTransactions(transactions.merkleTreeRoot(), transactions.encoded, {
gasLimit: loadsOfGas,
});
const root = new TransactionRootHeader({
producer: address, // - transactionRootCommitter [32 bytes] -- padded address
merkleTreeRoot: transactions.merkleTreeRoot(), // - transactionRootMerkleTreeRoot [32 bytes] -- padded bytes32
commitmentHash: transactions.hash, // - transactionRootCommitmentHash [32 bytes]
index: big(0), // - transactionRootIndex [32 bytes] -- padded uint16
});
const commitReceipt = await getReceipt((await contract.commitBlock(1, [root.hash], {
value: await contract.BOND_SIZE(),
gasLimit: loadsOfGas,
})).hash);
// SECOND TRANSACTION in SEPERATE BLOCK, at a Time in Future..
await wait(10000);
// No MYSQL
if (!process.env.nomysql) {
// Testing wallet
const wallet_db = new MemoryDB();
await wallet_db.clear();
const wallet = new Wallet({
signer: accounts[0],
provider,
db: wallet_db,
chainId: 10,
rpc: wrappedRPC(accounts[0]),
_addresses: {
local: {
fuel: contract.address,
ether: '0x0000000000000000000000000000000000000000',
fakeDai: String('0xaB9c0bBFDD156Ed089299675c97896a8A01A1d6f').toLowerCase(),
},
},
_ids: {
local: {
'0x0000000000000000000000000000000000000000': '0',
[String('0xaB9c0bBFDD156Ed089299675c97896a8A01A1d6f').toLowerCase()]: '1',
},
},
_ignoreFrom: true,
_post: post(remoteDB, mempoolDB, accountsDB, faucetDB),
});
console.log('wallet sync');
// attempt sync
await wallet.sync();
_t.equal(1, 1, 'wallet sync');
// Block Number
const num = await wallet.blockNumber();
// Check wallet
_t.equal(num.toNumber(), 1, 'block num check');
const bal = await wallet.balance(wallet.tokens.ether);
_t.equal(bal.toNumber(), 35, 'wallet balance');
// Deposit Result
const depositResult = await wallet.deposit(130, wallet.tokens.ether);
// Attempt transfer
_t.equal((await wallet.balance(wallet.tokens.ether)).toNumber(), 130 + 35, 'wallet balancen after deposit');
}
console.log('Past wallet');
// Unsigned Transaction
const unsignedTransaction2 = new TransactionUnsigned({
inputs: [
new TransactionInputUTXO({
utxoID: (new UTXOProof({
transactionHashId: unsignedTransaction.hash,
outputIndex: 0,
type: FuelOutputTypes.UTXO,
amount: big(10),
tokenID: big(0),
owner: address,
})).hash,
witnessReference: 0,
}),
new TransactionInputUTXO({
utxoID: (new UTXOProof({
transactionHashId: unsignedTransaction.hash,
outputIndex: 1,
type: FuelOutputTypes.UTXO,
amount: big(10),
tokenID: big(0),
owner: address,
})).hash,
witnessReference: 0,
}),
new TransactionInputChange({
utxoID: (new UTXOProof({
transactionHashId: unsignedTransaction.hash,
outputIndex: 2,
type: FuelOutputTypes.Change,
amount: big(10),
tokenID: big(0),
owner: 0,
})).hash,
witnessReference: 0,
}),
// 3 is Withdrawal
new TransactionInputHTLC({
utxoID: (new UTXOProof({
transactionHashId: unsignedTransaction.hash,
outputIndex: 4,
type: FuelOutputTypes.HTLC,
amount: big(5),
tokenID: big(0),
owner: address,
digest: utils.keccak256(utils.hexZeroPad('0x01', 32)),
expiry: big(2),
returnWitnessIndex: 0,
})).hash,
preImage: utils.hexZeroPad('0x01', 32),
witnessReference: 0,
}),
],
outputs: [
new TransactionOutputUTXO({
amount: big(10),
tokenID: big(0),
owner: address,
}),
new TransactionOutputUTXO({
amount: big(10),
tokenID: big(0),
owner: address,
}),
new TransactionOutputChange({
amount: big(10),
tokenID: big(0),
ownerAsWitnessIndex: 0,
}),
new TransactionOutputHTLC({
amount: big(5),
tokenID: big(0),
owner: address,
digest: utils.keccak256(utils.hexZeroPad('0x04', 32)),
expiry: big(7),
returnWitnessIndex: 0,
})
],
});
const transactions2 = new BlockTransactions([
new TransactionLeaf({
metadata: [
new TransactionMetadata({
blockHeight: big(1),
transactionRootIndex: 0,
transactionIndex: 0,
outputIndex: 0,
}),
new TransactionMetadata({
blockHeight: big(1),
transactionRootIndex: 0,
transactionIndex: 0,
outputIndex: 1,
}),
new TransactionMetadata({
blockHeight: big(1),
transactionRootIndex: 0,
transactionIndex: 0,
outputIndex: 2,
}),
// 3 is Withdrawal
new TransactionMetadata({
blockHeight: big(1),
transactionRootIndex: 0,
transactionIndex: 0,
outputIndex: 4,
}),
],
witnesses: [
new TransactionWitness(constructWitness(unsignedTransaction2, accounts[0])),
],
unsignedTransaction: unsignedTransaction2,
}),
]);
await contract.submitTransactions(transactions2.merkleTreeRoot(), transactions2.encoded, {
gasLimit: loadsOfGas,
});
const root2 = new TransactionRootHeader({
producer: address, // - transactionRootCommitter [32 bytes] -- padded address
merkleTreeRoot: transactions2.merkleTreeRoot(), // - transactionRootMerkleTreeRoot [32 bytes] -- padded bytes32
commitmentHash: transactions2.hash, // - transactionRootCommitmentHash [32 bytes]
index: big(0), // - transactionRootIndex [32 bytes] -- padded uint16
});
const commitReceipt2 = await getReceipt((await contract.commitBlock(2, [root2.hash], {
value: await contract.BOND_SIZE(),
gasLimit: loadsOfGas,
})).hash);
// Lets attempt to deploy a double spend here...
// Than it should revert..
const doubleSpendAttempt = new TransactionUnsigned({
inputs: [
new TransactionInputUTXO({ // THIS IS ALREADY SPEND!!
utxoID: (new UTXOProof({
transactionHashId: unsignedTransaction.hash,
outputIndex: 0,
type: FuelOutputTypes.UTXO,
amount: big(10),
tokenID: big(0),
owner: address,
})).hash,
witnessReference: 0,
}),
],
outputs: [
new TransactionOutputUTXO({
amount: big(10),
tokenID: big(0),
owner: address,
}),
],
});
const doubleSpendAttemptTransactions = new BlockTransactions([
new TransactionLeaf({
metadata: [
new TransactionMetadata({
blockHeight: big(1),
transactionRootIndex: 0,
transactionIndex: 0,
outputIndex: 0,
}),
],
witnesses: [
new TransactionWitness(constructWitness(doubleSpendAttempt, accounts[0])),
],
unsignedTransaction: doubleSpendAttempt,
}),
]);
const contractFromSecond = contract.connect(wallets[3]);
await contractFromSecond.submitTransactions(doubleSpendAttemptTransactions.merkleTreeRoot(),
doubleSpendAttemptTransactions.encoded, {
gasLi