UNPKG

fuel-core

Version:

Essential javascript architecture for the Fuel side chain

1,555 lines (1,311 loc) 54.9 kB
const { utils } = require('ethers'); const { abi, big, BN, emptyAddress, emptyBytes32, RLP, hexToInt, bytes, eq, lt, lte, gt, gte, normalizeKey, wait, } = require('../utils/utils'); const _utils = require('../utils/utils'); const { FuelInterface, FuelUtilityInterface, ERC20EventsInterface, ERC20Interface, FuelEventsInterface, FuelConstants, FuelErrorCodes, FuelFraudNames, FuelFraudCodes, FuelConstantsCode, FuelInputTypes, FuelOutputTypes, FuelProofTypes, TransactionLengthByteSize, FuelDBKeys, } = require('../interfaces/interfaces'); const interfaces = require('../interfaces/interfaces'); const errors = require('../errors/errors'); const structs = require('../structs/structs'); const { TypeDB, TypeProvider, TypeFunction, TypeRPC, TypeArray, TypeInstance, TypeString, TypeDefined, TypeNumber, TypeBoolean, TypeAddress, TypeHex, TypeBigNumber, TypeObject, } = require('../types/types'); const { // Block / Root Headers BlockHeader, GenesisBlock, TransactionRootHeader, TransactionMerkleProof, // Inputs TransactionInputUTXO, TransactionInputHTLC, TransactionInputChange, TransactionInputDeposit, // Outputs TransactionOutputUTXO, TransactionOutputWithdrawal, TransactionOutputChange, TransactionOutputHTLC, // Transactional Structures TransactionUnsigned, TransactionMetadata, TransactionWitness, TransactionData, TransactionLength, TransactionLengthSpecifier, EmptyTransactionLeaf, RealEmptyTransactionLeaf, TransactionLeaf, // Utility reduceTokenID, encodeSignature, decodeSignature, constructWitness, ecrecoverPacked, constructDepositHashID, constructWithdrawalHashID, serializeRLP, constructMerkleTreeRoot, // UTXO Proofs UTXOProof, getUTXO, getDeposit, encodeProofs, TransactionProofLengths, TransactionProofs, EmptyUTXOProofs, TransactionProof, OverflowingTransactionMerkleProof, // Utility Proofs EmptyProof, FillProof, // Malformed Block Proofs MalformedBlockProof, BlockFraudProofLengths, BlockTransactions, BlockFraudProof, // Transaction Fraud Proofs BytesEncode, ProofType, MalformedTransactionProof, InvalidTransactionProof, InvalidTransactionInputProof, InvalidTransactionDoubleSpendProof, WithdrawalProof, SummingProof, DepositProof, UserWithdrawalProof, BondWithdrawalProof, } = require('../structs/structs'); const { parseTransactions } = require('../blocks/parseTransactions'); const MemoryDB = require('../dbs/MemoryDB'); // Simulation DB, clear every block const SimulationDB = require('../dbs/SimulationDB'); // Verify Amount Length function verifyAmountLength(amountLength) { const len = hexToInt(amountLength); // Assert amounts length greater than zero errors.assertOrFraud(len > 0, FuelConstants.FraudCode_TransactionOutputAmountLengthUnderflow); // Assert amounts length less than 33 (i.e 1 <> 32) errors.assertOrFraud(len <= 32, FuelConstants.FraudCode_TransactionOutputAmountLengthOverflow); // Amount length return len; } // A wrapped version which checks for overflow and throws fraud function _substr(str, start, len) { if (start > str.length || (start + len) > str.length) { errors.assertOrFraud(0, FuelConstants.FraudCode_ComputedTransactionLengthOverflow); } // substr return str.substr(start, len); } function constructDigest(preImage) { TypeHex(preImage, 32); return utils.keccak256(preImage); } // Do this in single WHILE loop, efficiency discovered here.. // Combine with the parseTransactions routine, use same memory array for leafs and processed tx;s // That should greatly reduce memory footprint and speed up parsing by not having so many promises / functions.. // Transaction Leaf, returns TransactionLeaf, throws malformed transaction error function parseTransaction({ transactionLeaf, transactionIndex, db, block, root, numTokens, validateBlockAndRoot, accounts, transactions }) { return new Promise((resolve, reject) => { try { // Enforce Types /* TypeHex(transactionLeaf); TypeNumber(transactionIndex); TypeInstance(block, BlockHeader); TypeInstance(root, TransactionRootHeader); TypeBigNumber(blockTip); TypeBigNumber(numTokens); if (typeof db !== 'undefined') { TypeDB(db); } */ // Remove hex const raw = transactionLeaf.slice(2); // Transaction Length const checkBlockAndRoot = validateBlockAndRoot === false ? false : true; // default true const transactionLength = hexToInt('0x' + _substr(raw, 0, bytes(2))); const metadataLength = hexToInt('0x' + _substr(raw, bytes(2), bytes(1))); // Check metadata length errors.assertOrFraud(metadataLength < FuelConstants.TransactionLengthMax, FuelConstants.FraudCode_TransactionMetadataLengthOverflow); let promises = []; let metadata = []; // Metadata Index for (var metadataIndex = 0; metadataIndex < metadataLength; metadataIndex++) { const pos = bytes(3) + bytes(metadataIndex * FuelConstants.MetadataSize); // Parse metadata const data = { blockHeight: big('0x' + _substr(raw, pos + bytes(0), bytes(4))), transactionRootIndex: hexToInt('0x' + _substr(raw, pos + bytes(4), bytes(1))), transactionIndex: hexToInt('0x' + _substr(raw, pos + bytes(5), bytes(2))), outputIndex: hexToInt('0x' + _substr(raw, pos + bytes(7), bytes(1))), }; // Assert input index overflow (i.e. metadata does not exist) errors.assertOrFraud(metadataIndex < metadataLength, FuelConstants.FraudCode_MetadataReferenceOverflow); if (checkBlockAndRoot) { // Assert Valid Metadata Block height errors.assertOrFraud(data.blockHeight.gt(0), FuelConstants.FraudCode_MetadataBlockHeightUnderflow); // Assert Valid Metadata Block height errors.assertOrFraud(data.transactionRootIndex <= FuelConstants.TRANSACTION_ROOTS_MAX, FuelConstants.FraudCode_InvalidTransactionRootIndexOverflow); // Check overflow of current block height errors.assertOrFraud(data.blockHeight.lte(block.height), FuelConstants.FraudCode_MetadataBlockHeightOverflow); } // Check Output Index errors.assertOrFraud(data.outputIndex < FuelConstants.TransactionLengthMax, FuelConstants.FraudCode_MetadataOutputIndexOverflow); metadata[metadataIndex] = data; } // Metadata Bytes Size const metadataSize = metadataLength * FuelConstants.MetadataSize; // Get witnesses length const witnessesLength = hexToInt('0x' + _substr(raw, bytes(3 + metadataSize), bytes(1))); // Signatures let witnesses = []; // Witness Length correctness errors.assertOrFraud(witnessesLength > FuelConstants.TransactionLengthMin, FuelConstants.FraudCode_TransactionWitnessesLengthUnderflow); errors.assertOrFraud(witnessesLength <= FuelConstants.TransactionLengthMax, FuelConstants.FraudCode_TransactionWitnessesLengthOverflow); // Total Witness Size in Bytes const witnessesSize = witnessesLength * FuelConstants.WitnessSize; // Unsigned Transaction Position in Bytes let position = bytes(4 + metadataSize + witnessesSize); // where UTXO, deposit, withdrawal proofs are located. const zero = big(0); // Inputs Length const inputsLength = hexToInt('0x' + _substr(raw, position, bytes(1))); const outputsLength = hexToInt('0x' + _substr(raw, position + bytes(1), bytes(1))); // Input Length Under/Overflow Checks errors.assertOrFraud(inputsLength > FuelConstants.TransactionLengthMin, FuelConstants.FraudCode_TransactionInputsLengthUnderflow); errors.assertOrFraud(inputsLength <= FuelConstants.TransactionLengthMax, FuelConstants.FraudCode_TransactionInputsLengthOverflow); // Assert metadata length correctness (metadata length can be zero) errors.assertOrFraud(metadataLength <= inputsLength, FuelConstants.FraudCode_TransactionMetadataLengthOverflow); // Output Length Under/Overflow Checks errors.assertOrFraud(outputsLength > FuelConstants.TransactionLengthMin, FuelConstants.FraudCode_TransactionOutputsLengthUnderflow); errors.assertOrFraud(outputsLength <= FuelConstants.TransactionLengthMax, FuelConstants.FraudCode_TransactionOutputsLengthOverflow); // generate unsigned transaciton hash id from raw data now position is checked! const transactionHashId = utils.keccak256('0x' + _substr(raw, position, bytes(transactionLength) - position)); // Define all output owners by way of signature reduction let recoveredWitnesses = []; // signatures for (var witnessIndex = 0; witnessIndex < witnessesLength; witnessIndex++) { // Witness Signature Position const signaturePosition = (4 + metadataSize) + (witnessIndex * FuelConstants.WitnessSize); // get input const signature = '0x' + _substr(raw, bytes(signaturePosition), bytes(FuelConstants.WitnessSize)); // Witnesses witnesses.push( signature, ); // recover witness owners recoveredWitnesses.push(ecrecoverPacked(transactionHashId, signature)); } // inputs and outputs let inputs = []; let outputs = []; // outsum let outsum = {}; // Go past input lengths position += bytes(2); // Go through inputs for (var inputIndex = 0; inputIndex < inputsLength; inputIndex++) { // Input Type const inputType = hexToInt('0x' + _substr(raw, position, bytes(1))); // Increase position past type position += bytes(1); // Input let input = null; // Entry from DB let entry = null; // Input length let inputLength = null; // Handle Type switch(inputType) { case FuelInputTypes.UTXO: input = { utxoID: '0x' + _substr(raw, position, bytes(32)), witnessReference: hexToInt('0x' + _substr(raw, position + bytes(32), bytes(1))), }; inputLength = 33; break; case FuelInputTypes.Deposit: input = { depositHashID: '0x' + _substr(raw, position, bytes(32)), witnessReference: hexToInt('0x' + _substr(raw, position + bytes(32), bytes(1))), }; inputLength = 33; // Add check to promises if (checkBlockAndRoot) { promises.push(() => new Promise((res, rej) => { // UTXO Proof getDeposit(db, input.depositHashID) .then(depositCheck => { try { errors.assertOrFraud(gt(depositCheck.proof.amount, 0), FuelConstants.FraudCode_TransactionInputDepositZero); res(); } catch (error) { return rej(error); } }) .catch(rej); })); } // Remove spend inputs if (accounts) { promises.push(accounts.del(FuelDBKeys.deposit + input.depositHashID.slice(2))); } break; case FuelInputTypes.HTLC: input = { utxoID: '0x' + _substr(raw, position, bytes(32)), witnessReference: hexToInt('0x' + _substr(raw, position + bytes(32), bytes(1))), preImage: '0x' + _substr(raw, position + bytes(32) + bytes(1), bytes(32)), }; inputLength = 65; break; case FuelInputTypes.Change: input = { utxoID: '0x' + _substr(raw, position, bytes(32)), witnessReference: hexToInt('0x' + _substr(raw, position + bytes(32), bytes(1))), }; inputLength = 33; break; default: errors.assertOrFraud(0, FuelConstants.FraudCode_InvalidTransactionInputType) } // Witness Overflow check errors.assertOrFraud(input.witnessReference < witnessesLength, FuelConstants.FraudCode_TransactionInputWitnessReferenceOverflow); // Increase Position position += bytes(inputLength); // Tag input input._kind = inputType; // Inputs inputs.push(input); } // Output Handling for (var outputIndex = 0; outputIndex < outputsLength; outputIndex++) { // Input Type const outputType = hexToInt('0x' + _substr(raw, position, bytes(1))); // Increase position past type position += bytes(1); // Input let output = null; // length let amountLength = null; // output length let outputLength = null; // Handle Type switch(outputType) { case FuelOutputTypes.UTXO: // Amount Length amountLength = verifyAmountLength('0x' + _substr(raw, position, bytes(1))); // Token Owner output = { amount: big('0x' + _substr(raw, position + bytes(1), bytes(amountLength))), owner: '0x' + _substr(raw, position + bytes(1) + bytes(amountLength), bytes(20)), tokenID: big('0x' + _substr(raw, position + bytes(1) + bytes(amountLength) + bytes(20), bytes(4))), }; // set owner output._ownerAddress = output.owner; // Output length outputLength = 1 + 20 + 4 + amountLength; break; case FuelOutputTypes.Withdrawal: // Amount Length amountLength = verifyAmountLength('0x' + _substr(raw, position, bytes(1))); // Token Owner output = { amount: big('0x' + _substr(raw, position + bytes(1), bytes(amountLength))), owner: '0x' + _substr(raw, position + bytes(1) + bytes(amountLength), bytes(20)), tokenID: big('0x' + _substr(raw, position + bytes(1) + bytes(amountLength) + bytes(20), bytes(4))), }; // set owner output._ownerAddress = output.owner; // Output length outputLength = 1 + 20 + 4 + amountLength; break; case FuelOutputTypes.HTLC: // Amount Length amountLength = verifyAmountLength('0x' + _substr(raw, position, bytes(1))); // So many structurs.. const htlcDataPosition = position + bytes(1) + bytes(amountLength) + bytes(20) + bytes(4); // Token Owner output = { amount: big('0x' + _substr(raw, position + bytes(1), bytes(amountLength))), owner: '0x' + _substr(raw, position + bytes(1) + bytes(amountLength), bytes(20)), tokenID: big('0x' + _substr(raw, position + bytes(1) + bytes(amountLength) + bytes(20), bytes(4))), digest: '0x' + _substr(raw, htlcDataPosition, bytes(32)), expiry: big('0x' + _substr(raw, htlcDataPosition + bytes(32), bytes(4))), returnWitnessIndex: hexToInt('0x' + _substr(raw, htlcDataPosition + bytes(32) + bytes(4), bytes(1))), }; // Output length outputLength = 1 + 20 + 4 + amountLength + 32 + 4 + 1; // set owner output._ownerAddress = output.owner; // Assert Token ID is Valid errors.assertOrFraud(gt(output.digest, 0), FuelConstants.FraudCode_TransactionOutputHTLCDigestZero); // Assert Expiry is Valid errors.assertOrFraud(gt(output.expiry, 0), FuelConstants.FraudCode_TransactionOutputHTLCExpiryZero); if (checkBlockAndRoot) { // Check expiry is greater than it's own block header errors.assertOrFraud(gt(output.expiry, block.height), FuelConstants.FraudCode_OutputHTLCExpiryUnderflow); } // Assert Valid Return Witness errors.assertOrFraud(output.returnWitnessIndex < witnessesLength, FuelConstants.FraudCode_TransactionOutputWitnessReferenceOverflow); // Recovery owner output._returnOwnerAddress = recoveredWitnesses[output.returnWitnessIndex]; break; case FuelOutputTypes.Change: // Amount Length amountLength = verifyAmountLength('0x' + _substr(raw, position, bytes(1))); // Token Owner output = { amount: big('0x' + _substr(raw, position + bytes(1), bytes(amountLength))), ownerAsWitnessIndex: hexToInt('0x' + _substr(raw, position + bytes(1) + bytes(amountLength), bytes(1))), tokenID: big('0x' + _substr(raw, position + bytes(1) + bytes(amountLength) + bytes(1), bytes(4))), }; // Invalid Witness Reference out of bounds errors.assertOrFraud(output.ownerAsWitnessIndex < witnessesLength, FuelConstants.FraudCode_TransactionOutputWitnessReferenceOverflow); // set owner output._ownerAddress = recoveredWitnesses[output.ownerAsWitnessIndex]; // Output length outputLength = 1 + 1 + 4 + amountLength; break; default: errors.assertOrFraud(0, FuelConstants.FraudCode_InvalidTransactionOutputType) } // set kind output._kind = outputType; // Input const tokenID = output.tokenID.toString(); // increase sum outsum[tokenID] = (outsum[tokenID] || zero).add(output.amount); // Assert Token ID is Valid errors.assertOrFraud(lt(output.tokenID, numTokens), FuelConstants.FraudCode_TransactionOutputTokenIDOverflow); // Increase Position position += bytes(outputLength); // Store eveyrthing in db if (db) { const utxoProof = new UTXOProof({ transactionHashId, outputIndex, type: output._kind, amount: output.amount, owner: output.owner || output.ownerAsWitnessIndex, tokenID: output.tokenID, digest: output.digest, expiry: output.expiry, returnWitness: output.returnWitnessIndex, block, root, transactionIndex, _ownerAddress: output._ownerAddress, }); // Withdrawals const dbKey = (output._kind !== FuelOutputTypes.Withdrawal ? FuelDBKeys.UTXO : FuelDBKeys.withdrawal) + utxoProof.hash.slice(2); // console.log('DB out key / utxo id', dbKey, '0x' + dbKey.slice(4)); // UTXO in DB promises.push(db.put(dbKey, utxoProof.rlp())); // Store utxo for historical purposes if (transactions) { promises.push(db.put(FuelDBKeys.storage + dbKey.slice(2), utxoProof.rlp())); } // Associate these accounts with this utxo.. if (accounts) { promises.push(accounts.put(dbKey, String(output._ownerAddress).toLowerCase())); // Establish relationship between this tx and the utxo owner if (transactions) { // Store historical relationship promises.push(accounts.put(FuelDBKeys.storage + dbKey.slice(2), String(output._ownerAddress).toLowerCase())); // Store transaction relationship to account promises.push(accounts.put(FuelDBKeys.transaction + transactionHashId.slice(2), String(output._ownerAddress).toLowerCase())); } } } // Inputs outputs.push(output); } // Positon const transactionLeafLength = raw.length; // If transaction storage is activated, store the leaf, values, root and index. if (transactions) { promises.push(db.put(FuelDBKeys.transaction + transactionHashId.slice(2), RLP.encode([ transactionLeaf, // db leaf _utils.serializeRLP(block.values), // value _utils.serializeRLP(root.values), // root big(transactionIndex).toHexString(), // tx index.. ]))); } // Invalid transaction length errors.assertOrFraud(transactionLeafLength === position, FuelConstants.FraudCode_ComputedTransactionLengthOverflow); // Resolve parse Promise.all(promises) .then(() => { // Return Transaction Leaf, dont release partial proofs if invalid input detected! resolve({ recoveredWitnesses, transactionHashId, metadata, witnesses, inputs, outputs, outsum, }); return; }) .catch(error => { logger.error(error); const err = new errors.ByPassError(error); err.transactionIndex = transactionIndex; err.fraudCode = error.fraudCode; err.stack = err.stack; return reject(err); }); } catch (error) { logger.error(error); const err = new errors.ByPassError(error); err.transactionIndex = transactionIndex; err.fraudCode = error.fraudCode; err.stack = err.stack; return reject(err); } }); } async function getToken({ db, tokenID }) { TypeDB(db); try { return RLP.decode(await db.get(FuelDBKeys.token + big(tokenID).toHexString().slice(2)))[0]; } catch (error) { throw new errors.ByPassError(error); } } // Transaction Leaf, returns TransactionLeaf, throws malformed transaction error async function validateTransaction({ parsedTransaction, db, accounts, block, contract, logger, root, numTokens, transactionIndex, leaf, rpc, leafs }) { try { // Type Object / Open Transaction Data TypeObject(parsedTransaction); const { transactionHashId, inputs, outputs, outsum, metadata, witnesses, recoveredWitnesses } = parsedTransaction; // Enforce Types TypeHex(transactionHashId, 32); TypeHex(leaf); TypeNumber(transactionIndex); TypeArray(inputs); TypeArray(outputs); TypeArray(metadata); TypeArray(witnesses); TypeObject(outsum); TypeObject(contract); TypeFunction(rpc); TypeArray(recoveredWitnesses); TypeInstance(block, BlockHeader); TypeInstance(root, TransactionRootHeader); TypeBigNumber(numTokens); TypeDB(db); if (accounts) { TypeDB(accounts); } // Proofs and bools let proofs = []; let invalidInput = null; let invalidTransaction = false; const zero = big(0); // In sum let insum = {}; // token address => big(0); // metadata selected let metadataIndex = 0; // Go through inputs for (var inputIndex = 0; inputIndex < inputs.length; inputIndex++) { // Input Index let entry = null; const input = inputs[inputIndex]; const inputType = input._kind; // Handle Type switch(inputType) { case FuelInputTypes.UTXO: // UTXO Proof entry = !invalidInput ? await getUTXO(db, input.utxoID) : null; // Removal of DB if (entry) { await db.del(FuelDBKeys.UTXO + input.utxoID.slice(2)); await db.del(FuelDBKeys.mempool + FuelDBKeys.UTXO.slice(2) + input.utxoID.slice(2)); if (accounts) { await accounts.del(FuelDBKeys.UTXO + input.utxoID.slice(2)); await accounts.del(FuelDBKeys.mempool + FuelDBKeys.UTXO.slice(2) + input.utxoID.slice(2)); } } break; case FuelInputTypes.Deposit: // UTXO Proof entry = !invalidInput ? await getDeposit(db, input.depositHashID) : null; // Entry if (entry) { // Assert deposit is not zero errors.assertOrFraud(gt(entry.proof.amount, 0), FuelConstants.FraudCode_TransactionInputDepositZero); // Remove Deposit await db.del(FuelDBKeys.deposit + input.depositHashID.toLowerCase().slice(2)); if (accounts) { await accounts.del(FuelDBKeys.deposit + input.depositHashID.toLowerCase().slice(2)); } } break; case FuelInputTypes.HTLC: // UTXO Proof entry = !invalidInput ? await getUTXO(db, input.utxoID) : null; // Check digest if (entry && !invalidTransaction) { if (lt(block.height, entry.proof.expiry)) { invalidTransaction = !eq(entry.proof.digest, constructDigest(input.preImage)); } // Remove Deposit await db.del(FuelDBKeys.UTXO + input.utxoID.slice(2)); await db.del(FuelDBKeys.mempool + FuelDBKeys.UTXO.slice(2) + input.utxoID.slice(2)); if (accounts) { await accounts.del(FuelDBKeys.UTXO + input.utxoID.slice(2)); await accounts.del(FuelDBKeys.mempool + FuelDBKeys.UTXO.slice(2) + input.utxoID.slice(2)); } } break; case FuelInputTypes.Change: // UTXO Proof entry = !invalidInput ? await getUTXO(db, input.utxoID) : null; if (entry) { await db.del(FuelDBKeys.UTXO + input.utxoID.slice(2)); await db.del(FuelDBKeys.mempool + FuelDBKeys.UTXO.slice(2) + input.utxoID.slice(2)); if (accounts) { await accounts.del(FuelDBKeys.UTXO + input.utxoID.slice(2)); await accounts.del(FuelDBKeys.mempool + FuelDBKeys.UTXO.slice(2) + input.utxoID.slice(2)); } } break; } // console.log('Entry lookup hash / index / key / entry', // transactionHashId, transactionIndex, // input.utxoID || input.depositHashID, entry); // This means either it doesn't exist or is double spend if (!entry && !invalidInput) { // This is the invalid input invalidInput = { metadata: metadata[metadataIndex], metadataIndex, input, inputType, inputIndex, witnessAddress: recoveredWitnesses[input.witnessReference], }; // Clear Proofs and Sums from Memory proofs = null; insum = null; } // If there is an entry and no invalid input if (entry && !invalidInput) { proofs.push( entry.proof, ); // Token ID const tokenID = entry.proof.tokenID.toString(); // owner from proof const owner = entry.proof._ownerAddress; // increase sum insum[tokenID] = (insum[tokenID] || zero).add(entry.proof.amount); // Check witness if (!invalidTransaction) { invalidTransaction = !eq(owner, recoveredWitnesses[input.witnessReference]); } } // Select next metadata if (inputType !== FuelInputTypes.Deposit) { metadataIndex += 1; } } // if no invalid input, test sums if (!invalidInput && !invalidTransaction) { // Insum const tokenIDs = Object.keys(insum); // Interate Through Token ids, detect invalid sum for (var tokenIndex = 0; tokenIndex < tokenIDs.length; tokenIndex++) { const tokenID = tokenIDs[tokenIndex]; if (!invalidTransaction) { // Ins != Outs, than it's invalid! invalidTransaction = !eq(insum[tokenID] || zero, outsum[tokenID] || zero); // Set Invalid Sum token if (invalidTransaction) { // Invalid Summing Tokenx proofs = [{ _kind: 'summing', tokenID, token: await getToken({ db, tokenID }), }].concat(proofs); // Break the For Loop break; } } } // Outsum const outSumTokenIDs = Object.keys(outsum); // Interate Through Token ids, detect invalid sum for (var tokenIndex = 0; tokenIndex < outSumTokenIDs.length; tokenIndex++) { const tokenID = outSumTokenIDs[tokenIndex]; if (!invalidTransaction) { // Ins != Outs, than it's invalid! invalidTransaction = !eq(insum[tokenID] || zero, outsum[tokenID] || zero); // Set Invalid Sum token if (invalidTransaction) { // Invalid Summing Tokenx proofs = [{ _kind: 'summing', tokenID, token: await getToken({ db, tokenID }), }].concat(proofs); // Break the For Loop break; } } } } // If Invalid, Create Invalid Transaction Proof if (invalidTransaction) { throw new errors.ProofError(new InvalidTransactionProof(new TransactionProof({ block, root, merkle: new TransactionMerkleProof({ transactionLeafs: leafs.map(v => new FillProof(v)), transactionIndex }), transaction: new TransactionData({ inputIndex: 0, outputIndex: 0, witnessIndex: 0, transactionIndex, transactionLeaf: new FillProof(leaf), }), proofs: new TransactionProofs(proofs.map(proofData => { if (proofData._kind === 'summing') { return new SummingProof(proofData.token); } else if (proofData._kind === 'utxo') { return new UTXOProof(proofData); } else { return new DepositProof(proofData); } })), }))); } else if (invalidInput) { // Transaction Proof const transactionProofB = await isInvalidInputReference({ invalidInput, db, rpc, numTokens, contract, logger }); // Invalid Transaction Input Doesn't Exist if (transactionProofB) { logger.log(transactionProofB.message); throw new errors.ProofError(new InvalidTransactionInputProof(new TransactionProof({ block, root, merkle: new TransactionMerkleProof({ transactionLeafs: leafs.map(v => new FillProof(v)), transactionIndex }), transaction: new TransactionData({ inputIndex: invalidInput.inputIndex, outputIndex: 0, witnessIndex: 0, transactionIndex, transactionLeaf: new FillProof(leaf), }), proofs: 0, }), transactionProofB)); } else { // Scan entire chain for Double Spend const ds_transactionProofB = await isDoubleSpend({ invalidInput, db, rpc, numTokens, contract, logger }); // We have a serious problem if (!ds_transactionProofB) { throw new errors.FuelError('We have a serious problem.. double spend not found in double spend serious..'); } // Double Spend Proof const doubleSpendProof = new InvalidTransactionDoubleSpendProof(new TransactionProof({ block, root, merkle: new TransactionMerkleProof({ transactionLeafs: leafs.map(v => new FillProof(v)), transactionIndex }), transaction: new TransactionData({ inputIndex: invalidInput.inputIndex, outputIndex: 0, witnessIndex: 0, transactionIndex, transactionLeaf: new FillProof(leaf), }), proofs: 0, }), ds_transactionProofB); // Double spend proof logger.log(doubleSpendProof); // create malformed transaction proof throw new errors.ProofError(doubleSpendProof); } } } catch (error) { throw new errors.ByPassError(error); } } // Is Invalid, Determine if Input Reference Even exists async function isInvalidInputReference({ invalidInput, db, rpc, numTokens, contract, logger }) { try { TypeDB(db); TypeObject(contract); TypeFunction(rpc); TypeBigNumber(numTokens); TypeObject(contract); // Spread Data const { metadata, metadataIndex, input, inputType, inputIndex, witnessAddress, } = invalidInput; // Metadata const { blockHeight, transactionRootIndex, transactionIndex, outputIndex, } = metadata; logger.error(` Fraud System Activated, Invalid Input Reference Detected Below: Transaction Details: Block Height: ${blockHeight} Root Index: ${transactionRootIndex} Tx Index: ${transactionIndex} Output Index: ${outputIndex} Metadata and Input Details: Details: ${JSON.stringify(metadata)} Index: ${metadataIndex} Input: ${JSON.stringify(input)} Input Type: ${inputType} Input Index: ${inputIndex} Witness Address in Question: ${witnessAddress} `); // logger. // Block Header const blockHeader = await getBlockHeader(db, blockHeight); // FraudCode_InvalidTransactionRootIndexOverflow if(gte(transactionRootIndex, blockHeader.transactionRoots.length)) { // Just sumbit the first tx of the first root const proofTransactionRootIndex = 0; const proofRootHeader = await getTransactionRoot(db, blockHeader, proofTransactionRootIndex); const proofTransactions = await transactionsFromReceipt(rpc, proofRootHeader.transactionHash); const proofleafs = parseTransactions(proofTransactions); const proofTransactionIndex = 0; // Return Transaction Proof of any Transaction return new TransactionProof({ block: blockHeader, root: proofRootHeader, merkle: new TransactionMerkleProof({ transactionLeafs: proofleafs.map(v => new FillProof(v)), transactionIndex: proofTransactionIndex, }), transaction: new TransactionData({ inputIndex: 0, outputIndex: 0, witnessIndex: 0, transactionIndex: proofTransactionIndex, transactionLeaf: new FillProof(proofleafs[proofTransactionIndex]), }), proofs: 0, message: `Root overflow detected root index: ${transactionRootIndex} > roots length ${blockHeader.transactionRoots.length}`, }); } // Get actual root, transactions and leafs const rootHeader = await getTransactionRoot(db, blockHeader, transactionRootIndex); const transactions = await transactionsFromReceipt(rpc, rootHeader.transactionHash); const leafs = parseTransactions(transactions); // FraudCode_TransactionIndexOverflow if (gte(transactionIndex, leafs.length)) { // Return rightmost proof const proofTransactionIndex = leafs.length - 1; return new TransactionProof({ block: blockHeader, root: rootHeader, merkle: new TransactionMerkleProof({ transactionLeafs: leafs.map(v => new FillProof(v)), proofTransactionIndex }), transaction: new TransactionData({ inputIndex: 0, outputIndex: 0, witnessIndex: 0, transactionIndex: proofTransactionIndex, transactionLeaf: new FillProof(leafs[proofTransactionIndex]), }), proofs: 0, message: `Transaction index overflow index: ${transactionIndex} > leafs length ${leafs.length}`, }); } // get Merkle Proof of Transaction in Question const merkleProof = new TransactionMerkleProof({ transactionLeafs: leafs.map(v => new FillProof(v)), transactionIndex, }); // FraudCode_TransactionHashZero if (eq(merkleProof.transactionHash, emptyBytes32)) { // Return transaction, as Leaf Hash is Empty!! return new TransactionProof({ block: blockHeader, root: rootHeader, merkle: merkleProof, transaction: new TransactionData({ inputIndex: 0, outputIndex: 0, witnessIndex: 0, transactionIndex, transactionLeaf: new FillProof(leafs[transactionIndex]), }), proofs: 0, message: `Transaction Hash Zero ${merkleProof.transactionHash}`, }); } const { transactionHashId, recoveredWitnesses, witnesses, inputs, outputs, } = await parseTransaction({ transactionLeaf: leafs[transactionIndex], transactionIndex, block: blockHeader, root: rootHeader, numTokens, contract, validateBlockAndRoot: false, }); // FraudCode_MetadataOutputIndexOverflow if (gte(outputIndex, outputs.length)) { // Return transaction, as Leaf Hash is Empty!! return new TransactionProof({ block: blockHeader, root: rootHeader, merkle: merkleProof, transaction: new TransactionData({ inputIndex: 0, outputIndex: 0, witnessIndex: 0, transactionIndex, transactionLeaf: new FillProof(leafs[transactionIndex]), }), proofs: 0, message: `Transaction Index Overflow index: ${outputIndex} >= outputs length ${outputs.length}`, }); } // Proof output const output = outputs[outputIndex]; // Create Tx Proof for Specified ouput and index const transactionProof = new TransactionProof({ block: blockHeader, root: rootHeader, merkle: merkleProof, transaction: new TransactionData({ inputIndex: 0, outputIndex, witnessIndex: 0, transactionIndex, transactionLeaf: new FillProof(leafs[transactionIndex]), }), proofs: 0, message: 'Invalid output type withdrawl or mismatch output type', }); // FraudCode_InvalidInputWithdrawalSpend if (output.type === FuelOutputTypes.Withdrawal) { // Return transaction, as Leaf Hash is Empty!! transactionProof.message = 'Withdrawl spend'; return transactionProof; } // FraudCode_InvalidTypeReferenceMismatch if (inputType !== output._kind) { // Return transaction, as Leaf Hash is Empty!! transactionProof.message = 'Input type mismatch'; return transactionProof; } // build utxoID's const utxoID = input.utxoID; const outputUTXOProof = new UTXOProof({ transactionHashId, outputIndex, type: output._kind, amount: output.amount, owner: output.owner || output.ownerAsWitnessIndex, tokenID: output.tokenID, digest: output.digest, expiry: output.expiry, returnWitnessIndex: output.returnWitnessIndex, }); // FraudCode_InvalidUTXOHashReference if (!eq(utxoID, outputUTXOProof.hash)) { // Return transaction, as Leaf Hash is Empty!! transactionProof.message = `Invalid utxo id hash FraudCode_InvalidUTXOHashReference ${utxoID} ${outputUTXOProof.hash}`; return transactionProof; } // FraudCode_InvalidChangeInputSpender if (output.type === FuelOutputTypes.Change) { // Input Witness Address not Equal to Witness if (!eq(witnessAddress, output._ownerAddress)) { transactionProof.message = `Invalid witness address ${output.type}`; return transactionProof; } } // HTLC Output handling if (output.type === FuelOutputTypes.HTLC) { // If HTLC is expired if (gte(blockHeader.height, output.expiry)) { // FraudCode_InvalidReturnWitnessNotSpender if (!eq(witnessAddress, output._returnOwnerAddress)) { transactionProof.message = `Invalid HTLC witness address on expired block height ${blockHeader.height} ${output.expiry} ${witnessAddress} ${output._returnOwnerAddress}`; return transactionProof; } } } // Proof is Valid, must be a double spend.. return null; } catch (error) { throw new errors.ByPassError(error); } } // Get block header from DB async function getBlockHeader(db, blockHeight) { try { TypeDB(db); TypeBigNumber(blockHeight); // Block const blockRLP = await db.get(interfaces.FuelDBKeys.block + _utils.big(blockHeight.toNumber()).toHexString().slice(2)); // Block if (!blockRLP) { throw new Error(`block does not exist in db, ${blockHeight}`); } // Decoee RLP const blockDecoded = _utils.RLP.decode(blockRLP); // Block HEader return new structs.BlockHeader({ producer: blockDecoded[0], // - blockProducer [32 bytes] -- padded address previousBlockHash: blockDecoded[1], // - previousBlockHash [32 bytes] blockHeight: _utils.big(blockDecoded[3]), //- blockHeight [32 bytes] ethereumBlockNumber: _utils.big(blockDecoded[2]), // - ethereumBlockNumber [32 bytes] transactionRoots: blockDecoded[4], // }); } catch (error) { throw new errors.ByPassError(error); } } // Get Root Header from DB async function getTransactionRoot(db, blockHeader, rootIndex) { try { TypeDB(db); TypeInstance(blockHeader, structs.BlockHeader); TypeNumber(rootIndex); // Root hash const rootHash = blockHeader.transactionRoots[rootIndex]; // Block const transactionRootRLP = await db.get(interfaces.FuelDBKeys.transactionRoot + rootHash.slice(2)); // Block if (!transactionRootRLP) { throw new Error(`tx root does not exist in db, ${transactionRootRLP}`); } // Decode const transactionRootDecoded = _utils.RLP.decode(transactionRootRLP); // Block Header return new structs.TransactionRootHeader({ producer: transactionRootDecoded[0], merkleTreeRoot: transactionRootDecoded[1], commitmentHash: transactionRootDecoded[2], index: _utils.big(rootIndex), transactionHash: transactionRootDecoded[3], }); } catch (error) { throw new errors.ByPassError(error); } } // Scan Entire Chain for Input, than Produce Transaction Proof async function isDoubleSpend({ invalidInput, db, rpc, numTokens, contract, logger }) { try { TypeDB(db); TypeObject(invalidInput); TypeObject(contract); TypeFunction(rpc); TypeBigNumber(numTokens); // Spread Data const { metadata, metadataIndex, input, inputType, inputIndex, witnessAddress, } = invalidInput; // Metadata const { blockHeight, transactionRootIndex, transactionIndex, outputIndex, } = metadata; const blockTip = await contract.blockTip(); // get input utxo id const utxoID = input.utxoID; // Logger Error logger.error(` Fraud System Activated, Invalid Input Reference Detected Below: | Transaction Details: Block Height: ${blockHeight} Root Index: ${transactionRootIndex} Tx Index: ${transactionIndex} Output Index: ${outputIndex} | Metadata and Input Details: Details: ${metadata} Index: ${metadataIndex} Input: ${input} Input Type: ${inputType} Input Index: ${inputIndex} Witness Address in Question: ${witnessAddress} `); // search block height for (let searchBlockHeight = 1; // start past genesis searchBlockHeight <= blockTip.toNumber(); searchBlockHeight++) { // Block Header const blockHeader = await getBlockHeader(db, _utils.big(searchBlockHeight)); // Search roots for (let rootIndex = 0; rootIndex < blockHeader.transactionRoots.length; rootIndex++) { // get Root Header const rootHeader = await getTransactionRoot(db, blockHeader, rootIndex); const transactions = await transactionsFromReceipt(rpc, rootHeader.transactionHash); // Search for UTXO ID in transactions blob if (transactions.toLowerCase() .indexOf(utxoID.slice(2).toLowerCase()) !== -1) { // Get Leafs const leafs = parseTransactions(transactions); // Search Through Leafs for (let searchTransactionIndex = 0; searchTransactionIndex < leafs.length; searchTransactionIndex++) { // Find Specific Leaf if (leafs[searchTransactionIndex].toLowerCase() .indexOf(utxoID.slice(2).toLowerCase()) !== -1) { let searchInputs = []; // Parse Transaction try { const { inputs } = await parseTransaction({ transactionLeaf: leafs[searchTransactionIndex], block: blockHeader, root: rootHeader, numTokens, contract, }); // Search inputs searchInputs = inputs; } catch (error) { logger.log('while doing double spend, malformed tx'); } // Search through inputs now.. for (var searchInputIndex = 0; searchInputIndex < searchInputs.length; searchInputIndex++) { // Search input Object const searchInput = searchInputs[searchInputIndex]; // Check to make sure this is not the input referenced, which is valid but spent! const isNotReferencedOutput = !eq(blockHeader.height, blockHeight) || !eq(rootHeader.index, transactionRootIndex) || !eq(searchTransactionIndex, transactionIndex) || !eq(searchInputIndex, inputIndex); // Got you double spender!! if (eq(searchInput.utxoID, utxoID) && isNotReferencedOutput) { return new TransactionProof({ block: blockHeader, root: rootHeader, merkle: new TransactionMerkleProof({ transactionLeafs: leafs.map(v => new FillProof(v)), transactionIndex: searchTransactionIndex }), transaction: new TransactionData({ inputIndex: searchInputIndex, outputIndex: 0, witnessIndex: 0, transactionIndex, transactionLeaf: new FillProof(leafs[searchTransactionIndex]), }), proofs: 0, }); } } } } } } } } catch (error) { throw new errors.ByPassError(error); } } // simple get receipt function getReceipt(rpc, transactionHash) { TypeHex(transactionHash, 32); // get receipt return rpc('eth_getTransactionReceipt', transactionHash); } // Transactions From Receipt async function transactionsFromReceipt(rpc, transactionHash) { try { TypeFunction(rpc); TypeHex(transactionHash, 32); // Input Data const input = (await rpc('eth_getTransactionByHash', transactionHash)).input; // Transactions Data const transactions = abi.decode(['bytes32', 'bytes'], '0x' + input.slice(10))[1]; // Transactions return transactions; } catch (error) { throw new errors.ByPassError(error); } } // validate entire block in single thread async function processBlock(blockHeader, { db, accounts, transactions, rpc, numTokens, contract, logger, _roots }) { try { // Enforce types TypeDB(db); TypeInstance(blockHeader, BlockHeader); TypeFunction(rpc); TypeObject(contract); if (accounts) { TypeDB(accounts); } if (typeof transactions !== "undefined") { TypeBoolean(transactions); } if (typeof _roots !== "undefined") { TypeArray(_roots); } // Check for genesis if (blockHeader.height.eq(0)) { return { success: true, proof: null }; } // simulate writes in cache, storage as get if not in cache let simulationDB = new SimulationDB({ storage: db, cache: new MemoryDB(), }); // For handling accounts during processing.. let accountsSimulationDB = null; if (accounts) { accountsSimulationDB = new SimulationDB({ storage: accounts, cache: new MemoryDB(), }); } let transactionData = []; let roots = []; // Processing time.. logger.log('Block processing begining for side-chain block: ', blockHeader.height.toString()); // go through roots and parse out leafs logger.time(`Parallel transaction parsing ${blockHeader.transactionRoots.length}`); for (var transactionRootIndex = 0; transactionRootIndex < blockHeader.transactionRoots.length; transactionRootIndex++) { transactionData[transactionRootIndex] = []; // Transaction Root const transactionRoot = _roots ? _roots[transactionRootIndex].header.hash : blockHeader.transactionRoots[transactionRootIndex]; // RLP root const rlpRoot = _roots ? null : RLP.decode(await db.get(FuelDBKeys.transactionRoot + transactionRoot.slice(2))); // transaction root header const rootHeader = _roots ? _roots[transactionRootIndex].header : new TransactionRootHeader({ producer: rlpRoot[0], merkleTreeRoot: rlpRoot[1], commitmentHash: rlpRoot[2], transactionHash: rlpRoot[3], index: big(transactionRootIndex), }); // Transactions Data let transactions = _roots ? _roots[transactionRootIndex].transactions.encoded : await transactionsFromReceipt(rpc, rootHeader.transactionHash); // Try / Catch let leafs; // Attempt parsing out the leaves try { leafs = parseTransactions(transactions); transactions = null; // clear txs } catch (error) { logger.log('Malformed block error!'); // Submit malformed block proof return { success: false, proof: new Malf