xrpl
Version:
A TypeScript/JavaScript API for interacting with the XRP Ledger in Node.js and the browser
244 lines (209 loc) • 6.9 kB
text/typescript
/* eslint-disable @typescript-eslint/no-magic-numbers -- this file mimics
behavior in rippled. Magic numbers are used for lengths and conditions */
/* eslint-disable no-bitwise -- this file mimics behavior in rippled. It uses
bitwise operators for and-ing numbers with a mask and bit shifting. */
import { bytesToHex } from '@xrplf/isomorphic/utils'
import BigNumber from 'bignumber.js'
import { decode, encode } from 'ripple-binary-codec'
import { ValidationError, XrplError } from '../../errors'
import { APIVersion } from '../../models'
import { LedgerEntry } from '../../models/ledger'
import { LedgerVersionMap } from '../../models/ledger/Ledger'
import { Transaction, TransactionMetadata } from '../../models/transactions'
import { GlobalFlags } from '../../models/transactions/common'
import { hasFlag } from '../../models/utils'
import HashPrefix from './HashPrefix'
import sha512Half from './sha512Half'
import SHAMap, { NodeType } from './SHAMap'
const HEX = 16
interface HashLedgerHeaderOptions {
computeTreeHashes?: boolean
}
function intToHex(integer: number, byteLength: number): string {
const foo = Number(integer)
.toString(HEX)
.padStart(byteLength * 2, '0')
return foo
}
function bigintToHex(
integerString: string | number | BigNumber,
byteLength: number,
): string {
const hex = new BigNumber(integerString).toString(HEX)
return hex.padStart(byteLength * 2, '0')
}
function addLengthPrefix(hex: string): string {
const length = hex.length / 2
if (length <= 192) {
return bytesToHex([length]) + hex
}
if (length <= 12480) {
const prefix = length - 193
return bytesToHex([193 + (prefix >>> 8), prefix & 0xff]) + hex
}
if (length <= 918744) {
const prefix = length - 12481
return (
bytesToHex([
241 + (prefix >>> 16),
(prefix >>> 8) & 0xff,
prefix & 0xff,
]) + hex
)
}
throw new XrplError('Variable integer overflow.')
}
/**
* Hashes the Transaction object as the ledger does. Throws if the transaction is unsigned.
*
* @param tx - A transaction to hash. Tx may be in binary blob form. Tx must be signed.
* @returns A hash of tx.
* @throws ValidationError if the Transaction is unsigned.
* @category Utilities
*/
export function hashSignedTx(tx: Transaction | string): string {
let txBlob: string
let txObject: Transaction
if (typeof tx === 'string') {
txBlob = tx
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Required until updated in binary codec. */
txObject = decode(tx) as unknown as Transaction
} else {
txBlob = encode(tx)
txObject = tx
}
if (
txObject.TxnSignature === undefined &&
txObject.Signers === undefined &&
txObject.SigningPubKey === undefined &&
!hasFlag(txObject, GlobalFlags.tfInnerBatchTxn, 'tfInnerBatchTxn')
) {
throw new ValidationError('The transaction must be signed to hash it.')
}
const prefix = HashPrefix.TRANSACTION_ID.toString(16).toUpperCase()
return sha512Half(prefix.concat(txBlob))
}
/**
* Compute the hash of a ledger.
*
* @param ledgerHeader - Ledger to compute the hash of.
* @returns The hash of the ledger.
* @category Utilities
*/
export function hashLedgerHeader(
ledgerHeader: LedgerVersionMap<APIVersion>,
): string {
const prefix = HashPrefix.LEDGER.toString(HEX).toUpperCase()
const ledger =
prefix +
intToHex(Number(ledgerHeader.ledger_index), 4) +
bigintToHex(ledgerHeader.total_coins, 8) +
ledgerHeader.parent_hash +
ledgerHeader.transaction_hash +
ledgerHeader.account_hash +
intToHex(ledgerHeader.parent_close_time, 4) +
intToHex(ledgerHeader.close_time, 4) +
intToHex(ledgerHeader.close_time_resolution, 1) +
intToHex(ledgerHeader.close_flags, 1)
return sha512Half(ledger)
}
/**
* Compute the root hash of the SHAMap containing all transactions.
*
* @param transactions - List of Transactions.
* @returns The root hash of the SHAMap.
* @category Utilities
*/
export function hashTxTree(
transactions: Array<Transaction & { metaData?: TransactionMetadata }>,
): string {
const shamap = new SHAMap()
for (const txJSON of transactions) {
const txBlobHex = encode(txJSON)
const metaHex = encode(txJSON.metaData ?? {})
const txHash = hashSignedTx(txBlobHex)
const data = addLengthPrefix(txBlobHex) + addLengthPrefix(metaHex)
shamap.addItem(txHash, data, NodeType.TRANSACTION_METADATA)
}
return shamap.hash
}
/**
* Compute the state hash of a list of LedgerEntries.
*
* @param entries - List of LedgerEntries.
* @returns Hash of SHAMap that consists of all entries.
* @category Utilities
*/
export function hashStateTree(entries: LedgerEntry[]): string {
const shamap = new SHAMap()
entries.forEach((ledgerEntry) => {
const data = encode(ledgerEntry)
shamap.addItem(ledgerEntry.index, data, NodeType.ACCOUNT_STATE)
})
return shamap.hash
}
function computeTransactionHash(
ledger: LedgerVersionMap<APIVersion>,
options: HashLedgerHeaderOptions,
): string {
const { transaction_hash } = ledger
if (!options.computeTreeHashes) {
return transaction_hash
}
if (ledger.transactions == null) {
throw new ValidationError('transactions is missing from the ledger')
}
const transactionHash = hashTxTree(ledger.transactions)
if (transaction_hash !== transactionHash) {
throw new ValidationError(
'transactionHash in header' +
' does not match computed hash of transactions',
{
transactionHashInHeader: transaction_hash,
computedHashOfTransactions: transactionHash,
},
)
}
return transactionHash
}
function computeStateHash(
ledger: LedgerVersionMap<APIVersion>,
options: HashLedgerHeaderOptions,
): string {
const { account_hash } = ledger
if (!options.computeTreeHashes) {
return account_hash
}
if (ledger.accountState == null) {
throw new ValidationError('accountState is missing from the ledger')
}
const stateHash = hashStateTree(ledger.accountState)
if (account_hash !== stateHash) {
throw new ValidationError(
'stateHash in header does not match computed hash of state',
)
}
return stateHash
}
/**
* Compute the hash of a ledger.
*
* @param ledger - Ledger to compute the hash for.
* @param options - Allow client to recompute Transaction and State Hashes.
* @param options.computeTreeHashes - Whether to recompute the Transaction and State Hashes.
* @returns The has of ledger.
* @category Utilities
*/
function hashLedger(
ledger: LedgerVersionMap<APIVersion>,
options: {
computeTreeHashes?: boolean
} = {},
): string {
const subhashes = {
transaction_hash: computeTransactionHash(ledger, options),
account_hash: computeStateHash(ledger, options),
}
return hashLedgerHeader({ ...ledger, ...subhashes })
}
export default hashLedger