diff --git a/package.json b/package.json
index 387e314afd..bb770d3c9d 100644
--- a/package.json
+++ b/package.json
@@ -93,7 +93,7 @@
"url": "https://www.buymeacoffee.com/ricmoo"
}
],
- "gitHead": "556fdd91d9b6bf7db4041bb099e66b2080e1a985",
+ "gitHead": "12772e9498b70f8538838f30e16f3792ea90e173",
"homepage": "https://ethers.org",
"keywords": [
"ethereum",
@@ -106,7 +106,7 @@
"name": "ethers",
"publishConfig": {
"access": "public",
- "tag": "latest"
+ "tag": "next"
},
"repository": {
"type": "git",
@@ -131,5 +131,5 @@
"test-esm": "mocha --trace-warnings --reporter ./reporter.cjs ./lib.esm/_tests/test-*.js"
},
"sideEffects": false,
- "version": "6.11.1"
+ "version": "6.12.0-beta.1"
}
diff --git a/src.ts/_version.ts b/src.ts/_version.ts
index 41b134f1a9..35dd765723 100644
--- a/src.ts/_version.ts
+++ b/src.ts/_version.ts
@@ -3,4 +3,4 @@
/**
* The current version of Ethers.
*/
-export const version: string = "6.11.1";
+export const version: string = "6.12.0-beta.1";
diff --git a/src.ts/ethers.ts b/src.ts/ethers.ts
index 1f5c54d8c5..a02a9817b7 100644
--- a/src.ts/ethers.ts
+++ b/src.ts/ethers.ts
@@ -177,6 +177,7 @@ export type {
export type {
AccessList, AccessListish, AccessListEntry,
+ Blob, BlobLike, KzgLibrary,
TransactionLike
} from "./transaction/index.js";
diff --git a/src.ts/providers/abstract-signer.ts b/src.ts/providers/abstract-signer.ts
index afd9aa223e..3087624d85 100644
--- a/src.ts/providers/abstract-signer.ts
+++ b/src.ts/providers/abstract-signer.ts
@@ -194,8 +194,8 @@ export abstract class AbstractSigner
+
+ /**
+ * The maximum fee per blob gas (see [[link-eip-4844]]).
+ */
+ maxFeePerBlobGas?: null | BigNumberish;
+
+ /**
+ * Any blobs to include in the transaction (see [[link-eip-4844]]).
+ */
+ blobs?: null | Array;
+
+ /**
+ * An external library for computing the KZG commitments and
+ * proofs necessary for EIP-4844 transactions (see [[link-eip-4844]]).
+ *
+ * This is generally ``null``, unless you are creating BLOb
+ * transactions.
+ */
+ kzg?: null | KzgLibrary;
+
// Todo?
//gasMultiplier?: number;
};
@@ -332,7 +359,7 @@ export function copyRequest(req: TransactionRequest): PreparedTransactionRequest
if (req.data) { result.data = hexlify(req.data); }
- const bigIntKeys = "chainId,gasLimit,gasPrice,maxFeePerGas,maxPriorityFeePerGas,value".split(/,/);
+ const bigIntKeys = "chainId,gasLimit,gasPrice,maxFeePerBlobGas,maxFeePerGas,maxPriorityFeePerGas,value".split(/,/);
for (const key of bigIntKeys) {
if (!(key in req) || (req)[key] == null) { continue; }
result[key] = getBigInt((req)[key], `request.${ key }`);
@@ -358,6 +385,19 @@ export function copyRequest(req: TransactionRequest): PreparedTransactionRequest
result.customData = req.customData;
}
+ if ("blobVersionedHashes" in req && req.blobVersionedHashes) {
+ result.blobVersionedHashes = req.blobVersionedHashes.slice();
+ }
+
+ if ("kzg" in req) { result.kzg = req.kzg; }
+
+ if ("blobs" in req && req.blobs) {
+ result.blobs = req.blobs.map((b) => {
+ if (isBytesLike(b)) { return hexlify(b); }
+ return Object.assign({ }, b);
+ });
+ }
+
return result;
}
diff --git a/src.ts/transaction/index.ts b/src.ts/transaction/index.ts
index a67c044deb..d67055f6db 100644
--- a/src.ts/transaction/index.ts
+++ b/src.ts/transaction/index.ts
@@ -28,4 +28,6 @@ export { accessListify } from "./accesslist.js";
export { computeAddress, recoverAddress } from "./address.js";
export { Transaction } from "./transaction.js";
-export type { TransactionLike } from "./transaction.js";
+export type {
+ Blob, BlobLike, KzgLibrary, TransactionLike
+} from "./transaction.js";
diff --git a/src.ts/transaction/transaction.ts b/src.ts/transaction/transaction.ts
index b9ec51d75c..1413902aad 100644
--- a/src.ts/transaction/transaction.ts
+++ b/src.ts/transaction/transaction.ts
@@ -1,10 +1,12 @@
import { getAddress } from "../address/index.js";
import { ZeroAddress } from "../constants/addresses.js";
-import { keccak256, Signature, SigningKey } from "../crypto/index.js";
+import {
+ keccak256, sha256, Signature, SigningKey
+} from "../crypto/index.js";
import {
concat, decodeRlp, encodeRlp, getBytes, getBigInt, getNumber, hexlify,
- assert, assertArgument, isHexString, toBeArray, zeroPadValue
+ assert, assertArgument, isBytesLike, isHexString, toBeArray, zeroPadValue
} from "../utils/index.js";
import { accessListify } from "./accesslist.js";
@@ -23,6 +25,7 @@ const BN_28 = BigInt(28)
const BN_35 = BigInt(35);
const BN_MAX_UINT = BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
+const BLOB_SIZE = 4096 * 32;
/**
* A **TransactionLike** is an object which is appropriate as a loose
@@ -109,6 +112,61 @@ export interface TransactionLike {
* The versioned hashes (see [[link-eip-4844]]).
*/
blobVersionedHashes?: null | Array;
+
+ /**
+ * The blobs (if any) attached to this transaction (see [[link-eip-4844]]).
+ */
+ blobs?: null | Array
+
+ /**
+ * An external library for computing the KZG commitments and
+ * proofs necessary for EIP-4844 transactions (see [[link-eip-4844]]).
+ *
+ * This is generally ``null``, unless you are creating BLOb
+ * transactions.
+ */
+ kzg?: null | KzgLibrary;
+}
+
+/**
+ * A full-valid BLOb object for [[link-eip-4844]] transactions.
+ *
+ * The commitment and proof should have been computed using a
+ * KZG library.
+ */
+export interface Blob {
+ data: string;
+ proof: string;
+ commitment: string;
+}
+
+/**
+ * A BLOb object that can be passed for [[link-eip-4844]]
+ * transactions.
+ *
+ * It may have had its commitment and proof already provided
+ * or rely on an attached [[KzgLibrary]] to compute them.
+ */
+export type BlobLike = BytesLike | {
+ data: BytesLike;
+ proof: BytesLike;
+ commitment: BytesLike;
+};
+
+/**
+ * A KZG Library with the necessary functions to compute
+ * BLOb commitments and proofs.
+ */
+export interface KzgLibrary {
+ blobToKzgCommitment: (blob: Uint8Array) => Uint8Array;
+ computeBlobKzgProof: (blob: Uint8Array, commitment: Uint8Array) => Uint8Array;
+}
+
+function getVersionedHash(version: number, hash: BytesLike): string {
+ let versioned = version.toString(16);
+ while (versioned.length < 2) { versioned = "0" + versioned; }
+ versioned += sha256(hash).substring(4);
+ return "0x" + versioned;
}
function handleAddress(value: string): null | string {
@@ -199,13 +257,13 @@ function _parseLegacy(data: Uint8Array): TransactionLike {
v
});
- tx.hash = keccak256(data);
+ //tx.hash = keccak256(data);
}
return tx;
}
-function _serializeLegacy(tx: Transaction, sig?: Signature): string {
+function _serializeLegacy(tx: Transaction, sig: null | Signature): string {
const fields: Array = [
formatNumber(tx.nonce, "nonce"),
formatNumber(tx.gasPrice || 0, "gasPrice"),
@@ -302,14 +360,14 @@ function _parseEip1559(data: Uint8Array): TransactionLike {
// Unsigned EIP-1559 Transaction
if (fields.length === 9) { return tx; }
- tx.hash = keccak256(data);
+ //tx.hash = keccak256(data);
_parseEipSignature(tx, fields.slice(9));
return tx;
}
-function _serializeEip1559(tx: Transaction, sig?: Signature): string {
+function _serializeEip1559(tx: Transaction, sig: null | Signature): string {
const fields: Array = [
formatNumber(tx.chainId, "chainId"),
formatNumber(tx.nonce, "nonce"),
@@ -352,14 +410,14 @@ function _parseEip2930(data: Uint8Array): TransactionLike {
// Unsigned EIP-2930 Transaction
if (fields.length === 8) { return tx; }
- tx.hash = keccak256(data);
+ //tx.hash = keccak256(data);
_parseEipSignature(tx, fields.slice(8));
return tx;
}
-function _serializeEip2930(tx: Transaction, sig?: Signature): string {
+function _serializeEip2930(tx: Transaction, sig: null | Signature): string {
const fields: any = [
formatNumber(tx.chainId, "chainId"),
formatNumber(tx.nonce, "nonce"),
@@ -381,10 +439,36 @@ function _serializeEip2930(tx: Transaction, sig?: Signature): string {
}
function _parseEip4844(data: Uint8Array): TransactionLike {
- const fields: any = decodeRlp(getBytes(data).slice(1));
+ let fields: any = decodeRlp(getBytes(data).slice(1));
+
+ let typeName = "3";
+
+ let blobs: null | Array = null;
+
+ // Parse the network format
+ if (fields.length === 4 && Array.isArray(fields[0])) {
+ typeName = "3 (network format)";
+ const fBlobs = fields[1], fCommits = fields[2], fProofs = fields[3];
+ assertArgument(Array.isArray(fBlobs), "invalid network format: blobs not an array", "fields[1]", fBlobs);
+ assertArgument(Array.isArray(fCommits), "invalid network format: commitments not an array", "fields[2]", fCommits);
+ assertArgument(Array.isArray(fProofs), "invalid network format: proofs not an array", "fields[3]", fProofs);
+ assertArgument(fBlobs.length === fCommits.length, "invalid network format: blobs/commitments length mismatch", "fields", fields);
+ assertArgument(fBlobs.length === fProofs.length, "invalid network format: blobs/proofs length mismatch", "fields", fields);
+
+ blobs = [ ];
+ for (let i = 0; i < fields[1].length; i++) {
+ blobs.push({
+ data: fBlobs[i],
+ commitment: fCommits[i],
+ proof: fProofs[i],
+ });
+ }
+
+ fields = fields[0];
+ }
assertArgument(Array.isArray(fields) && (fields.length === 11 || fields.length === 14),
- "invalid field count for transaction type: 3", "data", hexlify(data));
+ `invalid field count for transaction type: ${ typeName }`, "data", hexlify(data));
const tx: TransactionLike = {
type: 3,
@@ -402,7 +486,9 @@ function _parseEip4844(data: Uint8Array): TransactionLike {
blobVersionedHashes: fields[10]
};
- assertArgument(tx.to != null, "invalid address for transaction type: 3", "data", data);
+ if (blobs) { tx.blobs = blobs; }
+
+ assertArgument(tx.to != null, `invalid address for transaction type: ${ typeName }`, "data", data);
assertArgument(Array.isArray(tx.blobVersionedHashes), "invalid blobVersionedHashes: must be an array", "data", data);
for (let i = 0; i < tx.blobVersionedHashes.length; i++) {
@@ -412,14 +498,16 @@ function _parseEip4844(data: Uint8Array): TransactionLike {
// Unsigned EIP-4844 Transaction
if (fields.length === 11) { return tx; }
- tx.hash = keccak256(data);
+ // @TODO: Do we need to do this? This is only called internally
+ // and used to verify hashes; it might save time to not do this
+ //tx.hash = keccak256(concat([ "0x03", encodeRlp(fields) ]));
_parseEipSignature(tx, fields.slice(11));
return tx;
}
-function _serializeEip4844(tx: Transaction, sig?: Signature): string {
+function _serializeEip4844(tx: Transaction, sig: null | Signature, blobs: null | Array): string {
const fields: Array = [
formatNumber(tx.chainId, "chainId"),
formatNumber(tx.nonce, "nonce"),
@@ -438,6 +526,20 @@ function _serializeEip4844(tx: Transaction, sig?: Signature): string {
fields.push(formatNumber(sig.yParity, "yParity"));
fields.push(toBeArray(sig.r));
fields.push(toBeArray(sig.s));
+
+ // We have blobs; return the network wrapped format
+ if (blobs) {
+ return concat([
+ "0x03",
+ encodeRlp([
+ fields,
+ blobs.map((b) => b.data),
+ blobs.map((b) => b.commitment),
+ blobs.map((b) => b.proof),
+ ])
+ ]);
+ }
+
}
return concat([ "0x03", encodeRlp(fields)]);
@@ -471,6 +573,8 @@ export class Transaction implements TransactionLike {
#accessList: null | AccessList;
#maxFeePerBlobGas: null | bigint;
#blobVersionedHashes: null | Array;
+ #kzg: null | KzgLibrary;
+ #blobs: null | Array;
/**
* The transaction type.
@@ -651,7 +755,7 @@ export class Transaction implements TransactionLike {
}
/**
- * The BLOB versioned hashes for Cancun transactions.
+ * The BLOb versioned hashes for Cancun transactions.
*/
get blobVersionedHashes(): null | Array {
// @TODO: Mutation is inconsistent; if unset, the returned value
@@ -671,6 +775,89 @@ export class Transaction implements TransactionLike {
this.#blobVersionedHashes = value;
}
+ /**
+ * The BLObs for the Transaction, if any.
+ *
+ * If ``blobs`` is non-``null``, then the [[seriailized]]
+ * will return the network formatted sidecar, otherwise it
+ * will return the standard [[link-eip-2718]] payload. The
+ * [[unsignedSerialized]] is unaffected regardless.
+ *
+ * When setting ``blobs``, either fully valid [[Blob]] objects
+ * may be specified (i.e. correctly padded, with correct
+ * committments and proofs) or a raw [[BytesLike]] may
+ * be provided.
+ *
+ * If raw [[BytesLike]] are provided, the [[kzg]] property **must**
+ * be already set. The blob will be correctly padded and the
+ * [[KzgLibrary]] will be used to compute the committment and
+ * proof for the blob.
+ *
+ * Setting this automatically populates [[blobVersionedHashes]],
+ * overwriting any existing values. Setting this to ``null``
+ * does **not** remove the [[blobVersionedHashes]], leaving them
+ * present.
+ */
+ get blobs(): null | Array {
+ if (this.#blobs == null) { return null; }
+ return this.#blobs.map((b) => Object.assign({ }, b));
+ }
+ set blobs(_blobs: null | Array) {
+ if (_blobs == null) {
+ this.#blobs = null;
+ return;
+ }
+
+ const blobs: Array = [ ];
+ const versionedHashes: Array = [ ];
+ for (let i = 0; i < _blobs.length; i++) {
+ const blob = _blobs[i];
+
+ if (isBytesLike(blob)) {
+ assert(this.#kzg, "adding a raw blob requires a KZG library", "UNSUPPORTED_OPERATION", {
+ operation: "set blobs()"
+ });
+
+ let data = getBytes(blob);
+ assertArgument(data.length <= BLOB_SIZE, "blob is too large", `blobs[${ i }]`, blob);
+
+ // Pad blob if necessary
+ if (data.length !== BLOB_SIZE) {
+ const padded = new Uint8Array(BLOB_SIZE);
+ padded.set(data);
+ data = padded;
+ }
+
+ const commit = this.#kzg.blobToKzgCommitment(data);
+ const proof = hexlify(this.#kzg.computeBlobKzgProof(data, commit));
+
+ blobs.push({
+ data: hexlify(data),
+ commitment: hexlify(commit),
+ proof
+ });
+ versionedHashes.push(getVersionedHash(1, commit));
+
+ } else {
+ const commit = hexlify(blob.commitment);
+ blobs.push({
+ data: hexlify(blob.data),
+ commitment: commit,
+ proof: hexlify(blob.proof)
+ });
+ versionedHashes.push(getVersionedHash(1, commit));
+ }
+ }
+
+ this.#blobs = blobs;
+ this.#blobVersionedHashes = versionedHashes;
+ }
+
+ get kzg(): null | KzgLibrary { return this.#kzg; }
+ set kzg(kzg: null | KzgLibrary) {
+ this.#kzg = kzg;
+ }
+
/**
* Creates a new Transaction with default values.
*/
@@ -689,6 +876,8 @@ export class Transaction implements TransactionLike {
this.#accessList = null;
this.#maxFeePerBlobGas = null;
this.#blobVersionedHashes = null;
+ this.#blobs = null;
+ this.#kzg = null;
}
/**
@@ -696,7 +885,7 @@ export class Transaction implements TransactionLike {
*/
get hash(): null | string {
if (this.signature == null) { return null; }
- return keccak256(this.serialized);
+ return keccak256(this.#getSerialized(true, false));
}
/**
@@ -735,29 +924,34 @@ export class Transaction implements TransactionLike {
return this.signature != null;
}
- /**
- * The serialized transaction.
- *
- * This throws if the transaction is unsigned. For the pre-image,
- * use [[unsignedSerialized]].
- */
- get serialized(): string {
- assert(this.signature != null, "cannot serialize unsigned transaction; maybe you meant .unsignedSerialized", "UNSUPPORTED_OPERATION", { operation: ".serialized"});
+ #getSerialized(signed: boolean, sidecar: boolean): string {
+ assert(!signed || this.signature != null, "cannot serialize unsigned transaction; maybe you meant .unsignedSerialized", "UNSUPPORTED_OPERATION", { operation: ".serialized"});
+ const sig = signed ? this.signature: null;
switch (this.inferType()) {
case 0:
- return _serializeLegacy(this, this.signature);
+ return _serializeLegacy(this, sig);
case 1:
- return _serializeEip2930(this, this.signature);
+ return _serializeEip2930(this, sig);
case 2:
- return _serializeEip1559(this, this.signature);
+ return _serializeEip1559(this, sig);
case 3:
- return _serializeEip4844(this, this.signature);
+ return _serializeEip4844(this, sig, sidecar ? this.blobs: null);
}
assert(false, "unsupported transaction type", "UNSUPPORTED_OPERATION", { operation: ".serialized" });
}
+ /**
+ * The serialized transaction.
+ *
+ * This throws if the transaction is unsigned. For the pre-image,
+ * use [[unsignedSerialized]].
+ */
+ get serialized(): string {
+ return this.#getSerialized(true, true);
+ }
+
/**
* The transaction pre-image.
*
@@ -765,18 +959,7 @@ export class Transaction implements TransactionLike {
* authorize this transaction.
*/
get unsignedSerialized(): string {
- switch (this.inferType()) {
- case 0:
- return _serializeLegacy(this);
- case 1:
- return _serializeEip2930(this);
- case 2:
- return _serializeEip1559(this);
- case 3:
- return _serializeEip4844(this);
- }
-
- assert(false, "unsupported transaction type", "UNSUPPORTED_OPERATION", { operation: ".unsignedSerialized" });
+ return this.#getSerialized(false, false);
}
/**
@@ -963,8 +1146,15 @@ export class Transaction implements TransactionLike {
if (tx.chainId != null) { result.chainId = tx.chainId; }
if (tx.signature != null) { result.signature = Signature.from(tx.signature); }
if (tx.accessList != null) { result.accessList = tx.accessList; }
+
+ // This will get overwritten by blobs, if present
if (tx.blobVersionedHashes != null) { result.blobVersionedHashes = tx.blobVersionedHashes; }
+ // Make sure we assign the kzg before assigning blobs, which
+ // require the library in the event raw blob data is provided.
+ if (tx.kzg != null) { result.kzg = tx.kzg; }
+ if (tx.blobs != null) { result.blobs = tx.blobs; }
+
if (tx.hash != null) {
assertArgument(result.isSigned(), "unsigned transaction cannot define hash", "tx", tx);
assertArgument(result.hash === tx.hash, "hash mismatch", "tx", tx);