ox
Version:
Ethereum Standard Library
755 lines (688 loc) • 21.5 kB
text/typescript
import * as BlobCells from './BlobCells.js'
import * as Bytes from './Bytes.js'
import * as Errors from './Errors.js'
import * as Hash from './Hash.js'
import * as Hex from './Hex.js'
import * as Cursor from './internal/cursor.js'
import type { Compute } from './internal/types.js'
import * as Kzg from './Kzg.js'
/** Blob limit per transaction. */
const blobsPerTransaction = 6
/** The number of bytes in a BLS scalar field element. */
export const bytesPerFieldElement = 32
/** The number of field elements in a blob. */
export const fieldElementsPerBlob = 4096
/** The number of bytes in a blob. */
export const bytesPerBlob = bytesPerFieldElement * fieldElementsPerBlob
/** Blob bytes limit per transaction. */
export const maxBytesPerTransaction =
bytesPerBlob * blobsPerTransaction -
// terminator byte (0x80).
1 -
// zero byte (0x00) appended to each field element.
1 * fieldElementsPerBlob * blobsPerTransaction
/** Root type for a Blob. */
export type Blob<type extends Hex.Hex | Bytes.Bytes = Hex.Hex | Bytes.Bytes> =
type
/** A list of {@link ox#Blobs.Blob}. */
export type Blobs<type extends Hex.Hex | Bytes.Bytes = Hex.Hex | Bytes.Bytes> =
readonly Blob<type>[]
/**
* Transform a list of Commitments to Blob Versioned Hashes.
*
* @example
* ```ts twoslash
* // @noErrors
* import { Blobs } from 'ox'
* import { kzg } from './kzg'
*
* const blobs = Blobs.from('0xdeadbeef')
* const commitments = Blobs.toCommitments(blobs, { kzg })
* const versionedHashes =
* Blobs.commitmentsToVersionedHashes(commitments) // [!code focus]
* // @log: ['0x...', '0x...']
* ```
*
* @example
* ### Configuring Return Type
*
* It is possible to configure the return type for the Versioned Hashes with the `as` option.
*
* ```ts twoslash
* // @noErrors
* import { Blobs } from 'ox'
* import { kzg } from './kzg'
*
* const blobs = Blobs.from('0xdeadbeef')
* const commitments = Blobs.toCommitments(blobs, { kzg })
* const versionedHashes = Blobs.commitmentsToVersionedHashes(
* commitments,
* {
* as: 'Bytes' // [!code focus]
* }
* )
* // @log: [Uint8Array [ ... ], Uint8Array [ ... ]]
* ```
*
* @example
* ### Versioning Hashes
*
* It is possible to configure the version for the Versioned Hashes with the `version` option.
*
* ```ts twoslash
* // @noErrors
* import { Blobs } from 'ox'
* import { kzg } from './kzg'
*
* const blobs = Blobs.from('0xdeadbeef')
* const commitments = Blobs.toCommitments(blobs, { kzg })
* const versionedHashes = Blobs.commitmentsToVersionedHashes(
* commitments,
* {
* version: 2 // [!code focus]
* }
* )
* ```
*
* @param commitments - A list of commitments.
* @param options - Options.
* @returns A list of Blob Versioned Hashes.
*/
export function commitmentsToVersionedHashes<
const commitments extends readonly Bytes.Bytes[] | readonly Hex.Hex[],
as extends 'Hex' | 'Bytes' =
| (commitments extends readonly Hex.Hex[] ? 'Hex' : never)
| (commitments extends readonly Bytes.Bytes[] ? 'Bytes' : never),
>(
commitments: commitments | readonly Bytes.Bytes[] | readonly Hex.Hex[],
options: commitmentsToVersionedHashes.Options<as> = {},
): commitmentsToVersionedHashes.ReturnType<as> {
const { version } = options
const as =
options.as ?? (typeof commitments[0] === 'string' ? 'Hex' : 'Bytes')
const hashes: Uint8Array[] | Hex.Hex[] = []
for (const commitment of commitments) {
hashes.push(
commitmentToVersionedHash(commitment, {
as,
version,
}) as never,
)
}
return hashes as never
}
export declare namespace commitmentsToVersionedHashes {
type Options<as extends 'Hex' | 'Bytes' | undefined = undefined> = {
/** Return type. */
as?: as | 'Hex' | 'Bytes' | undefined
/** Version to tag onto the hashes. */
version?: number | undefined
}
type ReturnType<as extends 'Hex' | 'Bytes' = 'Hex' | 'Bytes'> =
| (as extends 'Bytes' ? readonly Bytes.Bytes[] : never)
| (as extends 'Hex' ? readonly Hex.Hex[] : never)
type ErrorType = Errors.GlobalErrorType
}
/**
* Transform a Commitment to its Blob Versioned Hash.
*
* @example
* ```ts twoslash
* // @noErrors
* import { Blobs } from 'ox'
* import { kzg } from './kzg'
*
* const blobs = Blobs.from('0xdeadbeef')
* const [commitment] = Blobs.toCommitments(blobs, { kzg })
* const versionedHash =
* Blobs.commitmentToVersionedHash(commitment) // [!code focus]
* ```
*
* @example
* ### Configuring Return Type
*
* It is possible to configure the return type for the Versioned Hash with the `as` option.
*
* ```ts twoslash
* // @noErrors
* import { Blobs } from 'ox'
* import { kzg } from './kzg'
*
* const blobs = Blobs.from('0xdeadbeef')
* const [commitment] = Blobs.toCommitments(blobs, { kzg })
* const versionedHashes = Blobs.commitmentToVersionedHash(
* commitment,
* {
* as: 'Bytes' // [!code focus]
* }
* )
* // @log: [Uint8Array [ ... ], Uint8Array [ ... ]]
* ```
*
* @example
* ### Versioning Hashes
*
* It is possible to configure the version for the Versioned Hash with the `version` option.
*
* ```ts twoslash
* // @noErrors
* import { Blobs } from 'ox'
* import { kzg } from './kzg'
*
* const blobs = Blobs.from('0xdeadbeef')
* const [commitment] = Blobs.toCommitments(blobs, { kzg })
* const versionedHashes = Blobs.commitmentToVersionedHash(
* commitment,
* {
* version: 2 // [!code focus]
* }
* )
* ```
*
* @param commitment - The commitment.
* @param options - Options.
* @returns The Blob Versioned Hash.
*/
export function commitmentToVersionedHash<
const commitment extends Hex.Hex | Bytes.Bytes,
as extends 'Hex' | 'Bytes' =
| (commitment extends Hex.Hex ? 'Hex' : never)
| (commitment extends Bytes.Bytes ? 'Bytes' : never),
>(
commitment: commitment | Hex.Hex | Bytes.Bytes,
options: commitmentToVersionedHash.Options<as> = {},
): commitmentToVersionedHash.ReturnType<as> {
const { version = 1 } = options
const as = options.as ?? (typeof commitment === 'string' ? 'Hex' : 'Bytes')
const versionedHash = Hash.sha256(commitment, { as: 'Bytes' })
versionedHash.set([version], 0)
return (
as === 'Bytes' ? versionedHash : Hex.fromBytes(versionedHash)
) as commitmentToVersionedHash.ReturnType<as>
}
export declare namespace commitmentToVersionedHash {
type Options<as extends 'Hex' | 'Bytes' | undefined = undefined> = {
/** Return type. */
as?: as | 'Hex' | 'Bytes' | undefined
/** Version to tag onto the hash. */
version?: number | undefined
}
type ReturnType<as extends 'Hex' | 'Bytes' = 'Hex' | 'Bytes'> =
| (as extends 'Bytes' ? Bytes.Bytes : never)
| (as extends 'Hex' ? Hex.Hex : never)
type ErrorType = Errors.GlobalErrorType
}
/**
* Transforms arbitrary data to {@link ox#Blobs.Blobs}.
*
* @example
* ```ts twoslash
* import { Blobs } from 'ox'
*
* const blobs = Blobs.from('0xdeadbeef')
* ```
*
* @example
* ### Creating Blobs from a String
*
* An example of creating Blobs from a string using {@link ox#Hex.(from:function)}:
*
* ```ts twoslash
* import { Blobs, Hex } from 'ox'
*
* const blobs = Blobs.from(Hex.fromString('Hello world!'))
* ```
*
* @example
* ### Configuring Return Type
*
* It is possible to configure the return type for the Blobs with the `as` option.
*
* ```ts twoslash
* import { Blobs } from 'ox'
*
* const blobs = Blobs.from('0xdeadbeef', { as: 'Bytes' })
* // ^?
* ```
*
* @param data - The data to convert to {@link ox#Blobs.Blobs}.
* @param options - Options.
* @returns The {@link ox#Blobs.Blobs}.
*/
export function from<
const data extends Hex.Hex | Bytes.Bytes,
as extends 'Hex' | 'Bytes' =
| (data extends Hex.Hex ? 'Hex' : never)
| (data extends Bytes.Bytes ? 'Bytes' : never),
>(
data: data | Hex.Hex | Bytes.Bytes,
options: from.Options<as> = {},
): from.ReturnType<as> {
const as = options.as ?? (typeof data === 'string' ? 'Hex' : 'Bytes')
const data_ = (
typeof data === 'string' ? Bytes.fromHex(data) : data
) as Bytes.Bytes
const size_ = Bytes.size(data_)
if (!size_) throw new EmptyBlobError()
if (size_ > maxBytesPerTransaction)
throw new BlobSizeTooLargeError({
maxSize: maxBytesPerTransaction,
size: size_,
})
const blobs = []
let active = true
let position = 0
while (active) {
const blob = Cursor.create(new Uint8Array(bytesPerBlob))
let size = 0
while (size < fieldElementsPerBlob) {
const bytes = data_.slice(position, position + (bytesPerFieldElement - 1))
// Push a zero byte so the field element doesn't overflow the BLS modulus.
blob.pushByte(0x00)
// Push the current segment of data bytes.
blob.pushBytes(bytes)
// If we detect that the current segment of data bytes is less than 31 bytes,
// we can stop processing and push a terminator byte to indicate the end of the blob.
if (bytes.length < 31) {
blob.pushByte(0x80)
active = false
break
}
size++
position += 31
}
blobs.push(blob)
}
return (
as === 'Bytes'
? blobs.map((x) => x.bytes)
: blobs.map((x) => Hex.fromBytes(x.bytes))
) as never
}
export declare namespace from {
type Options<as extends 'Hex' | 'Bytes' | undefined = undefined> = {
/** Return type. */
as?: as | 'Hex' | 'Bytes' | undefined
}
type ReturnType<as extends 'Hex' | 'Bytes' = 'Hex' | 'Bytes'> =
| (as extends 'Bytes' ? readonly Bytes.Bytes[] : never)
| (as extends 'Hex' ? readonly Hex.Hex[] : never)
type ErrorType =
| BlobSizeTooLargeError
| EmptyBlobError
| Bytes.fromHex.ErrorType
| Hex.fromBytes.ErrorType
| Cursor.create.ErrorType
| Bytes.size.ErrorType
| Errors.GlobalErrorType
}
/**
* Transforms Ox-shaped {@link ox#Blobs.Blobs} into the originating data.
*
* @example
* ```ts twoslash
* import { Blobs, Hex } from 'ox'
*
* const blobs = Blobs.from('0xdeadbeef')
* const data = Blobs.to(blobs) // [!code focus]
* // @log: '0xdeadbeef'
* ```
*
* @example
* ### Configuring Return Type
*
* It is possible to configure the return type with second argument.
*
* ```ts twoslash
* import { Blobs } from 'ox'
*
* const blobs = Blobs.from('0xdeadbeef')
* const data = Blobs.to(blobs, 'Bytes')
* // @log: Uint8Array [ 13, 174, 190, 239 ]
* ```
*
* @param blobs - The {@link ox#Blobs.Blobs} to transform.
* @param to - The type to transform to.
* @returns The originating data.
*/
export function to<
const blobs extends Blobs<Hex.Hex> | Blobs<Bytes.Bytes>,
to extends 'Hex' | 'Bytes' =
| (blobs extends Blobs<Hex.Hex> ? 'Hex' : never)
| (blobs extends Blobs<Bytes.Bytes> ? 'Bytes' : never),
>(
blobs: blobs | Blobs<Hex.Hex> | Blobs<Bytes.Bytes>,
to?: to | 'Hex' | 'Bytes',
): to.ReturnType<to> {
const to_ = to ?? (typeof blobs[0] === 'string' ? 'Hex' : 'Bytes')
const blobs_ = (
typeof blobs[0] === 'string'
? blobs.map((x) => Bytes.fromHex(x as Hex.Hex))
: blobs
) as Bytes.Bytes[]
const length = blobs_.reduce((length, blob) => length + blob.length, 0)
const data = Cursor.create(new Uint8Array(length))
let active = true
for (const blob of blobs_) {
const cursor = Cursor.create(blob)
while (active && cursor.position < blob.length) {
// First byte will be a zero 0x00 byte – we can skip.
cursor.incrementPosition(1)
let consume = 31
if (blob.length - cursor.position < 31)
consume = blob.length - cursor.position
for (let i = 0; i < consume; i++) {
const byte = cursor.readByte()
const isTerminator =
byte === 0x80 && !cursor.inspectBytes(cursor.remaining).includes(0x80)
if (isTerminator) {
active = false
break
}
data.pushByte(byte)
}
}
}
const trimmedData = data.bytes.slice(0, data.position)
return (to_ === 'Hex' ? Hex.fromBytes(trimmedData) : trimmedData) as never
}
export declare namespace to {
type ReturnType<to extends 'Hex' | 'Bytes' = 'Hex'> =
| (to extends 'Bytes' ? Bytes.Bytes : never)
| (to extends 'Hex' ? Hex.Hex : never)
type ErrorType =
| Hex.fromBytes.ErrorType
| Bytes.fromHex.ErrorType
| Cursor.create.ErrorType
| Errors.GlobalErrorType
}
/**
* Transforms Ox-shaped {@link ox#Blobs.Blobs} into the originating data.
*
* @example
* ```ts twoslash
* import { Blobs, Hex } from 'ox'
*
* const blobs = Blobs.from('0xdeadbeef')
* const data = Blobs.toHex(blobs) // [!code focus]
* // @log: '0xdeadbeef'
* ```
*/
export function toHex(
blobs: Blobs<Hex.Hex> | Blobs<Bytes.Bytes>,
): toHex.ReturnType {
return to(blobs, 'Hex')
}
export declare namespace toHex {
type ReturnType = to.ReturnType<'Hex'>
type ErrorType = to.ErrorType | Errors.GlobalErrorType
}
/**
* Transforms Ox-shaped {@link ox#Blobs.Blobs} into the originating data.
*
* @example
* ```ts
* import { Blobs, Hex } from 'ox'
*
* const blobs = Blobs.from('0xdeadbeef')
* const data = Blobs.toBytes(blobs) // [!code focus]
* // @log: Uint8Array [ 13, 174, 190, 239 ]
* ```
*/
export function toBytes(
blobs: Blobs<Hex.Hex> | Blobs<Bytes.Bytes>,
): toBytes.ReturnType {
return to(blobs, 'Bytes')
}
/**
* Compute commitments from a list of {@link ox#Blobs.Blobs}.
*
* @example
* ```ts twoslash
* // @noErrors
* import { Blobs } from 'ox'
* import { kzg } from './kzg'
*
* const blobs = Blobs.from('0xdeadbeef')
* const commitments = Blobs.toCommitments(blobs, { kzg }) // [!code focus]
* ```
*
* @example
* ### Configuring Return Type
*
* It is possible to configure the return type with the `as` option.
*
* ```ts twoslash
* // @noErrors
* import { Blobs } from 'ox'
* import { kzg } from './kzg'
*
* const blobs = Blobs.from('0xdeadbeef')
* const commitments = Blobs.toCommitments(blobs, {
* as: 'Bytes', // [!code focus]
* kzg
* })
* // @log: [Uint8Array [ ... ], Uint8Array [ ... ]]
* ```
*
* @param blobs - The {@link ox#Blobs.Blobs} to transform to commitments.
* @param options - Options.
* @returns The commitments.
*/
export function toCommitments<
const blobs extends Blobs<Bytes.Bytes> | Blobs<Hex.Hex>,
as extends 'Hex' | 'Bytes' =
| (blobs extends Blobs<Hex.Hex> ? 'Hex' : never)
| (blobs extends Blobs<Bytes.Bytes> ? 'Bytes' : never),
>(
blobs: blobs | Blobs<Bytes.Bytes> | Blobs<Hex.Hex>,
options: toCommitments.Options<as>,
): toCommitments.ReturnType<as> {
const { kzg } = options
const as = options.as ?? (typeof blobs[0] === 'string' ? 'Hex' : 'Bytes')
const blobs_ = (
typeof blobs[0] === 'string'
? blobs.map((x) => Bytes.fromHex(x as any))
: blobs
) as Bytes.Bytes[]
const commitments: Bytes.Bytes[] = []
for (const blob of blobs_)
commitments.push(Uint8Array.from(kzg.blobToKzgCommitment(blob)))
return (
as === 'Bytes' ? commitments : commitments.map((x) => Hex.fromBytes(x))
) as never
}
export declare namespace toCommitments {
type Options<as extends 'Hex' | 'Bytes' = 'Hex'> = {
/** KZG implementation. */
kzg: Pick<Kzg.Kzg, 'blobToKzgCommitment'>
/** Return type. */
as?: as | 'Hex' | 'Bytes' | undefined
}
type ReturnType<as extends 'Hex' | 'Bytes' = 'Hex'> = Compute<
| (as extends 'Bytes' ? readonly Bytes.Bytes[] : never)
| (as extends 'Hex' ? readonly Hex.Hex[] : never)
>
type ErrorType =
| Bytes.fromHex.ErrorType
| Hex.fromBytes.ErrorType
| Errors.GlobalErrorType
}
export declare namespace toBytes {
type ReturnType = to.ReturnType<'Bytes'>
type ErrorType = to.ErrorType | Errors.GlobalErrorType
}
/**
* Compute the flat list of PeerDAS (EIP-7594) cell KZG proofs for a list of
* {@link ox#Blobs.Blobs}. Returns `128 * blobs.length` proofs, where
* `proofs[i * 128 + j]` is the proof for cell `j` of `blobs[i]`'s extended
* form.
*
* @example
* ```ts twoslash
* // @noErrors
* import { Blobs } from 'ox'
* import { kzg } from './kzg'
*
* const blobs = Blobs.from('0xdeadbeef')
* const cellProofs = Blobs.toCellProofs(blobs, { kzg }) // [!code focus]
* ```
*
* @example
* ### Configuring Return Type
*
* It is possible to configure the return type with the `as` option.
*
* ```ts twoslash
* // @noErrors
* import { Blobs } from 'ox'
* import { kzg } from './kzg'
*
* const blobs = Blobs.from('0xdeadbeef')
* const cellProofs = Blobs.toCellProofs(blobs, {
* as: 'Bytes', // [!code focus]
* kzg
* })
* // @log: [Uint8Array [ ... ], Uint8Array [ ... ], ...]
* ```
*
* @param blobs - The {@link ox#Blobs.Blobs} to transform to cell proofs.
* @param options - Options.
* @returns The flat list of cell KZG proofs.
*/
export function toCellProofs<
const blobs extends Blobs<Bytes.Bytes> | Blobs<Hex.Hex>,
as extends 'Hex' | 'Bytes' =
| (blobs extends Blobs<Hex.Hex> ? 'Hex' : never)
| (blobs extends Blobs<Bytes.Bytes> ? 'Bytes' : never),
>(
blobs: blobs | Blobs<Bytes.Bytes> | Blobs<Hex.Hex>,
options: toCellProofs.Options<as>,
): toCellProofs.ReturnType<as> {
const { kzg } = options
const as = options.as ?? (typeof blobs[0] === 'string' ? 'Hex' : 'Bytes')
if (as === 'Bytes') {
const proofs: Bytes.Bytes[] = []
for (const blob of blobs) {
const { proofs: cellProofs } = BlobCells.fromBlob(
blob as Hex.Hex | Bytes.Bytes,
{ kzg, as: 'Bytes' },
)
for (const proof of cellProofs) proofs.push(proof)
}
return proofs as never
}
const proofs: Hex.Hex[] = []
for (const blob of blobs) {
const { proofs: cellProofs } = BlobCells.fromBlob(
blob as Hex.Hex | Bytes.Bytes,
{ kzg, as: 'Hex' },
)
for (const proof of cellProofs) proofs.push(proof)
}
return proofs as never
}
export declare namespace toCellProofs {
type Options<as extends 'Hex' | 'Bytes' = 'Hex'> = {
/** KZG implementation. */
kzg: Pick<Kzg.Kzg, 'computeCellsAndKzgProofs'>
/** Return type. */
as?: as | 'Hex' | 'Bytes' | undefined
}
type ReturnType<as extends 'Hex' | 'Bytes' = 'Hex'> = Compute<
| (as extends 'Bytes' ? readonly Bytes.Bytes[] : never)
| (as extends 'Hex' ? readonly Hex.Hex[] : never)
>
type ErrorType = BlobCells.fromBlob.ErrorType | Errors.GlobalErrorType
}
/**
* Compute Blob Versioned Hashes from a list of {@link ox#Blobs.Blobs}.
*
* @example
* ```ts twoslash
* // @noErrors
* import { Blobs } from 'ox'
* import { kzg } from './kzg'
*
* const blobs = Blobs.from('0xdeadbeef')
* const versionedHashes = Blobs.toVersionedHashes(blobs, {
* kzg
* }) // [!code focus]
* ```
*
* @param blobs - The {@link ox#Blobs.Blobs} to transform into Blob Versioned Hashes.
* @param options - Options.
* @returns The Blob Versioned Hashes.
*/
export function toVersionedHashes<
const blobs extends Blobs<Bytes.Bytes> | Blobs<Hex.Hex>,
as extends 'Hex' | 'Bytes' =
| (blobs extends Blobs<Hex.Hex> ? 'Hex' : never)
| (blobs extends Blobs<Bytes.Bytes> ? 'Bytes' : never),
>(
blobs: blobs | Blobs<Bytes.Bytes> | Blobs<Hex.Hex>,
options: toVersionedHashes.Options<as>,
): toVersionedHashes.ReturnType<as> {
const commitments = toCommitments(blobs, options)
return commitmentsToVersionedHashes(commitments, options)
}
export declare namespace toVersionedHashes {
type Options<as extends 'Hex' | 'Bytes' = 'Hex'> = {
/** KZG implementation. */
kzg: Pick<Kzg.Kzg, 'blobToKzgCommitment'>
/** Return type. */
as?: as | 'Hex' | 'Bytes' | undefined
}
type ReturnType<as extends 'Hex' | 'Bytes' = 'Hex'> = Compute<
| (as extends 'Bytes' ? readonly Bytes.Bytes[] : never)
| (as extends 'Hex' ? readonly Hex.Hex[] : never)
>
type ErrorType =
| toCommitments.ErrorType
| commitmentsToVersionedHashes.ErrorType
| Errors.GlobalErrorType
}
/** Thrown when the blob size is too large. */
export class BlobSizeTooLargeError extends Errors.BaseError {
override readonly name = 'Blobs.BlobSizeTooLargeError'
constructor({ maxSize, size }: { maxSize: number; size: number }) {
super('Blob size is too large.', {
metaMessages: [`Max: ${maxSize} bytes`, `Given: ${size} bytes`],
})
}
}
/** Thrown when the blob is empty. */
export class EmptyBlobError extends Errors.BaseError {
override readonly name = 'Blobs.EmptyBlobError'
constructor() {
super('Blob data must not be empty.')
}
}
/** Thrown when the blob versioned hashes are empty. */
export class EmptyBlobVersionedHashesError extends Errors.BaseError {
override readonly name = 'Blobs.EmptyBlobVersionedHashesError'
constructor() {
super('Blob versioned hashes must not be empty.')
}
}
/** Thrown when the blob versioned hash size is invalid. */
export class InvalidVersionedHashSizeError extends Errors.BaseError {
override readonly name = 'Blobs.InvalidVersionedHashSizeError'
constructor({ hash, size }: { hash: Hex.Hex; size: number }) {
super(`Versioned hash "${hash}" size is invalid.`, {
metaMessages: ['Expected: 32', `Received: ${size}`],
})
}
}
/** Thrown when the blob versioned hash version is invalid. */
export class InvalidVersionedHashVersionError extends Errors.BaseError {
override readonly name = 'Blobs.InvalidVersionedHashVersionError'
constructor({ hash, version }: { hash: Hex.Hex; version: number }) {
super(`Versioned hash "${hash}" version is invalid.`, {
metaMessages: [
`Expected: ${Kzg.versionedHashVersion}`,
`Received: ${version}`,
],
})
}
}