import assert from 'assert' import crypto from 'crypto' const xpop = async ({ vl, ledger: { json, binary, }, validations, tx, }) => { const proof = create_proof(binary?.transactions, tx?.transaction?.hash) const computed_transactions_root = hash_proof(proof) const { tx_blob: blob, meta } = binary?.transactions?.filter(bintx => bintx?.tx_id === tx?.transaction?.hash)?.[0] const computed_ledger_hash = hash_ledger( json.ledger_index, json.total_coins, json.parent_hash, computed_transactions_root, json.account_hash, json.parent_close_time, json.close_time, json.close_time_resolution, json.close_flags, ) const data = validations.reduce((a, b) => Object.assign(a, { [b.validation_public_key]: b.data.toString('hex'), }), {}) assert(computed_ledger_hash === json.ledger_hash, 'Invalid ledger hash computed vs. closed ledger') const xpopObj = { ledger: { index: Number(json?.ledger_index), coins: json?.total_coins, phash: json?.parent_hash, txroot: computed_transactions_root, acroot: json?.account_hash, pclose: json?.parent_close_time, close: json?.close_time, cres: json?.close_time_resolution, flags: json?.close_flags, }, validation: { data, unl: vl.vl, }, transaction: { blob, meta, proof, } } return JSON.stringify(xpopObj) } export { xpop, } /** * Libs down (@richardah) * https://github.com/RichardAH/xpop-generator/blob/master/pov.js */ const make_vl_bytes = len => { const report_error = e => { console.error(e) } if (typeof(len) != 'number') { report_error("non-numerical length passed to make_vl_bytes") return false } len = Math.ceil(len) if (len <= 192) { let b1 = len.toString(16) return (b1.length == 1 ? '0' + b1 : b1).toUpperCase() } else if (len <= 12480) { let b1 = Math.floor((len - 193) / 256 + 193) let b2 = len - 193 - 256 * (b1 - 193) b1 = b1.toString(16) b2 = b2.toString(16) return ((b1.length == 1 ? '0' + b1 : b1) + (b2.length == 1 ? '0' + b2 : b2)).toUpperCase() } else if (len <= 918744) { let b1 = Math.floor((len - 12481) / 65536 + 241) let b2 = Math.floor((len - 12481 - 65536 * (b1 - 241)) / 256) let b3 = len - 12481 - 65536 * (b1 - 241) - 256 * b2 b1 = b1.toString(16) b2 = b2.toString(16) b3 = b3.toString(16) return ((b1.length == 1 ? '0' + b1 : b1) + (b2.length == 1 ? '0' + b2 : b2) + (b3.length == 1 ? '0' + b3 : b3)).toUpperCase() } else { report_error("cannot generate vl for length = " + len + ", too large") return false } } const sha512h = b => { if (typeof(b) == 'string') b = Buffer.from(b, 'hex') return crypto.createHash('sha512').update(b).digest().slice(0, 32).toString('hex').toUpperCase() } const prefix_LWR = '4C575200' const prefix_SND = '534E4400' const prefix_MIN = '4D494E00' const prefix_TXN = '54584E00' const hex = {0:'0', 1:'1', 2:'2', 3:'3', 4:'4', 5:'5', 6:'6', 7:'7', 8:'8', 9:'9',10:'A',11:'B',12:'C',13:'D',14:'E',15:'F'} const numToHex = (n, size) => { if (typeof(n) != 'string') n = n.toString(16) n = '0'.repeat((size*2)-n.length) + n return n } const hash_ledger = (ledger_index, total_coins, parent_hash, transaction_hash, account_hash, parent_close_time, close_time, close_time_resolution, close_flags) => { if (typeof(parent_hash) != 'string') parent_hash = parent_hash.toString('hex') if (typeof(transaction_hash) != 'string') transaction_hash = transaction_hash.toString('hex') if (typeof(account_hash) != 'string') account_hash = account_hash.toString('hex') if (typeof(ledger_index) == 'string') ledger_index = BigInt(ledger_index) if (typeof(total_coins) == 'string') total_coins = BigInt(total_coins) if (typeof(parent_close_time) == 'string') parent_close_time = BigInt(parent_close_time) if (typeof(close_time) == 'string') close_time = BigInt(close_time) if (typeof(close_time_resolution) == 'string') close_time_resolution = BigInt(close_time_resolution) if (typeof(close_flags) == 'string') close_flags = BigInt(close_flags) const payload = prefix_LWR + numToHex(ledger_index, 4) + numToHex(total_coins, 8) + parent_hash + transaction_hash + account_hash + numToHex(parent_close_time, 4) + numToHex(close_time, 4) + numToHex(close_time_resolution, 1) + numToHex(close_flags, 1).toUpperCase() return crypto.createHash('sha512'). update(Buffer.from(payload, 'hex')). digest(). slice(0,32). toString('hex'). toUpperCase() } const compute_tree = (tree, depth=0) => { const nullhash = '0'.repeat(64) let hasher = crypto.createHash('sha512') hasher.update(Buffer.from(prefix_MIN, 'hex')) for (let i = 0; i < 16; ++i) { let nibble = hex[i] let to_append = '' if (tree.children[nibble] === undefined) to_append = nullhash else if (Object.keys(tree.children[nibble].children).length == 0) to_append = tree.children[nibble].hash else to_append = compute_tree(tree.children[nibble], depth+1) hasher.update(Buffer.from(to_append, 'hex')) } tree.hash = hasher.digest().slice(0,32).toString('hex').toUpperCase() return tree.hash } const hash_txn = txn => { if (typeof(txn) != 'string') txn = txn.toString('hex') return sha512h(prefix_TXN + txn) } const hash_txn_and_meta = (txn, meta) => { if (typeof(txn) != 'string') txn = txn.toString('hex') if (typeof(meta) != 'string') meta = meta.toString('hex') const vl1 = make_vl_bytes(txn.length/2) const vl2 = make_vl_bytes(meta.length/2) return sha512h(prefix_SND + vl1 + txn + vl2 + meta + hash_txn(txn)) } const report_error = e => { throw(e) //console.error(e) } const create_tree = txns => { let root = {children: {}, hash: null, key: '0'.repeat(64)} // pass one: populate for (let k = 0; k < txns.length; ++k) { const txn = txns[k].tx_blob const meta = txns[k].meta const hash = hash_txn(txn) let node = root let upto = 0 let error = true while (upto < hash.length) { let nibble = hash[upto] if (!(nibble in node.children)) { node.children[nibble] = { children: {}, hash: hash_txn_and_meta(txn, meta), key : hash } error = false break } else if (Object.keys(node.children[nibble].children).length == 0) { // create a new node let oldnode = node.children[nibble] let newnibble = oldnode.key[upto+1] node.children[nibble] = {children: {}, hash: null, key: hash.slice(0,upto+1)} node.children[nibble].children[newnibble] = oldnode node = node.children[nibble] upto++ continue } else { node = node.children[nibble] upto++ continue } } if (error) { report_error(error) return false } } // pass two: recursively compute hashes compute_tree(root) return root } // generate the proof // pass valid merkle tree and the canonical txn hash as key const create_proof_from_tree = (tree, key, upto = 0) => { if (tree === undefined) return false tree = tree.children if (tree === undefined) return false let proof = [] let n = parseInt(key[upto], 16) for (let i = 0; i < 16; ++i) { const h = hex[i] if (i == n) { if (tree[h] === undefined) return false else if (tree[h].key == key) proof.push(tree[h].hash) else { let retval = create_proof_from_tree(tree[h], key, upto+1) if (!retval) return false proof.push(retval) } } else if (tree[h] === undefined) proof.push('0'.repeat(64)) else proof.push(tree[h].hash) } return proof } const create_proof = (txns, key) => { const tree = create_tree(txns) if (!tree) return false return create_proof_from_tree(tree, key, 0) } const hash_proof = (proof) => { if (proof === undefined) return false let hasher = crypto.createHash('sha512') hasher.update(Buffer.from(prefix_MIN, 'hex')) for (let i = 0; i < 16; ++i) { if (proof[i] === undefined) return false else if (typeof(proof[i]) == 'string') hasher.update(Buffer.from(proof[i], 'hex')) else hasher.update(Buffer.from(hash_proof(proof[i]), 'hex')) } return hasher.digest().slice(0,32).toString('hex').toUpperCase() } const verify_proof = (root_hash, proof) => { if (typeof(root_hash) != 'string' || typeof(proof) != 'object') return false return root_hash.toUpperCase() == hash_proof(proof) } const proof_contains = (proof, tx_blob, meta, already_computed = false) => { if (proof === undefined) return false const hash = (already_computed ? already_computed : hash_txn_and_meta(tx_blob, meta)) for (let i = 0; i < 16; ++i) { if (proof[i] === undefined) return false if (proof[i] == hash) return true if (typeof(proof[i]) == 'object' && proof_contains(proof[i], null, null, hash)) return true } return false }