From 0d22adbd629ab38796cf6eca049b34822bde08d9 Mon Sep 17 00:00:00 2001 From: Aere Network Date: Wed, 23 Sep 2026 01:28:43 +0300 Subject: [PATCH] Patches 0006 (AIP-20, native post-quantum transactions, type 0x50) and 0007 (AIP-21, per-block post-quantum finality record): the recipe now reproduces the chain-2800 fleet distribution of 2026-09-22 (wt3f12ef0d) class for class on consensus and on both AIPs, with the Apache 4(b) notice inside every modified upstream file and 20 lines of context; run-node.sh arms aere.pqtx.forkBlock=19900000 for followers of chain 2800; SPEC 2.7 and 5.3; RUN-A-NODE carries the measured record; LICENSING regenerated --- LICENSING.md | 4 +- RUN-A-NODE.md | 44 +- SPEC.md | 61 +- patches/0006-aere-pq-transactions.patch | 3853 +++++++++++++++++++++++ patches/0007-aere-pq-finality.patch | 2065 ++++++++++++ run-node.sh | 3 + 6 files changed, 6025 insertions(+), 5 deletions(-) create mode 100644 patches/0006-aere-pq-transactions.patch create mode 100644 patches/0007-aere-pq-finality.patch diff --git a/LICENSING.md b/LICENSING.md index cfba1c5..914b8ad 100644 --- a/LICENSING.md +++ b/LICENSING.md @@ -123,7 +123,7 @@ the section 4(b) notice of changed files), and each keeps its original notice pe -## anchor/: every file that carries someone else's copyright (generated 2026-09-17) +## anchor/: every file that carries someone else's copyright (generated 2026-09-23) This list is **generated from the files this package actually ships**, not written by hand: the previous hand-written table fell behind the tree on 2026-09-12 and seven upstream files went @@ -132,7 +132,7 @@ repository, and the publisher refuses to ship a package whose list disagrees wit You do not need that tool to check the list: every row below names a file in this package, and `grep -i copyright` on it shows the same line. -Measured 2026-09-17 over `anchor/`: **179 files**. **90** carry someone +Measured 2026-09-23 over `anchor/`: **179 files**. **90** carry someone else's copyright alone, **45** carry a shared notice naming both upstream and us, and **11** name only `contributors to the AERE Network`; 33 carry no copyright line at all. The first two groups are the table below. A shared notice is listed too: those are precisely the diff --git a/RUN-A-NODE.md b/RUN-A-NODE.md index c704231..aaf8456 100644 --- a/RUN-A-NODE.md +++ b/RUN-A-NODE.md @@ -150,8 +150,9 @@ The anchor ships in this repository both as source, under `anchor/`, and as `patches/0003-aere-pq-anchor.patch`. A node built without it has not been shown to import headers past block 13,014,000: the second client needed an explicit change to accept the sixth element, and no floor-only build has been observed following the chain past that height. Apply -`0001`, `0003`, `0004` and `0005` together and treat a build missing any of them as unable to -follow the current chain. +`0001`, `0003`, `0004`, `0005`, `0006` and `0007` together and treat a build missing any of them as +unable to follow the current chain (from 2026-09-22 the last two are part of the recipe; see the +section on them below). ## Patch 0005 exists because the import proof was actually attempted @@ -188,6 +189,45 @@ has NOT yet been repeated against this revision (the last measured run is the on previous revision). What is measured for this revision is that the same code follows the live tip on the network's reading nodes, and validates every hybrid anchor there, since 2026-09-05. **Revision note, 2026-09-10:** patches `0001`, `0003` and `0004` were regenerated from the production tree over the same file sets (the source files now carry the Apache-2.0 section 4(b) modification notices that production carries; `0003` keeps its 169 files). Measured the same day: a build from this recipe reproduces the fleet's three consensus jars class by class, and `LondonFeeMarket` and the precompile classes byte for byte. What the recipe still does not ship is the fleet's parallel block processor (a separate package and one hunk in `MainnetProtocolSpecs`), which does not change consensus; it is listed by the comparison, not hidden. +## Patches 0006 and 0007: native post-quantum transactions and verifiable per-block finality (2026-09-22) + +Since 2026-09-22 21:45Z every node of chain 2800 that validates or reads blocks runs a +distribution that carries two more Aere changes, and the recipe here carries them as +`patches/0006-aere-pq-transactions.patch` (AIP-20) and `patches/0007-aere-pq-finality.patch` +(AIP-21), generated as exact deltas over the tree that `0001+0003+0004+0005` produce. + +- **0006, AIP-20.** Transaction type `0x50`, authorized by an ML-DSA (FIPS 204) signature, sender + derived from the post-quantum public key. Activation is the per-node property + `aere.pqtx.forkBlock`, which `run-node.sh` now sets to **19,900,000** for chain 2800. Below that + height the node behaves exactly as before. **From that height a node without this patch, or + without the property, rejects the first block that carries such a transaction and stops + following the chain.** That is the one reason this revision is not optional. +- **0007, AIP-21.** The JSON-RPC method `aere_getPqFinality` and the on-disk window of Falcon seals + behind it. It changes nothing about block validity, so it has no activation height; a follower + built with it answers `unavailable` for every block (it hears no seals; only validators do), + which is the honest answer, and the method exists. + +Measured 2026-09-22 22:16Z: the six patches applied in order with plain `git apply` on a pristine +`d2032017` checkout (187 files changed), `./gradlew --no-daemon installDist -x spotlessJavaCheck +-x test` exited 0, and the resulting jars were compared class by class, in both directions, with +the fleet distribution running on every chain-2800 node (`wt3f12ef0d`): 3,503 classes in the +public build, 3,529 in the fleet; nothing only in the public build; **every consensus class and +every AIP-20/21 class has identical code** (`javap -p -c` on each pair whose bytes differ). Of the +44 classes whose bytes differ, 38 differ only in their line-number table, because the public +patches carry the Apache 4(b) modification notice at the top of each modified upstream file and +the fleet build of 22 September does not yet; the other 6 are the same known non-consensus +differences the recipe carried before this revision: the Block-STM wiring hunk in +`MainnetProtocolSpecs` (its 26 classes exist only in the fleet, as an optional parallel processor) +and five RPC read-path classes (`EthGetBalance`, `EthGetCode`, `EthGetStorageAt`, +`EthGetTransactionCount`, `BlockchainQueries`: the fleet's fix for `latest` resolving to a null +state during import, 2026-09-10). None of those touches block validity; they are listed here +rather than hidden. The from-genesis import proof has NOT been repeated with this revision yet; what is +measured is that the same code follows the live tip on the network's own readers since 2026-09-22. +The Apache-2.0 section 4(b) statement for the upstream files these patches modify is in each +patch message; the in-file "Modifications Copyright" blocks for those files are deliberately not in +the delta (it is exact to the fleet sources) and enter the production sources with the next fleet +distribution. + --- ## Sizing the JVM heap: bigger is not faster diff --git a/SPEC.md b/SPEC.md index d22c835..0a0d087 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,6 +1,6 @@ # Aere Network chain 2800: full protocol specification -Version: 1.2.0, written 2026-08-25, amended 2026-09-02 (section 2.6, D8, G11), 2026-09-05 (sections 2.6 and 3.1: the message-layer arming heights, the v2 hybrid certificate, the anchor interval schedule) and 2026-09-17 (sections 2.6 and 3.1: the four enforcement heights have passed, hybrid keys are live; the "emission only" qualification is dated history). Scope: everything a third party needs to implement a client +Version: 1.2.0, written 2026-08-25, amended 2026-09-02 (section 2.6, D8, G11), 2026-09-05 (sections 2.6 and 3.1: the message-layer arming heights, the v2 hybrid certificate, the anchor interval schedule) and 2026-09-17 (sections 2.6 and 3.1: the four enforcement heights have passed, hybrid keys are live; the "emission only" qualification is dated history), and 2026-09-22 (sections 2.7 and 5.3: AIP-21 verifiable per-block post-quantum finality live on chain 2800; AIP-20 native post-quantum transactions armed for height 19,900,000, patches 0006 and 0007 added to the recipe). Scope: everything a third party needs to implement a client that follows chain 2800 from block 0 to the live head and validates what the network validates, without reading Aere source code. It specifies the follower (validation) protocol completely; it describes seal production only where a follower must understand it to validate. @@ -373,6 +373,40 @@ every anchor certificate is v2 since block 17,047,600 (2026-09-04). Between 2026 2026-09-08 this paragraph read "post-quantum in EMISSION only"; that was true for those days and is history now. What remains classical is named in section 3.1. +### 2.7 Verifiable post-quantum finality of every block (AIP-21; live on chain 2800 since 2026-09-22) + +Normative for a validator-capable build; informative for a pure follower, which answers honestly +that it has nothing to report. + +- Every validator keeps the Falcon-512 seals it hears on QBFT COMMIT messages (section 2.6) in a + retention window of the last 256 heights, keyed by the canonical block hash (not by the QBFT + message digest, which carries the round and differs from the on-chain hash for any block + finalised in a round above zero; source: `patches/0007-aere-pq-finality.patch`, `PqSealCache`). + The window is persisted under `/aere-pq-seal-window` in segments of 64 heights (frame: + `length(4 BE) || RLP[domain, chainId, height, hash, certificate] || crc32(4 BE)`), restored at + startup only for frames whose hash is the node's own canonical hash at that height and whose + seals verify now; `-Daere.pq.finality.persist=false` disables the journal. +- JSON-RPC method `aere_getPqFinality(blockParameter)` (QBFT method set; `blockParameter` is a hex + quantity or `latest`) returns, for a block in the window: `blockNumber`, `blockHash`, `quorum` + (ceil(2N/3) of the validator set at that height), `falconSealsHeard`, `falconSealsVerified` (each + seal re-verified now against the registry epoch of that height, section 3.5), `finality` in + {`post-quantum`, `partial`, `none`, `unavailable`, `unverifiable-form`}, `retentionBlocks` (256) + and a `note`. An unknown block returns `null`; a block outside the window, or any block on a node + that hears no seals (a reader, a follower), returns `unavailable`. `post-quantum` means: at least + the commit quorum of distinct validators sealed this block hash with Falcon-512 and every counted + seal verifies. +- Nothing here changes block validity, so there is no activation height and no fork: a build + without patch 0007 follows the chain exactly as before; it only lacks the method and the window. + The permanent, header-committed record remains the anchor certificate (section 3); this record is + the per-block complement, held by validators. + +[MEASURED 2026-09-22 21:45Z] on chain 2800, `deploy/validators/aip2021-2026-09-22/dovedeste-aip2021-2800.sh` +(Aere repository): all nine Besu-derived validators answer `post-quantum` for head minus 16 with +`falconSealsVerified` 10 of 10 heard and `quorum` 7; the Nethermind-derived validator answers the +same verdict, hash and quorum for the same block; the two public readers answer `unavailable`. +Specification and the two-client conformance record: `aips/AIP-21.md`, `aips/aip21-conformitate/` +in the Aere repository. + --- ## 3. The post-quantum certificate anchor @@ -824,6 +858,31 @@ endpoints since the floor went live. - `--min-gas-price` is a local mempool policy, not consensus; the Foundation nodes run it at zero (source: `RUN-A-NODE.md`). +### 5.3 Native post-quantum transactions, type 0x50 (AIP-20; activation height 19,900,000). Normative. + +- From block **19,900,000** chain 2800 accepts EIP-2718 transaction type `0x50`. Its sender is not + recovered from a secp256k1 signature: it is derived from an ML-DSA (FIPS 204) public key carried + in the transaction, and the authorization is an ML-DSA signature over a signing pre-image that + includes the chain id (so the same key yields the same address on 28001 and 2800, and the chain + id prevents replay). Algorithm ids: 1 = ML-DSA-44, 2 = ML-DSA-65, 3 = ML-DSA-87 (source: + `patches/0006-aere-pq-transactions.patch`, `AerePqAlgorithm`; the exact RLP layout, the signing + pre-image and the 14 conformance vectors are in the same patch under + `ethereum/core/src/test/resources/aere/pqtx/`, and in `aips/AIP-20.md` in the Aere repository). +- The activation is per node, by the system property `aere.pqtx.forkBlock` (absent means never): + `run-node.sh` sets `-Daere.pqtx.forkBlock=19900000` for chain 2800. Below the height the type is + refused at the transaction pool ("not enabled until H"); a build without patch 0006 does not know + the type at all and answers `Invalid params` to such a raw transaction. +- Consequence for a follower: **a build without patch 0006, or with it but without the property, rejects + the first block that carries a type-0x50 transaction after 19,900,000 and stops following the chain.** + Apply `0001`, `0003`, `0004`, `0005`, `0006` and `0007` together. + +[MEASURED 2026-09-22 21:45Z] every node that validates or reads blocks on chain 2800 (nine Besu-derived +validators, the Nethermind-derived validator, both public readers, the archive unit) runs the same +distribution with `aere.pqtx.forkBlock=19900000`; a type-0x50 vector for chain 28001 sent through the +public door is refused with `Wrong chainId` (the binary knows the type). [NOT MEASURED] as of this +revision: a block carrying a type-0x50 transaction imported by every node after 19,900,000; that +is the next dated record. + --- ## 6. Sync requirements: what a third-party client must implement to follow the chain diff --git a/patches/0006-aere-pq-transactions.patch b/patches/0006-aere-pq-transactions.patch new file mode 100644 index 0000000..85a9b68 --- /dev/null +++ b/patches/0006-aere-pq-transactions.patch @@ -0,0 +1,3853 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aere Network +Date: Tue, 22 Sep 2026 22:13:31 +0000 +Subject: [PATCH] Aere Network: AIP-20, native post-quantum transactions (type 0x50) for chain 2800 + +AERE patch 0006: AIP-20, native post-quantum transactions (type 0x50, ML-DSA authorization) + +Why this patch exists: from block 19,900,000 chain 2800 accepts transaction type 0x50 +(AIP-20), whose sender is derived from an ML-DSA (FIPS 204) public key and whose +authorization is an ML-DSA-44/65/87 signature verified natively by the client, not +through an account contract. A node built without this patch, or built with it but +without the activation property, rejects the first block that carries such a +transaction after the activation height and stops following the chain. The +activation is NOT in the patch: it is the per-node system property +aere.pqtx.forkBlock, which run-node.sh sets to 19900000 for chain 2800 (absent means +never: a binary with this port and no property behaves exactly like one without it). + +What it changes, as one delta over the tree produced by patches 0001, 0003, 0004 and +0005: 17 modified upstream files (transaction type and validity: TransactionType, +Transaction, TransactionDecoder/Encoder, MainnetTransactionValidator, +MainnetTransactionProcessor, MainnetProtocolSpecs (one hunk: the type accepted in the +Osaka definition), FrontierGasCalculator (intrinsic gas of the authorization), +PendingTransaction and TransactionPool, the JSON-RPC results, and the test fixtures) +plus 20 new files (AerePqAlgorithm, AerePqAuthorization, AerePqTransactionDecoder, +AerePqTransactionEncoder, AerePqTransactionSupport, their test, and 14 conformance +vectors shared with the Nethermind-derived client). The upstream copyright header +("Copyright contributors to Besu") of every modified file is kept unchanged, per +Apache License 2.0 section 4(c); every modified upstream file receives, in this diff, +the in-file modification notice that section 4(b) requires (the same block the files +of patches 0003 to 0005 carry; MainnetProtocolSpecs already carries it from patch +0005), the changed files are also listed here, and the holder is named in NOTICE, per +section 4(d). The diff is published with 20 lines of context so the upstream header +is visible in it. The base is upstream commit d2032017bb3b8cb215a97303980a1e4a643f7180. + +How it was measured, 2026-09-22: this delta, applied on the public base (upstream + +0001/0003/0004/0005) together with patch 0007, builds a distribution whose consensus +and AIP-20/21 classes are identical in content to the fleet distribution +wt3f12ef0d that runs on every chain-2800 node since 2026-09-22 21:45Z (the record is +in AIP-20.md and AIP-21.md). Specification: aips/AIP-20.md. + +Apache License 2.0, section 4(b): the upstream files this patch modifies (the work of +Hyperledger Besu, whose copyright notices are kept unchanged) are, and this notice is +the statement that contributors to the Aere Network changed them: + datatypes/src/main/java/org/hyperledger/besu/datatypes/Transaction.java + datatypes/src/main/java/org/hyperledger/besu/datatypes/TransactionType.java + ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/TransactionBaseResult.java + ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/pending/PendingTransactionDetailResult.java + ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/Transaction.java + ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/TransactionDecoder.java + ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/TransactionEncoder.java + ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java + ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java + ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidator.java + ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/BlockDataGenerator.java + ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/TransactionTestFixture.java + ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransaction.java + ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java + ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseTransactionPoolTest.java + ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayersTest.java + evm/src/main/java/org/hyperledger/besu/evm/gascalculator/FrontierGasCalculator.java + +The in-file "Modifications Copyright" blocks for these files are not in this delta on +purpose: the delta is exact to the fleet distribution wt3f12ef0d, and the notices will +enter the production sources with the next fleet distribution, as was done on +2026-09-10 for patches 0003 to 0005. +--- +diff --git a/datatypes/src/main/java/org/hyperledger/besu/datatypes/AerePqAlgorithm.java b/datatypes/src/main/java/org/hyperledger/besu/datatypes/AerePqAlgorithm.java +new file mode 100755 +index 000000000..ef2d7f627 +--- /dev/null ++++ b/datatypes/src/main/java/org/hyperledger/besu/datatypes/AerePqAlgorithm.java +@@ -0,0 +1,98 @@ ++/* ++ * Copyright contributors to the AERE Network. ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with ++ * the License. You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on ++ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the ++ * specific language governing permissions and limitations under the License. ++ * ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++package org.hyperledger.besu.datatypes; ++ ++import java.util.Optional; ++ ++/** ++ * The post-quantum signature algorithms a type-0x50 transaction (AIP-20) may be authorized with. ++ * ++ *

The identifier, the exact key and signature lengths and the verification gas are the table of ++ * AIP-20. Only the three FIPS 204 ML-DSA parameter sets are valid in v1; the reserved identifiers ++ * (SLH-DSA 0x10, Falcon-512 0x20, hybrid flag 0x80) are deliberately absent so that a transaction ++ * naming them is invalid, not silently accepted. ++ */ ++public enum AerePqAlgorithm { ++ /** ML-DSA-44, FIPS 204, NIST category 2. */ ++ ML_DSA_44(0x01, 1312, 2420, 55_000L), ++ /** ML-DSA-65, FIPS 204, NIST category 3, the recommended default. */ ++ ML_DSA_65(0x02, 1952, 3309, 75_000L), ++ /** ML-DSA-87, FIPS 204, NIST category 5. */ ++ ML_DSA_87(0x03, 2592, 4627, 100_000L); ++ ++ private final int id; ++ private final int publicKeyLength; ++ private final int signatureLength; ++ private final long verifyGas; ++ ++ AerePqAlgorithm( ++ final int id, final int publicKeyLength, final int signatureLength, final long verifyGas) { ++ this.id = id; ++ this.publicKeyLength = publicKeyLength; ++ this.signatureLength = signatureLength; ++ this.verifyGas = verifyGas; ++ } ++ ++ /** ++ * Looks an algorithm up by its AIP-20 identifier. ++ * ++ * @param id the identifier as carried in the transaction ++ * @return the algorithm, or empty for any identifier this implementation does not accept ++ */ ++ public static Optional byId(final int id) { ++ for (final AerePqAlgorithm a : values()) { ++ if (a.id == id) { ++ return Optional.of(a); ++ } ++ } ++ return Optional.empty(); ++ } ++ ++ /** ++ * The AIP-20 identifier. ++ * ++ * @return the identifier byte value ++ */ ++ public int id() { ++ return id; ++ } ++ ++ /** ++ * The exact public key length in bytes. ++ * ++ * @return the length ++ */ ++ public int publicKeyLength() { ++ return publicKeyLength; ++ } ++ ++ /** ++ * The exact signature length in bytes. ++ * ++ * @return the length ++ */ ++ public int signatureLength() { ++ return signatureLength; ++ } ++ ++ /** ++ * The gas charged for one signature verification (AIP-20 table, Draft values). ++ * ++ * @return the gas ++ */ ++ public long verifyGas() { ++ return verifyGas; ++ } ++} +diff --git a/datatypes/src/main/java/org/hyperledger/besu/datatypes/AerePqAuthorization.java b/datatypes/src/main/java/org/hyperledger/besu/datatypes/AerePqAuthorization.java +new file mode 100755 +index 000000000..37cda70e9 +--- /dev/null ++++ b/datatypes/src/main/java/org/hyperledger/besu/datatypes/AerePqAuthorization.java +@@ -0,0 +1,110 @@ ++/* ++ * Copyright contributors to the AERE Network. ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with ++ * the License. You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on ++ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the ++ * specific language governing permissions and limitations under the License. ++ * ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++package org.hyperledger.besu.datatypes; ++ ++import java.util.Objects; ++ ++import org.apache.tuweni.bytes.Bytes; ++import org.apache.tuweni.bytes.Bytes32; ++ ++/** ++ * The post-quantum authorization of a type-0x50 transaction (AIP-20): the algorithm, the raw public ++ * key and the raw signature. No secp256k1 material exists for such a transaction. ++ * ++ * @param algorithm the ML-DSA parameter set ++ * @param publicKey the raw public key, exactly {@code algorithm.publicKeyLength()} bytes ++ * @param signature the raw signature, exactly {@code algorithm.signatureLength()} bytes ++ */ ++public record AerePqAuthorization(AerePqAlgorithm algorithm, Bytes publicKey, Bytes signature) { ++ ++ /** The EIP-2718 type byte of the transaction this authorization belongs to. */ ++ public static final byte TYPE_BYTE = (byte) 0x50; ++ ++ /** ++ * Validates the record's invariants at construction. ++ * ++ * @param algorithm the algorithm ++ * @param publicKey the public key ++ * @param signature the signature ++ */ ++ public AerePqAuthorization( ++ final AerePqAlgorithm algorithm, final Bytes publicKey, final Bytes signature) { ++ this.algorithm = Objects.requireNonNull(algorithm, "algorithm"); ++ this.publicKey = Objects.requireNonNull(publicKey, "publicKey"); ++ this.signature = Objects.requireNonNull(signature, "signature"); ++ } ++ ++ /** ++ * Whether the key and signature have exactly the lengths of the algorithm's table. A wrong ++ * length is an invalid transaction, never a truncated or padded verification. ++ * ++ * @return true when both lengths match ++ */ ++ public boolean lengthsValid() { ++ return publicKey.size() == algorithm.publicKeyLength() ++ && signature.size() == algorithm.signatureLength(); ++ } ++ ++ /** ++ * The bytes charged as authorization data (AIP-20 gas rule). ++ * ++ * @return key length plus signature length ++ */ ++ public int authorizationByteCount() { ++ return publicKey.size() + signature.size(); ++ } ++ ++ /** ++ * The sender address of AIP-20: {@code keccak256(0x50 || alg_id || public_key)[12:32]}. ++ * ++ * @return the derived sender ++ */ ++ public Address senderAddress() { ++ return deriveSender(algorithm, publicKey); ++ } ++ ++ /** ++ * Derives the AIP-20 sender address for a public key under an algorithm. ++ * ++ * @param algorithm the algorithm ++ * @param publicKey the raw public key ++ * @return the address ++ */ ++ public static Address deriveSender(final AerePqAlgorithm algorithm, final Bytes publicKey) { ++ final Bytes preimage = ++ Bytes.concatenate(Bytes.of(TYPE_BYTE), Bytes.of((byte) algorithm.id()), publicKey); ++ return Address.extract(Bytes32.wrap(Hash.hash(preimage).getBytes())); ++ } ++ ++ /** ++ * A copy that shares no byte array with the original (for detached transaction copies). ++ * ++ * @return the copy ++ */ ++ public AerePqAuthorization detachedCopy() { ++ return new AerePqAuthorization(algorithm, publicKey.copy(), signature.copy()); ++ } ++ ++ @Override ++ public String toString() { ++ return "AerePqAuthorization{" ++ + algorithm ++ + ", pk=" ++ + publicKey.size() ++ + "B, sig=" ++ + signature.size() ++ + "B}"; ++ } ++} +diff --git a/datatypes/src/main/java/org/hyperledger/besu/datatypes/Transaction.java b/datatypes/src/main/java/org/hyperledger/besu/datatypes/Transaction.java +index e867a21f9..8175f0b4f 100644 +--- a/datatypes/src/main/java/org/hyperledger/besu/datatypes/Transaction.java ++++ b/datatypes/src/main/java/org/hyperledger/besu/datatypes/Transaction.java +@@ -1,33 +1,39 @@ + /* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 ++ * ++ * Modifications Copyright contributors to the Aere Network. ++ * ++ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the ++ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was ++ * found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch. + */ + package org.hyperledger.besu.datatypes; + + import java.math.BigInteger; + import java.util.List; + import java.util.Optional; + + import org.apache.tuweni.bytes.Bytes; + + /** + * A transaction is a single cryptographically-signed instruction constructed by an actor externally + * to the scope of Ethereum. While it is assumed that the ultimate external actor will be human in + * nature, software tools will be used in its construction and dissemination. + * + *

There are two types of transactions: those which result in message calls and those which + * result in the creation of new accounts with associated code (known informally as ‘contract + * creation’). Message call transactions will have an address present in the {@link #getTo} method + * whereas contract creation transactions will not. + */ + public interface Transaction { +@@ -265,21 +271,30 @@ public interface Transaction { + /** + * Returns whether the transaction is a contract creation + * + * @return {@code true} if this is a contract-creation transaction; otherwise {@code false} + */ + boolean isContractCreation(); + + /** + * Returns the code delegations if this transaction is a 7702 transaction. + * + * @return the code delegations + */ + Optional> getCodeDelegationList(); + + /** + * Returns the size of the code delegation list. + * + * @return the size of the code delegation list + */ + int codeDelegationListSize(); ++ ++ /** ++ * The AIP-20 post-quantum authorization, present only for type 0x50 transactions. ++ * ++ * @return the authorization, or empty for every other type ++ */ ++ default Optional getPqAuthorization() { ++ return Optional.empty(); ++ } + } +diff --git a/datatypes/src/main/java/org/hyperledger/besu/datatypes/TransactionType.java b/datatypes/src/main/java/org/hyperledger/besu/datatypes/TransactionType.java +index 8b54a9723..cf6e077d4 100644 +--- a/datatypes/src/main/java/org/hyperledger/besu/datatypes/TransactionType.java ++++ b/datatypes/src/main/java/org/hyperledger/besu/datatypes/TransactionType.java +@@ -1,105 +1,116 @@ + /* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 ++ * ++ * Modifications Copyright contributors to the Aere Network. ++ * ++ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the ++ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was ++ * found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch. + */ + package org.hyperledger.besu.datatypes; + + import java.util.EnumSet; + import java.util.Optional; + import java.util.Set; + + /** The enum Transaction type. */ + public enum TransactionType { + /** The Frontier. */ + FRONTIER(0xf8, 0x00), + /** Access list transaction type. */ + ACCESS_LIST(0x01), + /** Eip1559 transaction type. */ + EIP1559(0x02), + /** Blob transaction type. */ + BLOB(0x03), + /** Eip7702 transaction type. */ +- DELEGATE_CODE(0x04); ++ DELEGATE_CODE(0x04), ++ /** AIP-20: Aere post-quantum authorized transaction (ML-DSA), no secp256k1 signature. */ ++ AERE_PQ(0x50); + + private static final Set ACCESS_LIST_SUPPORTED_TRANSACTION_TYPES = +- EnumSet.of(ACCESS_LIST, EIP1559, BLOB, DELEGATE_CODE); ++ EnumSet.of(ACCESS_LIST, EIP1559, BLOB, DELEGATE_CODE, AERE_PQ); + + private static final Set LEGACY_FEE_MARKET_TRANSACTION_TYPES = + EnumSet.of(FRONTIER, ACCESS_LIST); + + // The theoretical boundaries of the first byte of the RLP of a Frontier transaction. + // In practice Frontier transactions almost always start with 0xf8 or 0xf9. + private static final byte MIN_LEGACY_TX_OPAQUE_BYTE = (byte) 0xc0; + private static final byte MAX_LEGACY_TX_OPAQUE_BYTE = (byte) 0xfe; + + // This array helps to translate the first opaque byte of an RLP encoded transaction + // to its type. + // Note that Frontier type occupies more slots, since there are different first byte values, + // that represent such type. + // Holes in the array represents invalid first byte values, for which there is not (yet) + // a defined transaction type + private static final TransactionType[] transactionTypeByOpaqueByte = + new TransactionType[Byte.toUnsignedInt(MAX_LEGACY_TX_OPAQUE_BYTE) + 1]; + + private static final TransactionType[] transactionTypeByEthSerializedType = +- new TransactionType[values().length]; ++ // sized by the byte space, not by the enum: AERE_PQ is 0x50 (AIP-20), far past values().length ++ new TransactionType[256]; + + static { + EnumSet.allOf(TransactionType.class).stream() + .forEach( + tt -> { + tt.requireChainId = tt != FRONTIER; + tt.supportAccessList = ACCESS_LIST_SUPPORTED_TRANSACTION_TYPES.contains(tt); + tt.supportBaseFeeMarket = !LEGACY_FEE_MARKET_TRANSACTION_TYPES.contains(tt); + tt.supportBlob = tt == BLOB; + tt.supportDelegatedCode = tt == DELEGATE_CODE; ++ tt.supportPqAuthorization = tt == AERE_PQ; + if (tt == FRONTIER) { + for (int i = Byte.toUnsignedInt(MIN_LEGACY_TX_OPAQUE_BYTE); + i < Byte.toUnsignedInt(MAX_LEGACY_TX_OPAQUE_BYTE); + i++) { + transactionTypeByOpaqueByte[i] = FRONTIER; + } + } else { + transactionTypeByOpaqueByte[tt.getSerializedType()] = tt; + } + transactionTypeByEthSerializedType[tt.getEthSerializedType()] = tt; + }); + } + + private final byte typeValue; + private final byte serializedType; + boolean requireChainId; + boolean supportAccessList; + boolean supportBaseFeeMarket; + boolean supportBlob; + boolean supportDelegatedCode; ++ boolean supportPqAuthorization; + + TransactionType(final int typeValue, final int serializedType) { + this.typeValue = (byte) typeValue; + this.serializedType = (byte) serializedType; + } + + TransactionType(final int typeValue) { + this(typeValue, typeValue); + } + + /** + * Gets serialized type. + * + * @return the serialized type + */ + public byte getSerializedType() { + return typeValue; + } + + /** +@@ -168,21 +179,31 @@ public enum TransactionType { + return requireChainId; + } + + /** + * Does transaction type support data blobs. + * + * @return the boolean + */ + public boolean supportsBlob() { + return supportBlob; + } + + /** + * Does transaction type support delegate code. + * + * @return the boolean + */ + public boolean supportsDelegateCode() { + return supportDelegatedCode; + } ++ ++ /** ++ * Does transaction type carry an AIP-20 post-quantum authorization instead of a secp256k1 ++ * signature. ++ * ++ * @return the boolean ++ */ ++ public boolean supportsPqAuthorization() { ++ return supportPqAuthorization; ++ } + } +diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/TransactionBaseResult.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/TransactionBaseResult.java +index 750dd7bd2..b24ba106d 100644 +--- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/TransactionBaseResult.java ++++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/TransactionBaseResult.java +@@ -1,33 +1,39 @@ + /* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 ++ * ++ * Modifications Copyright contributors to the Aere Network. ++ * ++ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the ++ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was ++ * found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch. + */ + package org.hyperledger.besu.ethereum.api.jsonrpc.internal.results; + + import org.hyperledger.besu.datatypes.AccessListEntry; + import org.hyperledger.besu.datatypes.BytesHolder; + import org.hyperledger.besu.datatypes.TransactionType; + import org.hyperledger.besu.datatypes.Wei; + import org.hyperledger.besu.ethereum.core.Transaction; + + import java.util.List; + import java.util.Optional; + + import com.fasterxml.jackson.annotation.JsonGetter; + import com.fasterxml.jackson.annotation.JsonInclude; + import com.fasterxml.jackson.annotation.JsonPropertyOrder; + import org.apache.tuweni.bytes.Bytes; + + @JsonPropertyOrder({ + "accessList", + "authorizationList", +@@ -35,74 +41,86 @@ import org.apache.tuweni.bytes.Bytes; + "blockNumber", + "blockTimestamp", + "chainId", + "from", + "gas", + "gasPrice", + "maxPriorityFeePerGas", + "maxFeePerGas", + "maxFeePerBlobGas", + "hash", + "input", + "nonce", + "to", + "transactionIndex", + "type", + "value", + "yParity", + "v", + "r", + "s", ++ "algId", ++ "pqPublicKey", ++ "pqSignature", + "blobVersionedHashes" + }) + public class TransactionBaseResult implements TransactionResult { + + @JsonInclude(JsonInclude.Include.NON_NULL) + private final List accessList; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private final String chainId; + + private final String from; + private final String gas; + private final String gasPrice; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private final String maxPriorityFeePerGas; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private final String maxFeePerGas; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private final String maxFeePerBlobGas; + + private final String hash; + private final String input; + private final String nonce; + private final String to; + private final String type; + private final String value; + private final String yParity; + private final String v; + private final String r; + private final String s; + ++ @JsonInclude(JsonInclude.Include.NON_NULL) ++ private final String algId; ++ ++ @JsonInclude(JsonInclude.Include.NON_NULL) ++ private final String pqPublicKey; ++ ++ @JsonInclude(JsonInclude.Include.NON_NULL) ++ private final String pqSignature; ++ + @JsonInclude(JsonInclude.Include.NON_NULL) + private final List versionedHashes; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private final List authorizationList; + + public TransactionBaseResult(final Transaction transaction) { + this(transaction, Optional.empty()); + } + + public TransactionBaseResult(final Transaction transaction, final Optional maybeBaseFee) { + final TransactionType transactionType = transaction.getType(); + this.accessList = + transaction.getAccessList().orElse(transactionType.supportsAccessList() ? List.of() : null); + this.chainId = transaction.getChainId().map(Quantity::create).orElse(null); + this.from = transaction.getSender().toString(); + this.gas = Quantity.create(transaction.getGasLimit()); + this.maxPriorityFeePerGas = + transaction.getMaxPriorityFeePerGas().map(Wei::toShortHexString).orElse(null); + this.maxFeePerGas = transaction.getMaxFeePerGas().map(Wei::toShortHexString).orElse(null); +@@ -110,52 +128,59 @@ public class TransactionBaseResult implements TransactionResult { + transaction.getMaxFeePerBlobGas().map(Wei::toShortHexString).orElse(null); + this.gasPrice = + Quantity.create( + transaction + .getGasPrice() + .orElseGet( + () -> + maybeBaseFee.isPresent() + ? transaction.getEffectiveGasPrice(maybeBaseFee) + : transaction.getMaxFeePerGas().get())); + this.hash = transaction.getHash().toString(); + this.input = transaction.getPayload().toString(); + this.nonce = Quantity.create(transaction.getNonce()); + this.to = transaction.getTo().map(a -> a.getBytes().toHexString()).orElse(null); + if (transactionType == TransactionType.FRONTIER) { + this.type = Quantity.create(0); + this.yParity = null; + this.v = Quantity.create(transaction.getV()); + } else { + this.type = Quantity.create(transactionType.getSerializedType()); +- this.yParity = Quantity.create(transaction.getYParity()); ++ this.yParity = ++ transaction.getYParity() == null ? null : Quantity.create(transaction.getYParity()); + this.v = + (transactionType == TransactionType.ACCESS_LIST + || transactionType == TransactionType.EIP1559 + || transactionType == TransactionType.DELEGATE_CODE + || transactionType == TransactionType.BLOB) + ? Quantity.create(transaction.getYParity()) + : null; + } + this.value = Quantity.create(transaction.getValue()); +- this.r = Quantity.create(transaction.getR()); +- this.s = Quantity.create(transaction.getS()); ++ this.r = transaction.getR() == null ? null : Quantity.create(transaction.getR()); ++ this.s = transaction.getS() == null ? null : Quantity.create(transaction.getS()); ++ this.algId = ++ transaction.getPqAuthorization().map(a -> Quantity.create(a.algorithm().id())).orElse(null); ++ this.pqPublicKey = ++ transaction.getPqAuthorization().map(a -> a.publicKey().toHexString()).orElse(null); ++ this.pqSignature = ++ transaction.getPqAuthorization().map(a -> a.signature().toHexString()).orElse(null); + this.versionedHashes = + transaction + .getVersionedHashes() + .map( + hashes -> + hashes.stream().map(BytesHolder::getBytes).map(Bytes::toHexString).toList()) + .orElse(null); + this.authorizationList = + transaction + .getCodeDelegationList() + .map(cds -> cds.stream().map(CodeDelegationResult::new).toList()) + .orElse(null); + } + + @JsonGetter(value = "accessList") + public List getAccessList() { + return accessList; + } + + @JsonGetter(value = "chainId") +@@ -218,50 +243,70 @@ public class TransactionBaseResult implements TransactionResult { + return type; + } + + @JsonGetter(value = "value") + public String getValue() { + return value; + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonGetter(value = "yParity") + public String getYParity() { + return yParity; + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonGetter(value = "v") + public String getV() { + return v; + } + ++ @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonGetter(value = "r") + public String getR() { + return r; + } + ++ @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonGetter(value = "s") + public String getS() { + return s; + } + ++ @JsonInclude(JsonInclude.Include.NON_NULL) ++ @JsonGetter(value = "algId") ++ public String getAlgId() { ++ return algId; ++ } ++ ++ @JsonInclude(JsonInclude.Include.NON_NULL) ++ @JsonGetter(value = "pqPublicKey") ++ public String getPqPublicKey() { ++ return pqPublicKey; ++ } ++ ++ @JsonInclude(JsonInclude.Include.NON_NULL) ++ @JsonGetter(value = "pqSignature") ++ public String getPqSignature() { ++ return pqSignature; ++ } ++ + @JsonGetter(value = "blobVersionedHashes") + public List getVersionedHashes() { + return versionedHashes; + } + + @JsonGetter(value = "authorizationList") + public List getAuthorizationList() { + return authorizationList; + } + + @JsonGetter(value = "blockHash") + public String getBlockHash() { + return null; + } + + @JsonGetter(value = "blockNumber") + public String getBlockNumber() { + return null; + } + +diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/pending/PendingTransactionDetailResult.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/pending/PendingTransactionDetailResult.java +index 3531ffa4c..d03b782f5 100644 +--- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/pending/PendingTransactionDetailResult.java ++++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/pending/PendingTransactionDetailResult.java +@@ -1,33 +1,39 @@ + /* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 ++ * ++ * Modifications Copyright contributors to the Aere Network. ++ * ++ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the ++ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was ++ * found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch. + */ + package org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.pending; + + import org.hyperledger.besu.datatypes.TransactionType; + import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.JsonRpcResult; + import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.Quantity; + import org.hyperledger.besu.ethereum.core.Transaction; + + import com.fasterxml.jackson.annotation.JsonGetter; + import com.fasterxml.jackson.annotation.JsonInclude; + import com.fasterxml.jackson.annotation.JsonPropertyOrder; + + @JsonPropertyOrder({ + "from", + "gas", + "gasPrice", + "hash", + "input", + "nonce", + "to", +@@ -51,50 +57,50 @@ public class PendingTransactionDetailResult implements JsonRpcResult { + private final String yParity; + private final String v; + private final String r; + private final String s; + + public PendingTransactionDetailResult(final Transaction tx) { + TransactionType transactionType = tx.getType(); + this.from = tx.getSender().toString(); + this.gas = Quantity.create(tx.getGasLimit()); + this.gasPrice = tx.getGasPrice().map(Quantity::create).orElse(null); + this.hash = tx.getHash().toString(); + this.input = tx.getPayload().toString(); + this.nonce = Quantity.create(tx.getNonce()); + this.to = tx.getTo().map(a -> a.getBytes().toHexString()).orElse(null); + if (transactionType == TransactionType.FRONTIER) { + this.type = Quantity.create(0); + this.yParity = null; + this.v = Quantity.create(tx.getV()); + } else { + this.type = Quantity.create(transactionType.getSerializedType()); +- this.yParity = Quantity.create(tx.getYParity()); ++ this.yParity = tx.getYParity() == null ? null : Quantity.create(tx.getYParity()); + this.v = + (transactionType == TransactionType.ACCESS_LIST + || transactionType == TransactionType.EIP1559) + ? Quantity.create(tx.getYParity()) + : null; + } + this.value = Quantity.create(tx.getValue()); +- this.r = Quantity.create(tx.getR()); +- this.s = Quantity.create(tx.getS()); ++ this.r = tx.getR() == null ? null : Quantity.create(tx.getR()); ++ this.s = tx.getS() == null ? null : Quantity.create(tx.getS()); + } + + @JsonGetter(value = "from") + public String getFrom() { + return from; + } + + @JsonGetter(value = "gas") + public String getGas() { + return gas; + } + + @JsonGetter(value = "gasPrice") + public String getGasPrice() { + return gasPrice; + } + + @JsonGetter(value = "hash") + public String getHash() { + return hash; +diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/Transaction.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/Transaction.java +index 2b976857c..ec42cfd32 100644 +--- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/Transaction.java ++++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/Transaction.java +@@ -1,57 +1,65 @@ + /* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 ++ * ++ * Modifications Copyright contributors to the Aere Network. ++ * ++ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the ++ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was ++ * found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch. + */ + package org.hyperledger.besu.ethereum.core; + + import static com.google.common.base.Preconditions.checkArgument; + import static com.google.common.base.Preconditions.checkState; + import static org.hyperledger.besu.crypto.Hash.keccak256; + import static org.hyperledger.besu.datatypes.VersionedHash.SHA256_VERSION_ID; + + import org.hyperledger.besu.crypto.KeyPair; + import org.hyperledger.besu.crypto.SECPPublicKey; + import org.hyperledger.besu.crypto.SECPSignature; + import org.hyperledger.besu.crypto.SignatureAlgorithm; + import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; + import org.hyperledger.besu.datatypes.AccessListEntry; ++import org.hyperledger.besu.datatypes.AerePqAuthorization; + import org.hyperledger.besu.datatypes.Address; + import org.hyperledger.besu.datatypes.BlobType; + import org.hyperledger.besu.datatypes.BytesHolder; + import org.hyperledger.besu.datatypes.CodeDelegation; + import org.hyperledger.besu.datatypes.Hash; + import org.hyperledger.besu.datatypes.Sha256Hash; + import org.hyperledger.besu.datatypes.TransactionType; + import org.hyperledger.besu.datatypes.VersionedHash; + import org.hyperledger.besu.datatypes.Wei; + import org.hyperledger.besu.ethereum.core.encoding.AccessListTransactionEncoder; ++import org.hyperledger.besu.ethereum.core.encoding.AerePqTransactionEncoder; + import org.hyperledger.besu.ethereum.core.encoding.BlobTransactionEncoder; + import org.hyperledger.besu.ethereum.core.encoding.CodeDelegationTransactionEncoder; + import org.hyperledger.besu.ethereum.core.encoding.EncodingContext; + import org.hyperledger.besu.ethereum.core.encoding.TransactionDecoder; + import org.hyperledger.besu.ethereum.core.encoding.TransactionEncoder; + import org.hyperledger.besu.ethereum.core.kzg.Blob; + import org.hyperledger.besu.ethereum.core.kzg.BlobsWithCommitments; + import org.hyperledger.besu.ethereum.core.kzg.KZGCommitment; + import org.hyperledger.besu.ethereum.core.kzg.KZGProof; + import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; + import org.hyperledger.besu.ethereum.rlp.RLP; + import org.hyperledger.besu.ethereum.rlp.RLPInput; + import org.hyperledger.besu.ethereum.rlp.RLPOutput; + + import java.math.BigInteger; + import java.util.Collection; + import java.util.List; + import java.util.Objects; + import java.util.Optional; + +@@ -114,40 +122,43 @@ public class Transaction + private volatile Bytes32 hashNoSignature; + + // Caches the transaction sender. + private volatile Address sender; + + // Caches the hash used to uniquely identify the transaction. + private volatile Hash hash; + // Caches the size in bytes of the encoded transaction. + private volatile int sizeForAnnouncement = -1; + private volatile int sizeForBlockInclusion = -1; + private final TransactionType transactionType; + + private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithmFactory.getInstance(); + private final Optional> versionedHashes; + + private final Optional blobsWithCommitments; + private final Optional> maybeCodeDelegationList; + + private final Optional rawRlp; + ++ // AIP-20: present only for type 0x50; then `signature` is null and the sender comes from the key. ++ private final Optional pqAuthorization; ++ + public static Builder builder() { + return new Builder(); + } + + public static Transaction readFrom(final Bytes rlpBytes) { + return readFrom(RLP.input(rlpBytes)); + } + + /** + * Recreate a transaction from RLP serialized in the block body format + * + * @param rlpInput the RLP input in block body format + * @return the transaction + */ + public static Transaction readFrom(final RLPInput rlpInput) { + return readFrom(rlpInput, EncodingContext.BLOCK_BODY); + } + + /** + * Recreate a transaction from RLP serialized according to the specified format +@@ -191,40 +202,41 @@ public class Transaction + private Transaction( + final boolean forCopy, + final TransactionType transactionType, + final long nonce, + final Optional gasPrice, + final Optional maxPriorityFeePerGas, + final Optional maxFeePerGas, + final Optional maxFeePerBlobGas, + final long gasLimit, + final Optional

to, + final Wei value, + final SECPSignature signature, + final Payload payload, + final Optional> maybeAccessList, + final Address sender, + final Optional chainId, + final Optional> versionedHashes, + final Optional blobsWithCommitments, + final Optional> maybeCodeDelegationList, + final Optional rawRlp, ++ final Optional pqAuthorization, + final Optional hash, + final Optional sizeForAnnouncement, + final Optional sizeForBlockInclusion) { + + if (!forCopy) { + if (transactionType.requiresChainId()) { + checkArgument( + chainId.isPresent(), + "Chain id must be present for transaction type %s", + transactionType); + } + + if (maybeAccessList.isPresent()) { + checkArgument( + transactionType.supportsAccessList(), + "Must not specify access list for transaction not supporting it"); + } + + if (Objects.equals(transactionType, TransactionType.ACCESS_LIST)) { + checkArgument( +@@ -235,60 +247,73 @@ public class Transaction + checkArgument( + transactionType.supportsBlob(), + "Must not specify blob versioned hashes or max fee per blob gas for transaction not supporting it"); + } + + if (transactionType.supportsBlob()) { + checkArgument( + versionedHashes.isPresent(), "Must specify blob versioned hashes for blob transaction"); + checkArgument( + !versionedHashes.get().isEmpty(), + "Blob transaction must have at least one versioned hash"); + checkArgument( + maxFeePerBlobGas.isPresent(), "Must specify max fee per blob gas for blob transaction"); + } + + if (transactionType.supportsDelegateCode()) { + checkArgument( + maybeCodeDelegationList.isPresent(), + "Must specify code delegation authorizations for code delegation transaction"); + } ++ ++ if (transactionType.supportsPqAuthorization()) { ++ checkArgument( ++ pqAuthorization.isPresent(), ++ "Must specify a post-quantum authorization for an AIP-20 transaction"); ++ checkArgument(signature == null, "An AIP-20 transaction carries no secp256k1 signature"); ++ } else { ++ checkArgument( ++ pqAuthorization.isEmpty(), ++ "Must not specify a post-quantum authorization for transaction type %s", ++ transactionType); ++ } + } + + this.transactionType = transactionType; + this.nonce = nonce; + this.gasPrice = gasPrice; + this.maxPriorityFeePerGas = maxPriorityFeePerGas; + this.maxFeePerGas = maxFeePerGas; + this.maxFeePerBlobGas = maxFeePerBlobGas; + this.gasLimit = gasLimit; + this.to = to; + this.value = value; + this.signature = signature; + this.payload = payload; + this.maybeAccessList = maybeAccessList; + this.sender = sender; + this.chainId = chainId; + this.versionedHashes = versionedHashes; + this.blobsWithCommitments = blobsWithCommitments; + this.maybeCodeDelegationList = maybeCodeDelegationList; + this.rawRlp = rawRlp; ++ this.pqAuthorization = pqAuthorization; + hash.ifPresent(h -> this.hash = h); + sizeForAnnouncement.ifPresent(i -> this.sizeForAnnouncement = i); + sizeForBlockInclusion.ifPresent(i -> this.sizeForBlockInclusion = i); + } + + /** + * Returns the transaction nonce. + * + * @return the transaction nonce + */ + @Override + public long getNonce() { + return nonce; + } + + /** + * Return the transaction gas price. + * + * @return the transaction gas price + */ +@@ -455,140 +480,150 @@ public class Transaction + @Override + public Optional getChainId() { + return chainId; + } + + /** + * Returns the transaction sender. + * + * @return the transaction sender + */ + @Override + public Address getSender() { + if (sender == null) { + Optional
cachedSender = Optional.ofNullable(senderCache.getIfPresent(getHash())); + sender = cachedSender.orElseGet(this::computeSender); + } + return sender; + } + + private Address computeSender() { ++ if (pqAuthorization.isPresent()) { ++ // AIP-20: no recovery; the sender is derived from the post-quantum public key. ++ final Address pqSender = pqAuthorization.get().senderAddress(); ++ senderCache.put(getHash(), pqSender); ++ return pqSender; ++ } + final SECPPublicKey publicKey = + signatureAlgorithm + .recoverPublicKeyFromSignature(getOrComputeSenderRecoveryHash(), signature) + .orElseThrow( + () -> + new IllegalStateException( + "Cannot recover public key from signature for " + this)); + final Address calculatedSender = + Address.extract(Bytes32.wrap(Hash.hash(publicKey.getEncodedBytes()).getBytes())); + senderCache.put(getHash(), calculatedSender); + return calculatedSender; + } + + /** + * Returns the public key extracted from the signature. + * + * @return the public key + */ + public Optional getPublicKey() { ++ if (pqAuthorization.isPresent()) { ++ return Optional.of(pqAuthorization.get().publicKey().toHexString()); ++ } + return signatureAlgorithm + .recoverPublicKeyFromSignature(getOrComputeSenderRecoveryHash(), signature) + .map(SECPPublicKey::toString); + } + + private Bytes32 getOrComputeSenderRecoveryHash() { + if (hashNoSignature == null) { + hashNoSignature = + computeSenderRecoveryHash( + transactionType, + nonce, + gasPrice.orElse(null), + maxPriorityFeePerGas.orElse(null), + maxFeePerGas.orElse(null), + maxFeePerBlobGas.orElse(null), + gasLimit, + to, + value, + payload, + maybeAccessList, + versionedHashes.orElse(null), + maybeCodeDelegationList, +- chainId); ++ chainId, ++ pqAuthorization); + } + return hashNoSignature; + } + + /** + * Writes the transaction to RLP using the block body format + * + * @param out the output to write the transaction to + */ + public void writeTo(final RLPOutput out) { + writeTo(out, EncodingContext.BLOCK_BODY); + } + + /** + * Writes the transaction to RLP using the specified format + * + * @param out the output to write the transaction to + * @param context the serialization format to use + */ + public void writeTo(final RLPOutput out, final EncodingContext context) { + TransactionEncoder.encodeRLP(this, out, context); + } + + @Override + public Bytes encoded() { + final BytesValueRLPOutput rplOutput = new BytesValueRLPOutput(); + writeTo(rplOutput); + return rplOutput.encoded(); + } + + @Override + public BigInteger getR() { +- return signature.getR(); ++ return signature == null ? null : signature.getR(); + } + + @Override + public BigInteger getS() { +- return signature.getS(); ++ return signature == null ? null : signature.getS(); + } + + @Override + public BigInteger getV() { + if (transactionType != null && transactionType != TransactionType.FRONTIER) { + // EIP-2718 typed transaction, use yParity: + return null; + } else { + final BigInteger recId = BigInteger.valueOf(signature.getRecId()); + return chainId + .map(bigInteger -> recId.add(REPLAY_PROTECTED_V_BASE).add(TWO.multiply(bigInteger))) + .orElseGet(() -> recId.add(REPLAY_UNPROTECTED_V_BASE)); + } + } + + @Override + public BigInteger getYParity() { + if (transactionType != null && transactionType != TransactionType.FRONTIER) { +- // EIP-2718 typed transaction, return yParity: +- return BigInteger.valueOf(signature.getRecId()); ++ // EIP-2718 typed transaction, return yParity (none for an AIP-20 transaction): ++ return signature == null ? null : BigInteger.valueOf(signature.getRecId()); + } else { + // legacy types never return yParity + return null; + } + } + + /** + * Returns the transaction hash. + * + * @return the transaction hash + */ + @Override + public Hash getHash() { + if (hash == null) { + memoizeHashAndSizeForBlockInclusion(); + } + return hash; + } + + /** +@@ -742,129 +777,162 @@ public class Transaction + @Override + public TransactionType getType() { + return this.transactionType; + } + + @Override + public Optional> getVersionedHashes() { + return versionedHashes; + } + + @Override + public Optional getBlobsWithCommitments() { + return blobsWithCommitments; + } + + @Override + public Optional> getCodeDelegationList() { + return maybeCodeDelegationList; + } + ++ @Override ++ public Optional getPqAuthorization() { ++ return pqAuthorization; ++ } ++ ++ /** ++ * The AIP-20 signing hash of a type-0x50 transaction: keccak256 of the signing pre-image. ++ * ++ * @return the 32-byte hash ++ */ ++ public Bytes getPqSigningHash() { ++ return getOrComputeSenderRecoveryHash(); ++ } ++ + @Override + public int codeDelegationListSize() { + return maybeCodeDelegationList.map(List::size).orElse(0); + } + + /** + * Return the list of transaction hashes extracted from the collection of Transaction passed as + * argument + * + * @param transactions a collection of transactions + * @return the list of transaction hashes + */ + public static List toHashList(final Collection transactions) { + return transactions.stream().map(Transaction::getHash).toList(); + } + + private static Bytes32 computeSenderRecoveryHash( + final TransactionType transactionType, + final long nonce, + final Wei gasPrice, + final Wei maxPriorityFeePerGas, + final Wei maxFeePerGas, + final Wei maxFeePerBlobGas, + final long gasLimit, + final Optional
to, + final Wei value, + final Payload payload, + final Optional> accessList, + final List versionedHashes, + final Optional> codeDelegationList, +- final Optional chainId) { ++ final Optional chainId, ++ final Optional pqAuthorization) { + if (transactionType.requiresChainId()) { + checkArgument(chainId.isPresent(), "Transaction type %s requires chainId", transactionType); + } + final Bytes preimage = + getPreimage( + transactionType, + nonce, + gasPrice, + maxPriorityFeePerGas, + maxFeePerGas, + maxFeePerBlobGas, + gasLimit, + to, + value, + payload.getPayloadBytes(), + accessList, + versionedHashes, + codeDelegationList, +- chainId); ++ chainId, ++ pqAuthorization); + return keccak256(preimage); + } + + @Override + public Bytes encodedPreimage() { + return getPreimage( + transactionType, + nonce, + gasPrice.orElse(null), + maxPriorityFeePerGas.orElse(null), + maxFeePerGas.orElse(null), + maxFeePerBlobGas.orElse(null), + gasLimit, + to, + value, + payload.getPayloadBytes(), + maybeAccessList, + versionedHashes.orElse(null), + maybeCodeDelegationList, +- chainId); ++ chainId, ++ pqAuthorization); + } + + private static Bytes getPreimage( + final TransactionType transactionType, + final long nonce, + final Wei gasPrice, + final Wei maxPriorityFeePerGas, + final Wei maxFeePerGas, + final Wei maxFeePerBlobGas, + final long gasLimit, + final Optional
to, + final Wei value, + final Bytes payload, + final Optional> accessList, + final List versionedHashes, + final Optional> codeDelegationList, +- final Optional chainId) { ++ final Optional chainId, ++ final Optional pqAuthorization) { + final Bytes preimage = + switch (transactionType) { ++ case AERE_PQ -> ++ AerePqTransactionEncoder.signingPreimage( ++ nonce, ++ maxPriorityFeePerGas, ++ maxFeePerGas, ++ gasLimit, ++ to, ++ value, ++ payload, ++ chainId, ++ accessList, ++ pqAuthorization.orElseThrow( ++ () -> ++ new IllegalStateException( ++ "Developer error: an AIP-20 transaction must carry its authorization here"))); + case FRONTIER -> frontierPreimage(nonce, gasPrice, gasLimit, to, value, payload, chainId); + case EIP1559 -> + eip1559Preimage( + nonce, + maxPriorityFeePerGas, + maxFeePerGas, + gasLimit, + to, + value, + payload, + chainId, + accessList); + case BLOB -> + blobPreimage( + nonce, + maxPriorityFeePerGas, + maxFeePerGas, + maxFeePerBlobGas, + gasLimit, + to, +@@ -1068,89 +1136,92 @@ public class Transaction + authorizationList, rlpOutput); + rlpOutput.endList(); + }); + return Bytes.concatenate(Bytes.of(TransactionType.DELEGATE_CODE.getSerializedType()), encoded); + } + + @Override + public boolean equals(final Object other) { + if (!(other instanceof Transaction that)) { + return false; + } + return Objects.equals(this.chainId, that.chainId) + && this.gasLimit == that.gasLimit + && Objects.equals(this.gasPrice, that.gasPrice) + && Objects.equals(this.maxPriorityFeePerGas, that.maxPriorityFeePerGas) + && Objects.equals(this.maxFeePerGas, that.maxFeePerGas) + && Objects.equals(this.maxFeePerBlobGas, that.maxFeePerBlobGas) + && this.nonce == that.nonce + && Objects.equals(this.payload, that.payload) + && Objects.equals(this.signature, that.signature) ++ && Objects.equals(this.pqAuthorization, that.pqAuthorization) + && Objects.equals(this.to, that.to) + && Objects.equals(this.value, that.value) + && Objects.equals(this.getV(), that.getV()); + } + + @Override + public int hashCode() { + return Objects.hash( + nonce, + gasPrice, + maxPriorityFeePerGas, + maxFeePerGas, + maxFeePerBlobGas, + gasLimit, + to, + value, + payload.getPayloadBytes(), + signature, ++ pqAuthorization, + chainId); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append( + transactionType.supportsBlob() + ? "Blob" + : isContractCreation() ? "ContractCreation" : "MessageCall") + .append("{"); + sb.append("type=").append(getType()).append(", "); + sb.append("nonce=").append(getNonce()).append(", "); + getGasPrice() + .ifPresent(gp -> sb.append("gasPrice=").append(gp.toHumanReadableString()).append(", ")); + if (getMaxPriorityFeePerGas().isPresent() && getMaxFeePerGas().isPresent()) { + sb.append("maxPriorityFeePerGas=") + .append(getMaxPriorityFeePerGas().map(Wei::toHumanReadableString).get()) + .append(", "); + sb.append("maxFeePerGas=") + .append(getMaxFeePerGas().map(Wei::toHumanReadableString).get()) + .append(", "); + getMaxFeePerBlobGas() + .ifPresent( + wei -> + sb.append("maxFeePerBlobGas=").append(wei.toHumanReadableString()).append(", ")); + } + sb.append("gasLimit=").append(getGasLimit()).append(", "); + if (getTo().isPresent()) sb.append("to=").append(getTo().get()).append(", "); + sb.append("value=").append(getValue()).append(", "); + sb.append("sig=").append(getSignature()).append(", "); ++ pqAuthorization.ifPresent(a -> sb.append("pq=").append(a).append(", ")); + if (chainId.isPresent()) sb.append("chainId=").append(getChainId().get()).append(", "); + if (transactionType.equals(TransactionType.ACCESS_LIST)) { + sb.append("accessList=").append(maybeAccessList).append(", "); + } + if (versionedHashes.isPresent()) { + final List vhs = versionedHashes.get(); + if (!vhs.isEmpty()) { + sb.append("versionedHashes=["); + sb.append( + vhs.get(0) + .toString()); // can't be empty if present, as this is checked in the constructor + for (int i = 1; i < vhs.size(); i++) { + sb.append(", ").append(vhs.get(i).toString()); + } + sb.append("], "); + } + } + if (transactionType.supportsBlob() && this.blobsWithCommitments.isPresent()) { + sb.append("numberOfBlobs=").append(blobsWithCommitments.get().getBlobs().size()).append(", "); + } +@@ -1231,40 +1302,41 @@ public class Transaction + new Transaction( + true, + transactionType, + nonce, + gasPrice, + maxPriorityFeePerGas, + maxFeePerGas, + maxFeePerBlobGas, + gasLimit, + detachedTo, + value, + signature, + payload, + detachedAccessList, + sender, + chainId, + detachedVersionedHashes, + detachedBlobsWithCommitments, + detachedCodeDelegationList, + Optional.empty(), ++ pqAuthorization.map(AerePqAuthorization::detachedCopy), + Optional.ofNullable(hash), + Optional.of(sizeForAnnouncement), + Optional.of(sizeForBlockInclusion)); + + // copy also the computed fields, to avoid to recompute them + copiedTx.sender = this.sender; + copiedTx.hashNoSignature = this.hashNoSignature; + + return copiedTx; + } + + private AccessListEntry accessListDetachedCopy(final AccessListEntry accessListEntry) { + final Address detachedAddress = Address.wrap(accessListEntry.address().getBytes().copy()); + final var detachedStorage = accessListEntry.storageKeys().stream().map(Bytes32::copy).toList(); + return new AccessListEntry(detachedAddress, detachedStorage); + } + + private CodeDelegation codeDelegationDetachedCopy(final CodeDelegation codeDelegation) { + final Address detachedAddress = Address.wrap(codeDelegation.address().getBytes().copy()); + return new org.hyperledger.besu.ethereum.core.CodeDelegation( +@@ -1313,62 +1385,64 @@ public class Transaction + protected long gasLimit = -1L; + + protected Optional
to = Optional.empty(); + + protected Wei value; + + protected SECPSignature signature; + + protected Payload payload; + + protected Optional> accessList = Optional.empty(); + + protected Address sender; + + protected Optional chainId = Optional.empty(); + protected Optional v = Optional.empty(); + protected List versionedHashes = null; + private BlobsWithCommitments blobsWithCommitments; + protected Optional> codeDelegationAuthorizations = Optional.empty(); + protected Bytes rawRlp = null; ++ protected Optional pqAuthorization = Optional.empty(); + private Optional hash = Optional.empty(); + private Optional sizeForAnnouncement = Optional.empty(); + private Optional sizeForBlockInclusion = Optional.empty(); + + public Builder copiedFrom(final Transaction toCopy) { + this.transactionType = toCopy.transactionType; + this.nonce = toCopy.nonce; + this.gasPrice = toCopy.gasPrice.orElse(null); + this.maxPriorityFeePerGas = toCopy.maxPriorityFeePerGas.orElse(null); + this.maxFeePerGas = toCopy.maxFeePerGas.orElse(null); + this.maxFeePerBlobGas = toCopy.maxFeePerBlobGas.orElse(null); + this.gasLimit = toCopy.gasLimit; + this.to = toCopy.to; + this.value = toCopy.value; + this.signature = toCopy.signature; + this.payload = toCopy.payload; + this.accessList = toCopy.maybeAccessList; + this.sender = toCopy.sender; + this.chainId = toCopy.chainId; + this.versionedHashes = toCopy.versionedHashes.orElse(null); + this.blobsWithCommitments = toCopy.blobsWithCommitments.orElse(null); + this.codeDelegationAuthorizations = toCopy.maybeCodeDelegationList; ++ this.pqAuthorization = toCopy.pqAuthorization; + return this; + } + + public Builder type(final TransactionType transactionType) { + this.transactionType = transactionType; + return this; + } + + public Builder chainId(final BigInteger chainId) { + this.chainId = Optional.of(chainId); + return this; + } + + public Builder v(final BigInteger v) { + this.v = Optional.of(v); + return this; + } + + public Builder gasPrice(final Wei gasPrice) { + this.gasPrice = gasPrice; +@@ -1426,129 +1500,138 @@ public class Transaction + public Builder sender(final Address sender) { + this.sender = sender; + return this; + } + + public Builder signature(final SECPSignature signature) { + this.signature = signature; + return this; + } + + public Builder versionedHashes(final List versionedHashes) { + this.versionedHashes = versionedHashes; + return this; + } + + public Builder rawRlp(final Bytes rawRlp) { + this.rawRlp = rawRlp; + return this; + } + ++ public Builder pqAuthorization(final AerePqAuthorization pqAuthorization) { ++ this.pqAuthorization = Optional.ofNullable(pqAuthorization); ++ return this; ++ } ++ + public Builder hash(final Hash hash) { + this.hash = Optional.ofNullable(hash); + return this; + } + + public Builder sizeForAnnouncement(final int sizeForAnnouncement) { + this.sizeForAnnouncement = Optional.of(sizeForAnnouncement); + return this; + } + + public Builder sizeForBlockInclusion(final int sizeForBlockInclusion) { + this.sizeForBlockInclusion = Optional.of(sizeForBlockInclusion); + return this; + } + + public Builder guessType() { +- if (codeDelegationAuthorizations.isPresent()) { ++ if (pqAuthorization.isPresent()) { ++ transactionType = TransactionType.AERE_PQ; ++ } else if (codeDelegationAuthorizations.isPresent()) { + transactionType = TransactionType.DELEGATE_CODE; + } else if (versionedHashes != null && !versionedHashes.isEmpty()) { + transactionType = TransactionType.BLOB; + } else if (maxPriorityFeePerGas != null || maxFeePerGas != null) { + transactionType = TransactionType.EIP1559; + } else if (accessList.isPresent()) { + transactionType = TransactionType.ACCESS_LIST; + } else { + transactionType = TransactionType.FRONTIER; + } + return this; + } + + public TransactionType getTransactionType() { + return transactionType; + } + + public Transaction build() { + if (transactionType == null) guessType(); + return new Transaction( + false, + transactionType, + nonce, + Optional.ofNullable(gasPrice), + Optional.ofNullable(maxPriorityFeePerGas), + Optional.ofNullable(maxFeePerGas), + Optional.ofNullable(maxFeePerBlobGas), + gasLimit, + to, + value, + signature, + payload, + accessList, + sender, + chainId, + Optional.ofNullable(versionedHashes), + Optional.ofNullable(blobsWithCommitments), + codeDelegationAuthorizations, + Optional.ofNullable(rawRlp), ++ pqAuthorization, + hash, + sizeForAnnouncement, + sizeForBlockInclusion); + } + + public Transaction signAndBuild(final KeyPair keys) { + checkState( + signature == null, "The transaction signature has already been provided to this builder"); + signature(computeSignature(keys)); + sender( + Address.extract( + Bytes32.wrap(Hash.hash(keys.getPublicKey().getEncodedBytes()).getBytes()))); + return build(); + } + + SECPSignature computeSignature(final KeyPair keys) { + return SignatureAlgorithmFactory.getInstance() + .sign( + computeSenderRecoveryHash( + transactionType, + nonce, + gasPrice, + maxPriorityFeePerGas, + maxFeePerGas, + maxFeePerBlobGas, + gasLimit, + to, + value, + payload, + accessList, + versionedHashes, + codeDelegationAuthorizations, +- chainId), ++ chainId, ++ Optional.empty()), + keys); + } + + public Builder kzgBlobs( + final BlobType blobType, + final List kzgCommitments, + final List blobs, + final List kzgProofs) { + if (this.versionedHashes == null || this.versionedHashes.isEmpty()) { + this.versionedHashes = + kzgCommitments.stream() + .map(c -> new VersionedHash(SHA256_VERSION_ID, Sha256Hash.sha256(c.getData()))) + .toList(); + } + this.blobsWithCommitments = + new BlobsWithCommitments(blobType, kzgCommitments, blobs, kzgProofs, versionedHashes); + return this; + } + + public Builder blobsWithCommitments(final BlobsWithCommitments blobsWithCommitments) { +diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/AerePqTransactionDecoder.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/AerePqTransactionDecoder.java +new file mode 100755 +index 000000000..29eaa6093 +--- /dev/null ++++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/AerePqTransactionDecoder.java +@@ -0,0 +1,111 @@ ++/* ++ * Copyright contributors to the AERE Network. ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with ++ * the License. You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on ++ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the ++ * specific language governing permissions and limitations under the License. ++ * ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++package org.hyperledger.besu.ethereum.core.encoding; ++ ++import org.hyperledger.besu.datatypes.AccessListEntry; ++import org.hyperledger.besu.datatypes.Address; ++import org.hyperledger.besu.datatypes.AerePqAlgorithm; ++import org.hyperledger.besu.datatypes.AerePqAuthorization; ++import org.hyperledger.besu.datatypes.Hash; ++import org.hyperledger.besu.datatypes.TransactionType; ++import org.hyperledger.besu.datatypes.Wei; ++import org.hyperledger.besu.ethereum.core.Transaction; ++import org.hyperledger.besu.ethereum.rlp.RLP; ++import org.hyperledger.besu.ethereum.rlp.RLPInput; ++ ++import java.math.BigInteger; ++ ++import org.apache.tuweni.bytes.Bytes; ++ ++/** ++ * Decodes an AIP-20 type-0x50 transaction: {@code 0x50 || rlp([chain_id, nonce, ++ * max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to, value, data, access_list, alg_id, ++ * public_key, signature])}. ++ * ++ *

The decoder is strict on shape: an unknown {@code alg_id} or a key/signature of the wrong ++ * length is rejected here, before any cryptography runs, with an {@link IllegalArgumentException} ++ * that the callers of {@link TransactionDecoder} already treat as "malformed transaction". ++ */ ++public class AerePqTransactionDecoder { ++ ++ private AerePqTransactionDecoder() { ++ // static ++ } ++ ++ /** ++ * Decodes the opaque bytes of a type-0x50 transaction. ++ * ++ * @param input the opaque bytes, type byte included ++ * @return the transaction ++ */ ++ public static Transaction decode(final Bytes input) { ++ final RLPInput txRlp = RLP.input(input.slice(1)); // skip the transaction type byte ++ txRlp.enterList(); ++ final BigInteger chainId = txRlp.readBigIntegerScalar(); ++ final Transaction.Builder builder = ++ Transaction.builder() ++ .type(TransactionType.AERE_PQ) ++ .chainId(chainId) ++ .nonce(txRlp.readLongScalar()) ++ .maxPriorityFeePerGas(Wei.of(txRlp.readUInt256Scalar())) ++ .maxFeePerGas(Wei.of(txRlp.readUInt256Scalar())) ++ .gasLimit(txRlp.readLongScalar()) ++ .to(txRlp.readBytes(v -> v.isEmpty() ? null : Address.wrap(v))) ++ .value(Wei.of(txRlp.readUInt256Scalar())) ++ .payload(txRlp.readBytes()) ++ .rawRlp(txRlp.raw()) ++ .accessList( ++ txRlp.readList( ++ accessListEntryRLPInput -> { ++ accessListEntryRLPInput.enterList(); ++ final AccessListEntry accessListEntry = ++ new AccessListEntry( ++ Address.wrap(accessListEntryRLPInput.readBytes()), ++ accessListEntryRLPInput.readList(RLPInput::readBytes32)); ++ accessListEntryRLPInput.leaveList(); ++ return accessListEntry; ++ })) ++ .sizeForAnnouncement(input.size()) ++ .sizeForBlockInclusion(input.size()) ++ .hash(Hash.hash(input)); ++ final int algId = txRlp.readIntScalar(); ++ final AerePqAlgorithm algorithm = ++ AerePqAlgorithm.byId(algId) ++ .orElseThrow( ++ () -> ++ new IllegalArgumentException( ++ "AIP-20: unknown post-quantum algorithm identifier " + algId)); ++ final Bytes publicKey = txRlp.readBytes(); ++ final Bytes signature = txRlp.readBytes(); ++ final AerePqAuthorization authorization = ++ new AerePqAuthorization(algorithm, publicKey, signature); ++ if (!authorization.lengthsValid()) { ++ throw new IllegalArgumentException( ++ "AIP-20: " ++ + algorithm ++ + " expects a " ++ + algorithm.publicKeyLength() ++ + "-byte public key and a " ++ + algorithm.signatureLength() ++ + "-byte signature, got " ++ + publicKey.size() ++ + " and " ++ + signature.size()); ++ } ++ final Transaction transaction = builder.pqAuthorization(authorization).build(); ++ txRlp.leaveList(); ++ return transaction; ++ } ++} +diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/AerePqTransactionEncoder.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/AerePqTransactionEncoder.java +new file mode 100755 +index 000000000..f2de33f67 +--- /dev/null ++++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/AerePqTransactionEncoder.java +@@ -0,0 +1,152 @@ ++/* ++ * Copyright contributors to the AERE Network. ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with ++ * the License. You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on ++ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the ++ * specific language governing permissions and limitations under the License. ++ * ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++package org.hyperledger.besu.ethereum.core.encoding; ++ ++import static org.hyperledger.besu.ethereum.core.encoding.AccessListTransactionEncoder.writeAccessList; ++ ++import org.hyperledger.besu.datatypes.AccessListEntry; ++import org.hyperledger.besu.datatypes.Address; ++import org.hyperledger.besu.datatypes.AerePqAuthorization; ++import org.hyperledger.besu.datatypes.BytesHolder; ++import org.hyperledger.besu.datatypes.TransactionType; ++import org.hyperledger.besu.datatypes.Wei; ++import org.hyperledger.besu.ethereum.core.Transaction; ++import org.hyperledger.besu.ethereum.rlp.RLP; ++import org.hyperledger.besu.ethereum.rlp.RLPOutput; ++ ++import java.math.BigInteger; ++import java.util.List; ++import java.util.Optional; ++ ++import org.apache.tuweni.bytes.Bytes; ++ ++/** ++ * Encodes an AIP-20 type-0x50 transaction and computes its signing pre-image. ++ * ++ *

Signing pre-image (AIP-20): {@code 0x50 || rlp([chain_id, nonce, max_priority_fee_per_gas, ++ * max_fee_per_gas, gas_limit, to, value, data, access_list, alg_id, public_key])}; the signing hash ++ * is its keccak256 and is what ML-DSA signs, in the pure (non pre-hash) interface with an empty ++ * context string. ++ */ ++public class AerePqTransactionEncoder { ++ ++ private AerePqTransactionEncoder() { ++ // static ++ } ++ ++ /** ++ * Writes the payload list of the transaction (everything after the type byte). ++ * ++ * @param transaction the transaction, type 0x50 ++ * @param out the RLP output ++ */ ++ public static void encode(final Transaction transaction, final RLPOutput out) { ++ final AerePqAuthorization auth = ++ transaction ++ .getPqAuthorization() ++ .orElseThrow( ++ () -> ++ new IllegalStateException( ++ "type 0x50 transaction without a post-quantum authorization")); ++ out.startList(); ++ writeUnsignedFields( ++ transaction.getChainId().orElseThrow(), ++ transaction.getNonce(), ++ transaction.getMaxPriorityFeePerGas().orElseThrow(), ++ transaction.getMaxFeePerGas().orElseThrow(), ++ transaction.getGasLimit(), ++ transaction.getTo(), ++ transaction.getValue(), ++ transaction.getPayload(), ++ transaction.getAccessList(), ++ auth, ++ out); ++ out.writeBytes(auth.signature()); ++ out.endList(); ++ } ++ ++ /** ++ * The signing pre-image of AIP-20 for the given fields. ++ * ++ * @param nonce the nonce ++ * @param maxPriorityFeePerGas the max priority fee per gas ++ * @param maxFeePerGas the max fee per gas ++ * @param gasLimit the gas limit ++ * @param to the recipient, empty for contract creation ++ * @param value the value ++ * @param payload the calldata ++ * @param chainId the chain id (mandatory for this type) ++ * @param accessList the access list ++ * @param authorization the authorization whose algorithm and public key are covered ++ * @return {@code 0x50 || rlp([...])} ++ */ ++ public static Bytes signingPreimage( ++ final long nonce, ++ final Wei maxPriorityFeePerGas, ++ final Wei maxFeePerGas, ++ final long gasLimit, ++ final Optional

to, ++ final Wei value, ++ final Bytes payload, ++ final Optional chainId, ++ final Optional> accessList, ++ final AerePqAuthorization authorization) { ++ final Bytes encoded = ++ RLP.encode( ++ rlpOutput -> { ++ rlpOutput.startList(); ++ writeUnsignedFields( ++ chainId.orElseThrow( ++ () -> new IllegalStateException("type 0x50 requires a chain id")), ++ nonce, ++ maxPriorityFeePerGas, ++ maxFeePerGas, ++ gasLimit, ++ to, ++ value, ++ payload, ++ accessList, ++ authorization, ++ rlpOutput); ++ rlpOutput.endList(); ++ }); ++ return Bytes.concatenate(Bytes.of(TransactionType.AERE_PQ.getSerializedType()), encoded); ++ } ++ ++ private static void writeUnsignedFields( ++ final BigInteger chainId, ++ final long nonce, ++ final Wei maxPriorityFeePerGas, ++ final Wei maxFeePerGas, ++ final long gasLimit, ++ final Optional
to, ++ final Wei value, ++ final Bytes payload, ++ final Optional> accessList, ++ final AerePqAuthorization authorization, ++ final RLPOutput out) { ++ out.writeBigIntegerScalar(chainId); ++ out.writeLongScalar(nonce); ++ out.writeUInt256Scalar(maxPriorityFeePerGas); ++ out.writeUInt256Scalar(maxFeePerGas); ++ out.writeLongScalar(gasLimit); ++ out.writeBytes(to.map(BytesHolder::getBytes).map(Bytes::copy).orElse(Bytes.EMPTY)); ++ out.writeUInt256Scalar(value); ++ out.writeBytes(payload); ++ writeAccessList(out, accessList); ++ out.writeIntScalar(authorization.algorithm().id()); ++ out.writeBytes(authorization.publicKey()); ++ } ++} +diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/TransactionDecoder.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/TransactionDecoder.java +index 23539c386..65aff2e43 100644 +--- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/TransactionDecoder.java ++++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/TransactionDecoder.java +@@ -1,64 +1,71 @@ + /* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 ++ * ++ * Modifications Copyright contributors to the Aere Network. ++ * ++ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the ++ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was ++ * found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch. + */ + package org.hyperledger.besu.ethereum.core.encoding; + + import static com.google.common.base.Preconditions.checkNotNull; + + import org.hyperledger.besu.datatypes.TransactionType; + import org.hyperledger.besu.ethereum.core.Transaction; + import org.hyperledger.besu.ethereum.rlp.RLP; + import org.hyperledger.besu.ethereum.rlp.RLPInput; + + import java.util.EnumMap; + import java.util.Map; + import java.util.Optional; + + import org.apache.tuweni.bytes.Bytes; + + public class TransactionDecoder { + + @FunctionalInterface + interface Decoder { + Transaction decode(Bytes input); + } + + private static final EnumMap TRANSACTION_DECODERS = + new EnumMap<>( + Map.of( + TransactionType.FRONTIER, FrontierTransactionDecoder::decode, + TransactionType.ACCESS_LIST, AccessListTransactionDecoder::decode, + TransactionType.EIP1559, EIP1559TransactionDecoder::decode, + TransactionType.BLOB, BlobTransactionDecoder::decode, +- TransactionType.DELEGATE_CODE, CodeDelegationTransactionDecoder::decode)); ++ TransactionType.DELEGATE_CODE, CodeDelegationTransactionDecoder::decode, ++ TransactionType.AERE_PQ, AerePqTransactionDecoder::decode)); + + private static final EnumMap POOLED_TRANSACTION_DECODERS = + new EnumMap<>(Map.of(TransactionType.BLOB, BlobPooledTransactionDecoder::decode)); + + /** + * Decodes an RLP input into a transaction. If the input represents a typed transaction, it uses + * the appropriate decoder for that type. Otherwise, it uses the frontier decoder. + * + * @param rlpInput the RLP input + * @return the decoded transaction + */ + public static Transaction decodeRLP( + final RLPInput rlpInput, final EncodingContext encodingContext) { + if (isTypedTransaction(rlpInput)) { + return decodeTransaction(rlpInput, encodingContext); + } else { + return FrontierTransactionDecoder.decode(rlpInput.readAsRlp()); + } + } + +diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/TransactionEncoder.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/TransactionEncoder.java +index 7d2b7b22d..45da4a413 100644 +--- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/TransactionEncoder.java ++++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/TransactionEncoder.java +@@ -1,64 +1,72 @@ + /* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 ++ * ++ * Modifications Copyright contributors to the Aere Network. ++ * ++ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the ++ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was ++ * found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch. + */ + package org.hyperledger.besu.ethereum.core.encoding; + + import static com.google.common.base.Preconditions.checkNotNull; + + import org.hyperledger.besu.datatypes.TransactionType; + import org.hyperledger.besu.ethereum.core.Transaction; + import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; + import org.hyperledger.besu.ethereum.rlp.RLP; + import org.hyperledger.besu.ethereum.rlp.RLPOutput; + + import com.google.common.collect.ImmutableMap; + import org.apache.tuweni.bytes.Bytes; + + public class TransactionEncoder { + + @FunctionalInterface + interface Encoder { + void encode(Transaction transaction, RLPOutput output); + } + + private static final ImmutableMap TYPED_TRANSACTION_ENCODERS = + ImmutableMap.of( + TransactionType.ACCESS_LIST, + AccessListTransactionEncoder::encode, + TransactionType.EIP1559, + EIP1559TransactionEncoder::encode, + TransactionType.BLOB, + BlobTransactionEncoder::encode, + TransactionType.DELEGATE_CODE, +- CodeDelegationTransactionEncoder::encode); ++ CodeDelegationTransactionEncoder::encode, ++ TransactionType.AERE_PQ, ++ AerePqTransactionEncoder::encode); + + private static final ImmutableMap POOLED_TRANSACTION_ENCODERS = + ImmutableMap.of(TransactionType.BLOB, BlobPooledTransactionEncoder::encode); + + /** + * Encodes a transaction into RLP format. + * + * @param transaction the transaction to encode + * @param rlpOutput the RLP output stream + * @param encodingContext the encoding context + */ + public static void encodeRLP( + final Transaction transaction, + final RLPOutput rlpOutput, + final EncodingContext encodingContext) { + final TransactionType transactionType = getTransactionType(transaction); + + Bytes opaqueBytes = encodeOpaqueBytes(transaction, encodingContext); + encodeRLP(transactionType, opaqueBytes, rlpOutput); + } +diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/AerePqTransactionSupport.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/AerePqTransactionSupport.java +new file mode 100755 +index 000000000..1eaa9aee8 +--- /dev/null ++++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/AerePqTransactionSupport.java +@@ -0,0 +1,196 @@ ++/* ++ * Copyright contributors to the AERE Network. ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with ++ * the License. You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on ++ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the ++ * specific language governing permissions and limitations under the License. ++ * ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++package org.hyperledger.besu.ethereum.mainnet; ++ ++import org.hyperledger.besu.datatypes.AerePqAlgorithm; ++import org.hyperledger.besu.datatypes.AerePqAuthorization; ++import org.hyperledger.besu.ethereum.core.Transaction; ++import org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason; ++ ++import java.util.Optional; ++ ++import org.apache.tuweni.bytes.Bytes32; ++import org.bouncycastle.pqc.crypto.mldsa.MLDSAParameters; ++import org.bouncycastle.pqc.crypto.mldsa.MLDSAPublicKeyParameters; ++import org.bouncycastle.pqc.crypto.mldsa.MLDSASigner; ++import org.slf4j.Logger; ++import org.slf4j.LoggerFactory; ++ ++/** ++ * AIP-20 support on the validation path: the activation height and the ML-DSA verification of a ++ * type-0x50 transaction. ++ * ++ *

Activation follows every other Aere rule: a per-node system property, {@code ++ * aere.pqtx.forkBlock}, set uniformly on every node that validates or reads blocks. Absent means ++ * NEVER: a binary that has this code and no property behaves exactly like one that does not have ++ * it, which is what makes staging the binary ahead of the height safe. ++ * ++ *

Verification is FIPS 204 ML-DSA in the pure interface with the empty context string, over the ++ * 32-byte signing hash of AIP-20. Bouncy Castle's {@link MLDSASigner} implements exactly that ++ * interface when initialised without a context parameter. ++ */ ++public final class AerePqTransactionSupport { ++ ++ private static final Logger LOG = LoggerFactory.getLogger(AerePqTransactionSupport.class); ++ ++ /** The system property that names the activation height; absent means never. */ ++ public static final String FORK_BLOCK_PROPERTY = "aere.pqtx.forkBlock"; ++ ++ private static final long NEVER = Long.MAX_VALUE; ++ private static final long FORK_BLOCK = readForkBlock(); ++ ++ private AerePqTransactionSupport() { ++ // static ++ } ++ ++ private static long readForkBlock() { ++ final String raw = System.getProperty(FORK_BLOCK_PROPERTY); ++ if (raw == null || raw.isBlank()) { ++ LOG.info("AERE PQ-TX: {} absent, type 0x50 transactions are never accepted", FORK_BLOCK_PROPERTY); ++ return NEVER; ++ } ++ try { ++ final long value = Long.parseLong(raw.trim()); ++ if (value < 0) { ++ throw new IllegalArgumentException("negative"); ++ } ++ LOG.info("AERE PQ-TX: type 0x50 transactions accepted from block {}", value); ++ return value; ++ } catch (final IllegalArgumentException e) { ++ throw new IllegalStateException( ++ "AERE PQ-TX: " ++ + FORK_BLOCK_PROPERTY ++ + " must be a non-negative block number, got '" ++ + raw ++ + "'. Refusing to start with an unreadable consensus parameter.", ++ e); ++ } ++ } ++ ++ /** ++ * The configured activation height. ++ * ++ * @return the height, or {@link Long#MAX_VALUE} when the type is never active ++ */ ++ public static long forkBlock() { ++ return FORK_BLOCK; ++ } ++ ++ /** ++ * Whether type-0x50 transactions are valid in a block at this height. ++ * ++ * @param blockNumber the block number being validated or built ++ * @return true at or above the activation height ++ */ ++ public static boolean isActiveAt(final long blockNumber) { ++ return blockNumber >= FORK_BLOCK; ++ } ++ ++ /** ++ * The message for a type-0x50 transaction seen below the activation height. ++ * ++ * @param blockNumber the block number ++ * @return a human-readable reason ++ */ ++ public static String inactiveMessage(final long blockNumber) { ++ return FORK_BLOCK == NEVER ++ ? "AIP-20 type 0x50 transactions are not enabled on this node (" ++ + FORK_BLOCK_PROPERTY ++ + " absent)" ++ : "AIP-20 type 0x50 transactions are valid from block " ++ + FORK_BLOCK ++ + ", this is block " ++ + blockNumber; ++ } ++ ++ /** ++ * Validates the post-quantum authorization of a type-0x50 transaction: presence, exact lengths, ++ * chain id presence, and the ML-DSA signature over the AIP-20 signing hash. On success the sender ++ * is derivable from the public key (no recovery is involved). ++ * ++ * @param transaction the transaction, type 0x50 ++ * @return valid, or INVALID_SIGNATURE / INVALID_TRANSACTION_FORMAT with the reason ++ */ ++ public static ValidationResult validateAuthorization( ++ final Transaction transaction) { ++ final Optional maybeAuth = transaction.getPqAuthorization(); ++ if (maybeAuth.isEmpty()) { ++ return ValidationResult.invalid( ++ TransactionInvalidReason.INVALID_TRANSACTION_FORMAT, ++ "type 0x50 transaction carries no post-quantum authorization"); ++ } ++ final AerePqAuthorization auth = maybeAuth.get(); ++ if (!auth.lengthsValid()) { ++ return ValidationResult.invalid( ++ TransactionInvalidReason.INVALID_SIGNATURE, ++ "post-quantum key or signature length does not match " + auth.algorithm()); ++ } ++ if (transaction.getChainId().isEmpty()) { ++ return ValidationResult.invalid( ++ TransactionInvalidReason.INVALID_TRANSACTION_FORMAT, ++ "type 0x50 transaction must carry a chain id"); ++ } ++ final Bytes32 signingHash; ++ try { ++ signingHash = Bytes32.wrap(transaction.getPqSigningHash()); ++ } catch (final RuntimeException e) { ++ return ValidationResult.invalid( ++ TransactionInvalidReason.INVALID_TRANSACTION_FORMAT, ++ "type 0x50 signing pre-image could not be computed: " + e.getMessage()); ++ } ++ final boolean ok; ++ try { ++ ok = verify(auth.algorithm(), auth.publicKey().toArrayUnsafe(), signingHash.toArrayUnsafe(), auth.signature().toArrayUnsafe()); ++ } catch (final RuntimeException e) { ++ // A malformed key that the length check let through: the verifier throws, the tx is invalid. ++ return ValidationResult.invalid( ++ TransactionInvalidReason.INVALID_SIGNATURE, ++ "post-quantum verification failed to run: " + e.getClass().getSimpleName()); ++ } ++ if (!ok) { ++ return ValidationResult.invalid( ++ TransactionInvalidReason.INVALID_SIGNATURE, ++ "post-quantum signature does not verify over the AIP-20 signing hash"); ++ } ++ return ValidationResult.valid(); ++ } ++ ++ /** ++ * FIPS 204 ML-DSA verification, pure interface, empty context. ++ * ++ * @param algorithm the parameter set ++ * @param publicKey the raw public key ++ * @param message the message (here the 32-byte signing hash) ++ * @param signature the raw signature ++ * @return true when the signature verifies ++ */ ++ public static boolean verify( ++ final AerePqAlgorithm algorithm, ++ final byte[] publicKey, ++ final byte[] message, ++ final byte[] signature) { ++ final MLDSAParameters parameters = ++ switch (algorithm) { ++ case ML_DSA_44 -> MLDSAParameters.ml_dsa_44; ++ case ML_DSA_65 -> MLDSAParameters.ml_dsa_65; ++ case ML_DSA_87 -> MLDSAParameters.ml_dsa_87; ++ }; ++ final MLDSAPublicKeyParameters pub = new MLDSAPublicKeyParameters(parameters, publicKey); ++ final MLDSASigner verifier = new MLDSASigner(); ++ verifier.init(false, pub); ++ verifier.update(message, 0, message.length); ++ return verifier.verifySignature(signature); ++ } ++} +diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java +index fb71c3558..e765d882f 100644 +--- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java ++++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java +@@ -1055,41 +1055,42 @@ public abstract class MainnetProtocolSpecs { + blobSchedule.getMax(), + blobSchedule.getTarget(), + miningConfiguration.getMaxBlobsPerTransaction(), + miningConfiguration.getMaxBlobsPerBlock())) + .evmBuilder( + (gasCalculator, __) -> + MainnetEVMs.osaka(gasCalculator, chainId.orElse(BigInteger.ZERO), evmConfiguration)) + .transactionValidatorFactoryBuilder( + (evm, gasLimitCalculator, feeMarket) -> + new TransactionValidatorFactory( + evm.getGasCalculator(), + gasLimitCalculator, + feeMarket, + true, + chainId, + Set.of( + TransactionType.FRONTIER, + TransactionType.ACCESS_LIST, + TransactionType.EIP1559, + TransactionType.BLOB, +- TransactionType.DELEGATE_CODE), ++ TransactionType.DELEGATE_CODE, ++ TransactionType.AERE_PQ), + Set.of(BlobType.KZG_CELL_PROOFS), + evm.getMaxInitcodeSize())) + .transactionPoolPreProcessor(new OsakaTransactionPoolPreProcessor()) + .precompileContractRegistryBuilder(MainnetPrecompiledContractRegistries::osaka) + .blockValidatorBuilder(MainnetBlockValidatorBuilder::osaka) + .hardforkId(OSAKA); + } + + static ProtocolSpecBuilder bpo1Definition( + final Optional chainId, + final boolean enableRevertReason, + final GenesisConfigOptions genesisConfigOptions, + final EvmConfiguration evmConfiguration, + final MiningConfiguration miningConfiguration, + final boolean isParallelTxProcessingEnabled, + final BalConfiguration balConfiguration, + final MetricsSystem metricsSystem) { + ProtocolSpecBuilder builder = + osakaDefinition( + chainId, +diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java +index 6d0ff35a2..cb8ddfa43 100644 +--- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java ++++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java +@@ -1,33 +1,39 @@ + /* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 ++ * ++ * Modifications Copyright contributors to the Aere Network. ++ * ++ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the ++ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was ++ * found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch. + */ + package org.hyperledger.besu.ethereum.mainnet; + + import static org.hyperledger.besu.evm.internal.Words.clampedAdd; + import static org.hyperledger.besu.evm.worldstate.CodeDelegationHelper.getTarget; + import static org.hyperledger.besu.evm.worldstate.CodeDelegationHelper.hasCodeDelegation; + + import org.hyperledger.besu.datatypes.AccessListEntry; + import org.hyperledger.besu.datatypes.Address; + import org.hyperledger.besu.datatypes.Hash; + import org.hyperledger.besu.datatypes.TransactionType; + import org.hyperledger.besu.datatypes.Wei; + import org.hyperledger.besu.ethereum.core.ProcessableBlockHeader; + import org.hyperledger.besu.ethereum.core.Transaction; + import org.hyperledger.besu.ethereum.core.feemarket.CoinbaseFeePriceCalculator; + import org.hyperledger.besu.ethereum.mainnet.block.access.list.AccessLocationTracker; + import org.hyperledger.besu.ethereum.mainnet.block.access.list.PartialBlockAccessView; + import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; + import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; + import org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason; +@@ -191,40 +197,48 @@ public class MainnetTransactionProcessor { + operationTracer, + blockHashLookup, + transactionValidationParams, + blobGasPrice, + Optional.empty()); + } + + public TransactionProcessingResult processTransaction( + final WorldUpdater worldState, + final ProcessableBlockHeader blockHeader, + final Transaction transaction, + final Address miningBeneficiary, + final OperationTracer operationTracer, + final BlockHashLookup blockHashLookup, + final TransactionValidationParams transactionValidationParams, + final Wei blobGasPrice, + final Optional accessLocationTracker) { + try { + final var transactionValidator = transactionValidatorFactory.get(); + LOG.trace("Starting execution of {}", transaction); ++ // AIP-20: below the activation height the type is invalid in blocks, on every node alike. ++ if (transaction.getType().supportsPqAuthorization() ++ && !AerePqTransactionSupport.isActiveAt(blockHeader.getNumber())) { ++ return TransactionProcessingResult.invalid( ++ ValidationResult.invalid( ++ TransactionInvalidReason.INVALID_TRANSACTION_FORMAT, ++ AerePqTransactionSupport.inactiveMessage(blockHeader.getNumber()))); ++ } + ValidationResult validationResult = + transactionValidator.validate( + transaction, + blockHeader.getBaseFee(), + Optional.ofNullable(blobGasPrice), + transactionValidationParams); + // Make sure the transaction is intrinsically valid before trying to + // compare against a sender account (because the transaction may not + // be signed correctly to extract the sender). + if (!validationResult.isValid()) { + LOG.debug("Invalid transaction: {}", validationResult.getErrorMessage()); + return TransactionProcessingResult.invalid(validationResult); + } + + final Address senderAddress = transaction.getSender(); + final MutableAccount sender = worldState.getOrCreateSenderAccount(senderAddress); + accessLocationTracker.ifPresent(t -> t.addTouchedAccount(senderAddress)); + + validationResult = + transactionValidator.validateForSender(transaction, sender, transactionValidationParams); +diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidator.java +index af237e5a2..4c1d265a8 100644 +--- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidator.java ++++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidator.java +@@ -1,33 +1,39 @@ + /* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 ++ * ++ * Modifications Copyright contributors to the Aere Network. ++ * ++ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the ++ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was ++ * found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch. + */ + package org.hyperledger.besu.ethereum.mainnet; + + import static org.hyperledger.besu.evm.account.Account.MAX_NONCE; + import static org.hyperledger.besu.evm.internal.Words.clampedAdd; + import static org.hyperledger.besu.evm.worldstate.CodeDelegationHelper.hasCodeDelegation; + + import org.hyperledger.besu.crypto.SECPSignature; + import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; + import org.hyperledger.besu.datatypes.BlobType; + import org.hyperledger.besu.datatypes.CodeDelegation; + import org.hyperledger.besu.datatypes.Hash; + import org.hyperledger.besu.datatypes.TransactionType; + import org.hyperledger.besu.datatypes.Wei; + import org.hyperledger.besu.ethereum.GasLimitCalculator; + import org.hyperledger.besu.ethereum.core.Transaction; + import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; + import org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason; + import org.hyperledger.besu.evm.account.Account; + import org.hyperledger.besu.evm.gascalculator.GasCalculator; +@@ -341,40 +347,45 @@ public class MainnetTransactionValidator implements TransactionValidator { + return codeHash.equals(Hash.EMPTY) || hasCodeDelegation(sender.getCode()); + } + + private ValidationResult validateTransactionSignature( + final Transaction transaction) { + if (chainId.isPresent() + && (transaction.getChainId().isPresent() && !transaction.getChainId().equals(chainId))) { + return ValidationResult.invalid( + TransactionInvalidReason.WRONG_CHAIN_ID, + String.format( + "transaction was meant for chain id %s and not this chain id %s", + transaction.getChainId().get(), chainId.get())); + } + + if (chainId.isEmpty() && transaction.getChainId().isPresent()) { + return ValidationResult.invalid( + TransactionInvalidReason.REPLAY_PROTECTED_SIGNATURES_NOT_SUPPORTED, + "replay protected signatures is not supported"); + } + ++ if (transaction.getType().supportsPqAuthorization()) { ++ // AIP-20: no secp256k1 material exists; the ML-DSA authorization is what is verified. ++ return AerePqTransactionSupport.validateAuthorization(transaction); ++ } ++ + final SECPSignature signature = transaction.getSignature(); + final BigInteger halfCurveOrder = SignatureAlgorithmFactory.getInstance().getHalfCurveOrder(); + if (disallowSignatureMalleability && signature.getS().compareTo(halfCurveOrder) > 0) { + return ValidationResult.invalid( + TransactionInvalidReason.INVALID_SIGNATURE, + String.format( + "Signature s value should be less than %s, but got %s", + halfCurveOrder, signature.getS())); + } + + // org.bouncycastle.math.ec.ECCurve.AbstractFp.decompressPoint throws an + // IllegalArgumentException for "Invalid point compression" for bad signatures. + try { + transaction.getSender(); + } catch (final IllegalArgumentException e) { + return ValidationResult.invalid( + TransactionInvalidReason.INVALID_SIGNATURE, + "sender could not be extracted from transaction signature"); + } + return ValidationResult.valid(); +diff --git a/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/BlockDataGenerator.java b/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/BlockDataGenerator.java +index 25b1f2d5f..65292ac2d 100644 +--- a/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/BlockDataGenerator.java ++++ b/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/BlockDataGenerator.java +@@ -1,33 +1,39 @@ + /* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 ++ * ++ * Modifications Copyright contributors to the Aere Network. ++ * ++ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the ++ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was ++ * found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch. + */ + package org.hyperledger.besu.ethereum.core; + + import static com.google.common.base.Preconditions.checkArgument; + import static java.util.stream.Collectors.toUnmodifiableSet; + import static org.hyperledger.besu.crypto.Hash.keccak256; + import static org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider.createInMemoryWorldStateArchive; + + import org.hyperledger.besu.crypto.KeyPair; + import org.hyperledger.besu.crypto.SecureRandomProvider; + import org.hyperledger.besu.crypto.SignatureAlgorithm; + import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; + import org.hyperledger.besu.datatypes.AccessListEntry; + import org.hyperledger.besu.datatypes.Address; + import org.hyperledger.besu.datatypes.Hash; + import org.hyperledger.besu.datatypes.Log; + import org.hyperledger.besu.datatypes.LogTopic; + import org.hyperledger.besu.datatypes.LogsBloomFilter; + import org.hyperledger.besu.datatypes.StorageSlotKey; + import org.hyperledger.besu.datatypes.TransactionType; +@@ -512,40 +518,42 @@ public class BlockDataGenerator { + public Transaction transaction(final TransactionType transactionType) { + return transaction(transactionType, bytes32(), address()); + } + + public Transaction transaction(final Bytes payload) { + return transaction(transactionType(), payload); + } + + public Transaction transaction(final TransactionType transactionType, final Bytes payload) { + return transaction(transactionType, payload, address()); + } + + public Transaction transaction( + final TransactionType transactionType, final Bytes payload, final Address to) { + return switch (transactionType) { + case FRONTIER -> frontierTransaction(payload, to); + case EIP1559 -> eip1559Transaction(payload, to); + case ACCESS_LIST -> accessListTransaction(payload, to); + case BLOB -> blobTransaction(payload, to); + case DELEGATE_CODE -> null; ++ // AIP-20 transactions need ML-DSA keys; the random generator does not produce them (same as 7702) ++ case AERE_PQ -> null; + }; + } + + private Transaction accessListTransaction(final Bytes payload, final Address to) { + return Transaction.builder() + .type(TransactionType.ACCESS_LIST) + .nonce(random.nextLong()) + .gasPrice(Wei.wrap(bytesValue(4))) + .gasLimit(positiveLong()) + .to(to) + .value(Wei.wrap(bytes32())) + .payload(payload) + .accessList(accessList()) + .chainId(BigInteger.ONE) + .signAndBuild(generateKeyPair()); + } + + private List accessList() { + final List

accessedAddresses = + Stream.generate(this::address).limit(1L + random.nextInt(3)).toList(); +diff --git a/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/TransactionTestFixture.java b/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/TransactionTestFixture.java +index 2b649f856..03a72eaf1 100644 +--- a/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/TransactionTestFixture.java ++++ b/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/TransactionTestFixture.java +@@ -1,33 +1,39 @@ + /* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 ++ * ++ * Modifications Copyright contributors to the Aere Network. ++ * ++ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the ++ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was ++ * found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch. + */ + package org.hyperledger.besu.ethereum.core; + + import org.hyperledger.besu.crypto.CodeDelegationSignature; + import org.hyperledger.besu.crypto.KeyPair; + import org.hyperledger.besu.crypto.SignatureAlgorithm; + import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; + import org.hyperledger.besu.datatypes.AccessListEntry; + import org.hyperledger.besu.datatypes.Address; + import org.hyperledger.besu.datatypes.Hash; + import org.hyperledger.besu.datatypes.TransactionType; + import org.hyperledger.besu.datatypes.VersionedHash; + import org.hyperledger.besu.datatypes.Wei; + import org.hyperledger.besu.ethereum.core.encoding.CodeDelegationTransactionEncoder; + import org.hyperledger.besu.ethereum.core.kzg.BlobsWithCommitments; + import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; + + import java.math.BigInteger; + import java.util.List; + import java.util.Optional; +@@ -96,40 +102,43 @@ public class TransactionTestFixture { + break; + case BLOB: + builder.maxPriorityFeePerGas(maxPriorityFeePerGas.orElse(Wei.of(500))); + builder.maxFeePerGas(maxFeePerGas.orElse(Wei.of(5000))); + builder.accessList(accessListEntries.orElse(List.of())); + builder.maxFeePerBlobGas(maxFeePerBlobGas.orElse(Wei.ONE)); + if (blobs.isPresent()) { + builder.kzgBlobs( + blobs.get().getBlobType(), + blobs.get().getKzgCommitments(), + blobs.get().getBlobs(), + blobs.get().getKzgProofs()); + } else versionedHashes.ifPresent(builder::versionedHashes); + break; + case DELEGATE_CODE: + builder.maxPriorityFeePerGas(maxPriorityFeePerGas.orElse(Wei.of(500))); + builder.maxFeePerGas(maxFeePerGas.orElse(Wei.of(5000))); + builder.accessList(accessListEntries.orElse(List.of())); + builder.codeDelegations(codeDelegations.orElse(List.of(CODE_DELEGATION))); + break; ++ case AERE_PQ: ++ throw new IllegalArgumentException( ++ "AIP-20 transactions need ML-DSA keys; this fixture does not create them"); + } + + to.ifPresent(builder::to); + chainId.ifPresent(builder::chainId); + v.ifPresent(builder::v); + + return builder.signAndBuild(keys); + } + + public TransactionTestFixture type(final TransactionType transactionType) { + this.transactionType = transactionType; + return this; + } + + public TransactionTestFixture nonce(final long nonce) { + this.nonce = nonce; + return this; + } + + public TransactionTestFixture gasPrice(final Wei gasPrice) { +diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/AerePqTransactionTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/AerePqTransactionTest.java +new file mode 100755 +index 000000000..6f6da5369 +--- /dev/null ++++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/AerePqTransactionTest.java +@@ -0,0 +1,210 @@ ++/* ++ * Copyright contributors to the AERE Network. ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with ++ * the License. You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on ++ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the ++ * specific language governing permissions and limitations under the License. ++ * ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++package org.hyperledger.besu.ethereum.core; ++ ++import static org.assertj.core.api.Assertions.assertThat; ++import static org.assertj.core.api.Assertions.assertThatThrownBy; ++ ++import org.hyperledger.besu.datatypes.AerePqAlgorithm; ++import org.hyperledger.besu.datatypes.Address; ++import org.hyperledger.besu.datatypes.BlobType; ++import org.hyperledger.besu.datatypes.Hash; ++import org.hyperledger.besu.datatypes.TransactionType; ++import org.hyperledger.besu.datatypes.Wei; ++import org.hyperledger.besu.ethereum.GasLimitCalculator; ++import org.hyperledger.besu.ethereum.core.encoding.EncodingContext; ++import org.hyperledger.besu.ethereum.core.encoding.TransactionDecoder; ++import org.hyperledger.besu.ethereum.core.encoding.TransactionEncoder; ++import org.hyperledger.besu.ethereum.mainnet.AerePqTransactionSupport; ++import org.hyperledger.besu.ethereum.mainnet.MainnetTransactionValidator; ++import org.hyperledger.besu.ethereum.mainnet.TransactionValidationParams; ++import org.hyperledger.besu.ethereum.mainnet.ValidationResult; ++import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; ++import org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason; ++import org.hyperledger.besu.evm.gascalculator.OsakaGasCalculator; ++ ++import java.io.IOException; ++import java.io.InputStream; ++import java.math.BigInteger; ++import java.nio.charset.StandardCharsets; ++import java.util.Optional; ++import java.util.Set; ++import java.util.regex.Matcher; ++import java.util.regex.Pattern; ++ ++import org.apache.tuweni.bytes.Bytes; ++import org.junit.jupiter.api.Test; ++ ++/** ++ * AIP-20 conformance on the Besu-derived client, against the vectors produced by the reference tooling ++ * (`tools/pqtx/pqtx.mjs vectors`, @noble/post-quantum): the same bytes must decode to the same sender and ++ * hash, verify under Bouncy Castle's ML-DSA, be charged the AIP-20 gas, and every negative vector must be ++ * refused for the reason the vector names. Two independent ML-DSA implementations agreeing on the same ++ * signature is the cross-client property the AIP requires. ++ */ ++class AerePqTransactionTest { ++ ++ private static final BigInteger CHAIN_ID = BigInteger.valueOf(28001); ++ ++ private static String vector(final String id) throws IOException { ++ try (InputStream in = ++ AerePqTransactionTest.class.getResourceAsStream("/aere/pqtx/" + id + ".json")) { ++ assertThat(in).as("vector " + id + " on the test classpath").isNotNull(); ++ return new String(in.readAllBytes(), StandardCharsets.UTF_8); ++ } ++ } ++ ++ private static String field(final String json, final String name) { ++ final Matcher m = Pattern.compile("\"" + name + "\"\\s*:\\s*\"([^\"]*)\"").matcher(json); ++ assertThat(m.find()).as("field " + name).isTrue(); ++ return m.group(1); ++ } ++ ++ private static Transaction decode(final String rawHex) { ++ return TransactionDecoder.decodeOpaqueBytes( ++ Bytes.fromHexString(rawHex), EncodingContext.POOLED_TRANSACTION); ++ } ++ ++ private static MainnetTransactionValidator validator() { ++ return new MainnetTransactionValidator( ++ new OsakaGasCalculator(), ++ GasLimitCalculator.constant(), ++ FeeMarket.london(0L), ++ true, ++ Optional.of(CHAIN_ID), ++ Set.of(TransactionType.EIP1559, TransactionType.AERE_PQ), ++ Set.of(BlobType.KZG_CELL_PROOFS), ++ Integer.MAX_VALUE); ++ } ++ ++ @Test ++ void validVectorsDecodeToTheReferenceSenderAndHashAndRoundTrip() throws IOException { ++ for (final int alg : new int[] {1, 2, 3}) { ++ final String v = vector("valid-ml-dsa-" + alg); ++ final Transaction tx = decode(field(v, "raw")); ++ assertThat(tx.getType()).isEqualTo(TransactionType.AERE_PQ); ++ assertThat(tx.getPqAuthorization()).isPresent(); ++ assertThat(tx.getPqAuthorization().get().algorithm()) ++ .isEqualTo(AerePqAlgorithm.byId(alg).orElseThrow()); ++ assertThat(tx.getSignature()).as("no secp256k1 signature on a type 0x50 tx").isNull(); ++ assertThat(tx.getSender()).isEqualTo(Address.fromHexString(field(v, "sender"))); ++ assertThat(tx.getHash()).isEqualTo(Hash.fromHexString(field(v, "hash"))); ++ assertThat(tx.getPqSigningHash()).isEqualTo(Bytes.fromHexString(field(v, "signingHash"))); ++ assertThat(tx.getChainId()).contains(CHAIN_ID); ++ assertThat(tx.getR()).isNull(); ++ assertThat(tx.getYParity()).isNull(); ++ final Bytes reEncoded = TransactionEncoder.encodeOpaqueBytes(tx, EncodingContext.BLOCK_BODY); ++ assertThat(reEncoded).as("round trip, alg " + alg).isEqualTo(Bytes.fromHexString(field(v, "raw"))); ++ // a detached copy keeps everything that identifies the transaction ++ final Transaction copy = tx.detachedCopy(); ++ assertThat(copy.getHash()).isEqualTo(tx.getHash()); ++ assertThat(copy.getSender()).isEqualTo(tx.getSender()); ++ assertThat(copy).isEqualTo(tx); ++ } ++ } ++ ++ @Test ++ void validVectorsVerifyUnderBouncyCastleAndPassTheValidator() throws IOException { ++ for (final int alg : new int[] {1, 2, 3}) { ++ final Transaction tx = decode(field(vector("valid-ml-dsa-" + alg), "raw")); ++ final ValidationResult auth = ++ AerePqTransactionSupport.validateAuthorization(tx); ++ assertThat(auth.isValid()) ++ .withFailMessage(() -> "ML-DSA verification, alg " + alg + ": " + auth.getErrorMessage()) ++ .isTrue(); ++ final ValidationResult full = ++ validator() ++ .validate( ++ tx, Optional.of(Wei.of(1_000_000_000L)), Optional.of(Wei.ZERO), TransactionValidationParams.processingBlock()); ++ assertThat(full.isValid()) ++ .withFailMessage(() -> "validator, alg " + alg + ": " + full.getErrorMessage()) ++ .isTrue(); ++ } ++ } ++ ++ @Test ++ void aip20GasIsChargedForTheAuthorization() throws IOException { ++ final Transaction tx = decode(field(vector("valid-ml-dsa-2"), "raw")); ++ final long intrinsic = new OsakaGasCalculator().transactionIntrinsicGasCost(tx, 0L); ++ // 21,000 + 16 * (1,952 + 3,309) + 75,000 = 180,176 (AIP-20 worked example, empty calldata) ++ assertThat(intrinsic).isEqualTo(180_176L); ++ final Transaction tx87 = decode(field(vector("valid-ml-dsa-3"), "raw")); ++ assertThat(new OsakaGasCalculator().transactionIntrinsicGasCost(tx87, 0L)) ++ .isEqualTo(21_000L + 16L * (2_592L + 4_627L) + 100_000L); ++ } ++ ++ @Test ++ void aCorruptedSignatureIsInvalidSignature() throws IOException { ++ for (final int alg : new int[] {1, 2, 3}) { ++ final Transaction tx = decode(field(vector("bad-signature-ml-dsa-" + alg), "raw")); ++ final ValidationResult r = ++ AerePqTransactionSupport.validateAuthorization(tx); ++ assertThat(r.isValid()).isFalse(); ++ assertThat(r.getInvalidReason()).isEqualTo(TransactionInvalidReason.INVALID_SIGNATURE); ++ } ++ } ++ ++ @Test ++ void theWrongChainIdIsRefusedByTheValidator() throws IOException { ++ final Transaction tx = decode(field(vector("wrong-chain-ml-dsa-2"), "raw")); ++ final ValidationResult r = ++ validator() ++ .validate(tx, Optional.of(Wei.of(1_000_000_000L)), Optional.of(Wei.ZERO), TransactionValidationParams.processingBlock()); ++ assertThat(r.isValid()).isFalse(); ++ assertThat(r.getInvalidReason()).isEqualTo(TransactionInvalidReason.WRONG_CHAIN_ID); ++ } ++ ++ @Test ++ void aShortSignatureAndAnUnknownAlgorithmAreRefusedBeforeAnyCryptography() throws IOException { ++ for (final int alg : new int[] {1, 2, 3}) { ++ final String raw = field(vector("short-signature-ml-dsa-" + alg), "raw"); ++ assertThatThrownBy(() -> decode(raw)) ++ .isInstanceOf(IllegalArgumentException.class) ++ .hasMessageContaining("byte signature"); ++ } ++ final String unknown = field(vector("unknown-alg-0x10"), "raw"); ++ assertThatThrownBy(() -> decode(unknown)) ++ .isInstanceOf(IllegalArgumentException.class) ++ .hasMessageContaining("unknown post-quantum algorithm identifier 16"); ++ } ++ ++ @Test ++ void theTypeIsNeverActiveWithoutTheForkBlockProperty() { ++ // the test JVM does not set aere.pqtx.forkBlock, so the rule is "never": the same as a binary without it ++ assertThat(AerePqTransactionSupport.forkBlock()).isEqualTo(Long.MAX_VALUE); ++ assertThat(AerePqTransactionSupport.isActiveAt(Long.MAX_VALUE - 1)).isFalse(); ++ assertThat(AerePqTransactionSupport.inactiveMessage(1)).contains("not enabled"); ++ } ++ ++ @Test ++ void aClassicalTransactionCarriesNoAuthorizationAndStillWorks() { ++ final Transaction tx = ++ Transaction.builder() ++ .type(TransactionType.EIP1559) ++ .chainId(CHAIN_ID) ++ .nonce(0) ++ .maxPriorityFeePerGas(Wei.ONE) ++ .maxFeePerGas(Wei.ONE) ++ .gasLimit(21_000) ++ .to(Address.ZERO) ++ .value(Wei.ZERO) ++ .payload(Bytes.EMPTY) ++ .signAndBuild( ++ org.hyperledger.besu.crypto.SignatureAlgorithmFactory.getInstance().generateKeyPair()); ++ assertThat(tx.getPqAuthorization()).isEmpty(); ++ assertThat(tx.getR()).isNotNull(); ++ assertThat(new OsakaGasCalculator().transactionIntrinsicGasCost(tx, 0L)).isEqualTo(21_000L); ++ } ++} +diff --git a/ethereum/core/src/test/resources/aere/pqtx/INDEX.json b/ethereum/core/src/test/resources/aere/pqtx/INDEX.json +new file mode 100755 +index 000000000..8ae144888 +--- /dev/null ++++ b/ethereum/core/src/test/resources/aere/pqtx/INDEX.json +@@ -0,0 +1,93 @@ ++[ ++ { ++ "id": "valid-ml-dsa-1", ++ "expect": "valid", ++ "alg": 1, ++ "hash": "0x5ee00afa7d595efaffab2cf573a82e50c7d0f37bc1c2a53a11f7120155bf57b6", ++ "sender": "0xa4fe6cb101b79c0531f265f6b2584b37073087a5" ++ }, ++ { ++ "id": "bad-signature-ml-dsa-1", ++ "expect": "INVALID_SIGNATURE", ++ "alg": 1, ++ "hash": null, ++ "sender": null ++ }, ++ { ++ "id": "wrong-chain-ml-dsa-1", ++ "expect": "WRONG_CHAIN_ID (when validated on 28001)", ++ "alg": 1, ++ "hash": null, ++ "sender": null ++ }, ++ { ++ "id": "short-signature-ml-dsa-1", ++ "expect": "INVALID (length)", ++ "alg": 1, ++ "hash": null, ++ "sender": null ++ }, ++ { ++ "id": "valid-ml-dsa-2", ++ "expect": "valid", ++ "alg": 2, ++ "hash": "0xc7903f14f8aa0ffc09cdfcd433e20f07ceaa63ab9b56b84ba41fc8dab7204952", ++ "sender": "0x0602be26ec9e01dd88121f7c203ead9eecb9ad3e" ++ }, ++ { ++ "id": "bad-signature-ml-dsa-2", ++ "expect": "INVALID_SIGNATURE", ++ "alg": 2, ++ "hash": null, ++ "sender": null ++ }, ++ { ++ "id": "wrong-chain-ml-dsa-2", ++ "expect": "WRONG_CHAIN_ID (when validated on 28001)", ++ "alg": 2, ++ "hash": null, ++ "sender": null ++ }, ++ { ++ "id": "short-signature-ml-dsa-2", ++ "expect": "INVALID (length)", ++ "alg": 2, ++ "hash": null, ++ "sender": null ++ }, ++ { ++ "id": "valid-ml-dsa-3", ++ "expect": "valid", ++ "alg": 3, ++ "hash": "0xe37a3c437aacfb3f7abe9bd9da80fb8a50398a56e6b60e6fd29f08abd049d9d7", ++ "sender": "0xaee69521a942a58325bd694f2cc3d64752465d4a" ++ }, ++ { ++ "id": "bad-signature-ml-dsa-3", ++ "expect": "INVALID_SIGNATURE", ++ "alg": 3, ++ "hash": null, ++ "sender": null ++ }, ++ { ++ "id": "wrong-chain-ml-dsa-3", ++ "expect": "WRONG_CHAIN_ID (when validated on 28001)", ++ "alg": 3, ++ "hash": null, ++ "sender": null ++ }, ++ { ++ "id": "short-signature-ml-dsa-3", ++ "expect": "INVALID (length)", ++ "alg": 3, ++ "hash": null, ++ "sender": null ++ }, ++ { ++ "id": "unknown-alg-0x10", ++ "expect": "INVALID (unknown alg_id)", ++ "alg": 16, ++ "hash": null, ++ "sender": null ++ } ++] +diff --git a/ethereum/core/src/test/resources/aere/pqtx/bad-signature-ml-dsa-1.json b/ethereum/core/src/test/resources/aere/pqtx/bad-signature-ml-dsa-1.json +new file mode 100755 +index 000000000..c0a74d633 +--- /dev/null ++++ b/ethereum/core/src/test/resources/aere/pqtx/bad-signature-ml-dsa-1.json +@@ -0,0 +1,6 @@ ++{ ++ "id": "bad-signature-ml-dsa-1", ++ "expect": "INVALID_SIGNATURE", ++ "alg": 1, ++ "raw": "0x50f90ec5826d6180843b9aca0084773594008302d5709400000000000000000000000000000000000000010180c001b90520372c0f4874450fb92ace74d41bc0326c7f739589f9b86a97afa87ef65d5939e608c81a44909e41f41931349f744b7aedff8d5270b91c4a2f61192e2bb6e3e6d12231deab235359e78ccada386becb6a6a1105afc0999243ab8b363f7cd2c19b2b741c9969dcf20252f20d130dc05d9244f58d6de9b4fb4b6f0125c43b60ebe3c7272b6ae7578f1abb4bb5e2dcb31eebf4aa7089aa2bf40a0ba5eb73aaa90e45472168738cb0e866547e079075d0341c4728ed794cd449d0cdeceaf64d09173a2677e3f2621b56014df58ee8de2bbcf8cb2bf369e57a28e40b24df6d0b15d3417b7b7fce3c49230b3bb7c7080c4f1344380db37299d8c0c29a7c9e95bfc4f3ec70b767895be6ad798449f94c92f320696f8f9ec844fc9451f5ab30874e6c78673b932e24a918d5bc03f4fa647c8679631ad8b30ba4f9a1a790a535104f5079f781fa4c4aad807777327e08bd6e14b957e527650d39e2de7d30da7b2b27457d3539c2f99903054bd1a6d6301b2f83249db80dedc911059b047b5552407ed32b79b57ea4968198decf978fed532277fb189841d0c17f261f1a7c4618c33b258a71cb6b2b921c48735c1fbc375a80fe2d702e9f70db34547147f6d65153f1c508ff90aaf57dfde59ce6e56c5c52049b10fbc2640cd937bf5027d755444cd5e84aad66be4ccf04f710f0b970cbe7fc5e4e7a44f5d8fc2af94ec6df87862dabe0d85dd17e43728800d5dc8ff9e9e1fd349b9d6431f264836a447646a1ee2d73e6a4d1803d71b98510d94d53b5d07764400f413b7837f463e273980296ad5e78a62c83fb91354c25c2e28bfa19480ca41b0ca2d8beed456029eabc0ba69cf3504cf1207ab21bed0e3a60795f5b066019132212a9bd20e98b43bb32484454b7693bc63764d0ba8768da4336ad16a979b1d3e8b06e89afc4caafaddead83ad861a4596814062468d8cb2e131e71cf38c4e335bb3d1d9629251fd50a4273999a20e11d9a881fa2465c2ef32301fa8805ccb38b4656e3c815b54ef42f292d05847c0e5149b0a2ffd1719fcad10730b763406e9e453d2e4a556c397a984a7d8622466522e79f715f980279cb2d1d74d5fd15f8beb8df9c0a1fb93f0d5494ab701cd60cf2b613b484a191d873449c4815f44eebb62658ef90f7033a5e48d40c8f4ec9a25d4f1b73a150e5b03a4feec9142362e77d74d364bb8a40f9873efa2d9f458d84277fdb0063642d9ebc794356368a0b4e53f6ab0d9acf4418fa42d05c11db12cb67d6c4cbbfde4d85358abaf8042510db53c1540fcb40045b334bc54a17608e82433d1e9e53c99769b86b69b8cf1914584cdef479061718a2cc5bec5c31272e0463139721c0c3523a6ea31832b5c15aa59819304c217fbc51a2636bfff331e6dc1037e89be15c17d6708762976329353612f312b828e525c09b81afafa5ae5c25d82b70b6e55f892429a93e5eb4048ade675583e828da988f9d253123a06c0ed047adf4d93d20cc6c865f5a761fec27811305165cd4bda05f563215d0612944b88ade9d64477ae8fb441978720e70b0d77e2edafd3a2c571cd28a42cca2f289d157cec813c24e7f56051b4f28c153adfbb2c1ff90ea1fbc03cb89603387169072442f5d3081140e2256ebadcd775d2f1da92c15a9ff1ce6e980b3b1e91a7e379081ed46a4259fada21536af318eb2043ea16cece60b73acd415d239389cad9212e15bad1b1ae38f93c756635a9b87417faa844e4f43bae1f1ae68641a04f0ff1d55f9045cf7e3ed59fde5c07219b9113329356d4300885ee19c9955c5757db33c6ae75b49d035003171d02ccbd7321d6a14db0bd011d4877f04070dbda3b67bf22d083eb90974674be581b59cd495740a8a9aaaf7c6801a8bcedb531ace47da1be56ab30f1c4ccdaad6e81aa97352baca12b6c69754123bdee38c0d57bf15f0850572dc9b456234eaf37eb478f9117f952ae95a0d165d12d948b51672a03c9610a98cf332d6ad7ab8a87defb3ef7249cf8c3a815129ecf5600500702b7916ffe570cef0714e0c083a581c2971c45c4aa6b1c5226400e5fd594ea3ae97fe94a0dc7f1e0979b11a2f3d15ad54fea957992ada816f97cd76168a53ebffae36e5787b0f69e94b3e08eadefe4937ce8b383b5022e57ba7135cccd5b48e38916f1f0bdcbc5858bf93cad75195d4329ab41a518d820262b1b610f7bdbe1d6749d006a0dfe18d0be1a19fb3d8e37795b5bc37ebcd884f91459164cd2536b3496c4b25a4c74282ece5d64ab861184e942cf97f0edbe6e1293c4504d861e60d98c4f6523df27a1e80843005ad21665b3adcf46f349fb0d905d70631d2ee51c0086b252778eac27346eee65c0fc88c6db5936337b0afa7d8cec656612c81e11cc55abf421df770e6c2588c743d5540e1280be76309defaffde2b82c808976c337ee13735a21900a4686423625a0ba4819581edbe574e6d7053c5fd98d288db4104ea64fbc0e7ad11d50eb1470a882aa00ebfd983cdda14db34f7f968ccc93097cf037dceeb1375f2df4de09c8785f5fef8fbbe284566e5c9a2d38f5d2f9145b7f5087f6a89a56679680ca4180ad7da3ee45da685f0255a45db806e1379c1ed59bfe63feb7d74a239334c80ab9c1859d205ba798810adf8afc569170f94167439d137215d2ce0b2fa983ac138a3ce201cec4927c62b740912e7287417f86beb07af9c83fff843df1abd6e6f81865b82b3168528d1c473b4999b0179f1de8ae22e8ed7f698ee8463eb93fcfd4e017f29ab3f15a2e061de6deb09459d4ecc62741ffd8514d5c1f10eb3b1525ad60512ffad00e360e0d6add8ed6d6df48d63cf1e36f306bd76b1a245cdd09951727367e45b3a02d748dbfcbeea342be4cb0e1e3108e57993581409f8c6b34e2d38becfcf15c653b92c88412c087eddb0bc21eeb620317f49a9bf5d6164418a26322e1964fd676a547862c2a7ce5f13898e3fadec79a638f257532cfe977fe8ba5e96782f9e1e3e8b5f896a80d816141f0c988ea59ea321cffd23f298b76e49e92189e7f65718d14091daeb82d432470a2e472a39a154226a43c543db2bb933adad08344259ba08a65c543d2ee07b67328ca2d4e60e550b85215ea5873fedb505cc1e4db49e129249fb632ba22f11b897b9423d08f96ceaa863cd29dc16af6ac76c4388c2795250c04377277626992b085ed7be2cf1d63a2eda4b65e287081fdc4e9a3224ff57d81b3ba8d04eb88430a01c5eba35fad028e8de1812920cee2576d0bd291407c5fb70c5ce07104b9af7b98c4c709c7f019d4a66b136120b92612ad5e73dfcfc1da0fec3a7928e57d7d4769eecbaa4c269f2859d31d47d1ae40e4da5bfa6592074c5bf54b04c707b8d97455a0a606686e06bf5ef7617da2532f305267f61c5f627216b06ecc3da6c6f7b420e2dfecaa41657eab20c89927ad6eb5c4fcd513da89a637755f4a98c45cfa108f37415bbf49ee6cacfda8f22f7101a9798d844709a38f9168685f105157a937bb10d4f5b342deab8b8be8231490da52cc0c407bf942270ad93d4a9bed5c06499584835dbba75577dbbc47fd3d397f0417ade99549de4412c8893cf3e1a2ed74c234a7b60461efad57d9c9fed9eec3ebd652282b045174e19355d8cfcc10add9192ffffd428150dc3f48e0417e47d6407008c62486cd4057535a5a9ffc45ef559a51fbbece64306f64da9740108d0b51d01423379bb0a72fe050d2abf5f5acd25cff57f8e3bcdf938749c7bbf77437424ada88c167bcb6addc309b949a8e200b9a5b6797522635731b250e2405f98d054aaff4179ccb4b24159e4c033329100cadbef3da262203622d27899166f6c3c79a35daf7840853b9a3ce494bf035235cada8c31e28be56f0976ef9b96f15de57e6896a5aed57a7b5ce77cacd1c806ecf83566bee5d577f7414a106ce16ca6e8cbc144f571b1a37647041694a6409f68d15604960417a60bc4f1a878eb01e89159e701bb8fd96be45395a8b8c4f0cc5b982582741c4ccaff8961ea892f084b10646c7a27687a2d3c92324cc3a19a5b38ca2d5c573167ad54486ab3adfe9219882f947cc6a93a3827f4b51d76e90ffd1587646d921482022db8b5a22519be3a1e62e9a13ff9926e75c42e07aa295c8486df69dc96168e920e13c85eeda86dc560b9f5d4c77cb8ff001d390dc6c5a911c573ab1711df534930ef28832936c80cafcde731e999ce90741e669d93f653efba12466726fa997e7ad24d34395a02b8a4286f3604f120bf47e8ec41954e6def2de896be477ef7916f18214d28f58ae91ed8d7eae9e2df600d313b6cdb952195e9d030e0b8fc98a68cced3070f4abd2d252727320eb54b602ec861ba630ed3c5d6fc8c8b39f4f1e9c198a6dc8d5f825159ce15fcd7bea5701a225ac41bb152d1a077adc03ecf06870375f2d2618094a648b40244e4a981e7efeadc608c082dc1824c4ffdbc39afcc8c464ed5ca8dfc1540ae3a6c034364c356bf13f880abe8633fa041db78f4be0d17a4bb00959a697935c844c6ddc02222f7aebb1af2c96a757536159dc1dbc31131aefe4c198ccd2123eb5d0ca6147bbc9ab43e993508ef4ca383e2847d9fcb4c975dcb240bcfd164daa835311cf7dbde0f256deb5f3748b55c312a29a042725ecf07caa904513e71b4c0ebd3b31c2938e710e7c6be819bb3125e638ee4b54b4c8fe16df1cee7587e6bdfdcccf2e3b2aa4a54c86b0ae711f056c229c644f519f60570aed60fd3d0638dc341625fb4949d355e7d84cb478133d25730162623acbf5d36b9ed98dbd88b6bf7bf1f55006f5ba2d16297557f966cc01baca56e3f6435b724c0b6fa4cc0074efb0bb2cf0cba9a1c73d3337db7dc0c78b87746db7bbe5fc003edf608ce5d91dbd8b3cec0f738de14c8e2b6df47df91805eab5635402ff26245430641abd9511cffa6bf8d352a1f0d97ce2b08561c97a5f07a07e0ac42a2a73366442e0bf125a718553794b021640ef9f9f07040068cce2337606af46f897b7ad8ced05e14f7ab3e2bd847f810058d665a0c3b6903efd8f398c31e356b48216111d30348acf93d28dbec8e603cd16ea5d3b24f38367c52f5629fd51bec34e86ff21d4f5466a53aea7048babf3afaecec2c9de099bcb2845f66689e02306760ef5274e65f36fc61edf8112b3b414a878b8dbdc2ced0d7dae0e95c5d6e7c80a3b4cbccfe13293e454b6c757a8d9496999fb0c4d4dfe5e7f3f40e11172324323556687e95a4a6bfc4dadbec000000000000000000000000000000101a2f41" ++} +diff --git a/ethereum/core/src/test/resources/aere/pqtx/bad-signature-ml-dsa-2.json b/ethereum/core/src/test/resources/aere/pqtx/bad-signature-ml-dsa-2.json +new file mode 100755 +index 000000000..c07213da9 +--- /dev/null ++++ b/ethereum/core/src/test/resources/aere/pqtx/bad-signature-ml-dsa-2.json +@@ -0,0 +1,6 @@ ++{ ++ "id": "bad-signature-ml-dsa-2", ++ "expect": "INVALID_SIGNATURE", ++ "alg": 2, ++ "raw": "0x50f914be826d6180843b9aca008477359400830383209400000000000000000000000000000000000000010180c002b907a05424000ae9a496294dd6b99046cfdbb235ae71031b379ce274c13b0c96411740cb61021a5390fbe8fb56b642b4e5e5abb7aeab143e0037f8db283b9f2febd650265ed6b05f1b867c124aef245c1811422f92414447fd0b20844e0f1b1cb4eec052400aad52b9d773aae0015c0c390bfdde3d367bd8ef9c3c73bca3164b9d418ecbb2d25303bbd12739a858c5fac4cbfa0203b9f3c319e9bc89d17a7a244b57d6613b5caabdb528f077eff9727015b9c266d777c0ba11920118c7ec4dd59b380c40e361a5b23ce04be4c61e12a1c21e091fa0d890687cbf588944db3e34253c75cdf797378de79162e0b4ca309f550365a289f652dd6d559478912cff70a5417589f73f14640970694d7b5a6bd8f000a65c632f0326ed3d128fda96dbea68cb812634dd75e9f63813a5645336681378b6a63220c066550aba43a5b937131d41f08752d13949a115a74bf96fa1925ff4338e10df0301abe40aaa49e14a1a1d58eef005a0f1d0299267002cebeda20cadcea7419cd0c365c53aede1aa36efaf764d3d36169747cdc8ee9a09e0a0ac6bcaeaeded0cd303c965d705d1b91a8b35f408ad8a033b02ff57610bddbfa60e2114e53f20f8fcad15dffac6a494c3f4f631590f2be3422af14cba7c900c9c38796952e20c3069bea2ab54bf0edf9315903aa5cf68549f878eafcc77561b36f8b78d950871ed4914417316f868a2bc54c72d3c9929f0138a2f0cbccb7c4141e94bb8a7ae11f660861cbbe498697d7327709271460a75bd4e024de3d4d6fe5af066944c96ba87459fa948a95eeb61abbaf643190ccaa9cd18591eb6f249a6eb92db89fc95616d21b41d7d15424b80a750938a12cd57d50ef95aaf222025dc4d81df320d9bc32c608b1c5a1aac8bd14c0f7a4c4380cdd65c793a6287050ff4d609102fd91e47e7365fb55513aeca273618528c199f7bb151e9c11a0bf202147734d1be9d5d0e9318096fa567317ed9fa6a8efe201b915b9aa05ca0d19e15c9a80449cc130b398ebbac3d938f55eea8f74e024b0e33dfecdbea02b920ddc5b277146685916194bfa1f8ac7784c7ed76923d4ff9ed97d3ceac283f9b26977df0133c5fa4fb5fcec577bc152e926529f06ff50c204c083c0b4d6b8d474cf5cf7cfcf7acf76921225443eb0abf5e4c76d0a3f524243ee4e372e80ff0f76dd9d135f240114a2ebe6d69609b6c60d275f251d7dceadc600d18e0cf8bc8fa8392e61fa32b8f7481c4883e19d820b51af0f87d3f7faae44e3b908fc759c6de59361487d8e424ffbd662d57a68801110e9cb099214143253949cc479eaae0e321fecab90c63b06819ce3cf56fc2fc1419408075c658024a28950aa47d549bcdb8b5bc9aeab463466c7834ca7d73058c206a95a023f0c8ac95badd383e63041e1f0ef69f51634f5ce2f48d80f8f2e92f4a1fb5322930141fc50e1e20bd235a8b8d768d473b46711257d028e710f82f0b6bd923da05ca64ae805f708577e125b3fca2feb5656f61ded034eaae481a195628801c82db85345f9ffdc72c860cffe2264ba4be2a08fdb37a4f5f80276baba4ea637847335c89dbd35504389f68b2d61ea66d4eac5fd0057d9e757d929214a7d83daf2fc2b3f840e083792d20d772584b252ba955ae8ae8c36af648c59ae0b25d3bca19413fbd749077b6969db5847409d13e4a67387e59116fe3836a733f3253d47f9919f49618fac8edf5fd711a4239447655d6c40f479f86ef3c9d4dc1a6f9683a3f1bc5c9730025e76ea1588d43494f59823f42c207e34412f73c7e2a126501b8fb810f12995c213063d909a5a9c2653d3511ceb809efa69c638f4c977d46a2a305cc831ecc0feebf5d9356ede6c780da78fe2ad683b3312282d5142612c0d1f85108de04de5c3d5414b62826d5b75f8887dc2d932c40f306c08e5d4f69ef4d344fe4317552df9890d0961e3d857bd9b2c5b44536d8606f62f66d14ddd43b0e5d3379cf66d664616df12b1c62385a57f6a562c34e120b79d64ca39d8b23e1e07b2c03910d711f09df5b54cb0e133dd751bea7832fdb38de2f1dafae69fbdf82b6887ff0a92940299020a835b958bb01b25b8699a0846614cb256a6621ef92e4a0947df9141ed4f91eca527aef25862158a8fa712dd60dda947f6a16e077810e29542a56f9244c34a73454b9ad0d8e8c40bd503efe12c4e5bf7d0ff8e6ab642a039f4e8b575d33c0c3de77a1abfa60b0252d7a549dd791d9630c8b848988157608448d77938bfb27beaea8f9b4a259263797c5b2fbdc7f469b2769f7dcb7b9478cdf5f109322eb2f0a83163469b72d8121fe8e01df227563cbd941a1f63961e2a010c6322a812a014279583a0066492a17d1a15c07024f6b8b1561041b3db608ec1c7475ce4d086e60523e85d7a955841558a35535d6e52cb166669a69049646b94d2fdb901ffbb1446a2d7f58696bce000ccbbb6942b0ad6480d006602c228218d3a2f0bc6a5d5c73acda9fc4b0b496a7d527050f8d77b71641f030368e9494bfe8e9dbf6dd4040426da1b0423739b40450273f4a8d551637e5fe3ca8d54fc476e6278e75cebf4a4ac3ac8ef3807041c4b274568797c8caaffae7df08c6c158d92411db981ea02cc212064c3ad6a9cd1cc02087acce5ceb8d7cddaec429d199c3d90670288be6e88e6826d1f6c417df5f35139c71a62c0267adfce7cb3aa49e827165b28c8cc157a691cb52ef103d8d7ca0ebfc628904d0b2d365dd3401b620d6d93cef670e2fb90ced32fc1486a0e70aeb6fef62ce5a0dfb51fcec12164e4024b0c49ce8b65e01356d80a9571af8f69a9c7256ed549d699a31ceff21c13c482ee0c5759f96125b0099495feacf22a001b2c887b107a9ca9e84de461fbc6e143566de56c710156de5b5f4b7ffe29c0124c1fbb6a792110284c9ad713d89bbb5ed444d09408c65a9f74c5f6c4fd281d31738db82913dcd1ac1fccbba24dc933fb9a8e6c4e426fb98590d7da0252c34d866c114526ded1c9d96daea12ecbd8f2a44531dfb72f02c02b267edf0560b1fb06b087bb6ce02b2922d92f81bb7da99b90e4a2dd68aa858759584490be602369dd606a510b6228e3b941421a3006a697b79866cae011d8aa496de4f3bfffdc0a087d01323db849f6237d7ac476d18bb417f26a697388b4c0a78533f3dd763398719c9ee5bf0299786fb8c4143af8c1195103390d4494b735c06b5815c454dba2fcdd27d214c48712609ffe7ad7c082e835d7477de2358fb3c4b5162027f816ca85aa4a46c25105175e77c8e5394db6fb3999c25ce578f066329182b28fd5b68769cb07ebc67e47a537f6cb62bfd18af162113a4df7f8908e0ce953b6a9637c850f24de0fda239c25fe4352f3b381fc270384d51d1af1ef0dea8501f98ea71f26de97e98799c4b2a316a216bd4af4861ad83fa4570532029564581bb35ac543355ba3aa246b59c6a5d9a91b938f3c4f0ad6d6f567fffdb6fb78402ded5fdc544a0f5fd87e4677326ca003dffdba8fd390487a6469991506a18f376b12db5be3bdcf4a4bc2f5b34dd8f7066bec3b40a6502f414eac3cd1ed43431b033757e86cca57691b0e1f7eec68fad9423f8b0620aa94798546bc878cf29d74ae74af5244ed9b97107fb83abe25dc160ec05cda1c50b76db981aca781868a7913a3e7c98aa5a3ea8bc81270e6f565138dba33c9c7e887641fd3dafd458b9c64600151038db05725a976950307c662c2eb668e3067f943b9bfd805307729b39ed77309fd5e67ecfc56cc231057a6628fb6f528fb969378529dea2c58fa0b4d791ca591ced733e8cf93a8e19907538f45c2618612648d3e1a78492afc154b7bb0285aea97d618e74a74530b492c66c6bd31557cdc55476b3f163196ec8e87f110834ffb8c232dc77cbf2f7b490f520b3b328af44fecca860a19bee9af6a5f69f63487d7dc6b7bf1233c4735522fedd475f905f044a0a7f658a7a378958cca56a163a343a010f4b65d1bc38cd57d08e0f887293fbc106ad7b15aacca6d51558cb8478b119acde04a19b007802a8e9578121304622aee7858d1794ade88973d6fdf981ebabc98fc417d765c075bf8f76b9781dff7c60071878027c057aade8eaf820186035499e9e5fbf8791e6fc0a0e49047af312a8b1b1e80628dc4db4f45842e19096a54733d28028203aa668bd8655631a01c000c956c32ff10572fdbc95b2f26d09fca84c093ddaa122ad93598c03de9273873ece5096648a6251a27e993ec7b6ea4f2181c09bb06852799323f3e993a4b7461439f702cb9df183fcaab11b8bd2ad391924d5e6ef261fea01c91cdc10d9345c290dfc3c040318d11419563d407c8365fef3dd31d8f9d7d338f5f2cd00433d1d06547ae9c650d14d9a7e5b683f0458e7a909d90d4a57201ca5b8753c13bda3591f9cadffd0987d1c7fca1c5a0cec1e13c612d0390d7c0e2f422c97cda29959cd7b714fbeaa5da7b0bcf911c2744a9a25c7d49e93342f61e10a33fda80fadfed2668bfab8767ce80480093b4845cd5ec83082e8f9ad63236743e72842d1a3226640a549c2c1d002a176e2427b2ae612dc272e3d9fa80a904eb8f93f494a52706346c08b06c9d1d8a61a9c7112c0933910882f0da6eea3c389f4faeea51c03394eab4cc02056d704b8839afd76f0839be5ffdc873b88f9a755be6f0229b0843839a25d63c18da89be1c5da930a101913ffba01fd00f2a447d5f58ae8f21554feb47f42d40c32158219dbbb1e5e96ab3dc26cd6a435525c265b65b0bf48d68e9b3c9178a509833aa47c23e63aa9a6f74bef1e4ef863556b11c014b85ac7c157f1e64531b676eb28c68e82e0089d2bd798213c836cc354d4883dc0b607456ab7f894f0a1f73356fc05a5c97a9cf3b231890c39105b751f6fc54063a08f3d3a264ab90cb7de1d0e2f6acb92302cc9b4bb512e720e6ff8343004028ad223a70d395c5f974d3ddd380f798e80edb4b13555c3be0cb1a500e19a8b1b05f9cae85d7eed756ebcfa57828f1054296ffa48d426ceb83c69ebe1b983421f42316ca6f50bf8df0f4d4aeef85005638a304d88764ec26366a1437867c89800aee55ba428e0c07542e123d748c795560153007f768b20bb057fc2643b7db992c3b9cbfd88657b17fbe8a783da2050affb518cab4226cdf28fa97f48651bd142932835f061f8642e79d1f7ae8ff8665aa85aba24a850b2042dd37e61e665a451f17cbe9d57ed5ecb1dce9ec34c576762929dbf46964de7c44c9f6b10fcf52b48649ded2d491bbe4bb6ad578cd31cbdea6df8342dbb7f33857617250b014fc6d887a448f7a5cef25ff69dbc94733bfac1d5f8593af7fbe4759d23ba8da1f405ba179c96d8ae49eb760446d842f0d61ab08508a3e31e4953e13a1b197d00644303e8a9513199583709f41757740a0dff347c3859d3a4ec391481af349019f75ccfdba111a4c338fd8283733115ce7c114111abb157c1a5a84f7b39779a54f000827a250bff00dbe9cb7162d81f6c942b719f9eb973ac7d4652b911622396c517c6e9c7e5f839c049855567fb53ae84579388e3c87af420272558c686209fd92fc339a553f873995932d9dce52f5e60cc0889c3283f94a0d2d5d011154d9935e1dfbb7e747b393bdd8a84f601adca56d4ac5e8e6f28100a7d5522de50dbf1bd7f5604fdfa660a58dc681b4b230abbc4f594a972a73456de1b7f48975cd03313a80074f5d6701db61860f33dfcc9f06e313e8881c385a50de28ced014daf702f01c45368419b73383008b9191293f59fa87a1ceb0cee64d486842ed7c289dca3b3f2e261df644de0f739079ba5ea195468f0693a995dae8e0f7e4611196824f329fc803bc3588d7d941d1fe86a3ecdb3eb8f6d4066ecee371f9a14e5bd46a40b53e87325bab7255099b4fe7f7b09eddaefc6582feecb8dec02b5e58d735a927d052e263027fa879fceb852b9e08db4636c7152f272a43ae4037c50210e90caa3e008fbfa98ed42cd425259096fc06644edab9b768779dcd892747f18a9cecc6aa31dc27d3f12a540447653edd5bed68f50da71b7992434b74d0209746f93ea429691fcb99da0ca32df822ec4332f7eb4bbe6d2111ac84a55932b3e173e388ab96a5a8a17c7d8b8131cf566e0335b0c4a37c5ae24e73c81146031295b69ce3032fa8e4c7616558179cc561f14864190d458cb82370749d595966047de698256126a4ca23a6bbd39201b8649fbcd387474ecf5b9f732e65938fe393a2a1269b06b9b49c5d8882d70776fc823da5842d812be2a65d2f5ee0d64f8258e761e6f2d4c1a825963b367d70b9e81b46fc25e4804fe9decf3aa0e47f1eba573a24f0e4f7b8cf8a886c7ef4175bd317709feeabe46df9719e7d7a4bc2c1bbc15a58b5332ea558e160f5d3b7aa84fe54ea7a24bd8e394ab794e72db7946780272556be33b7f32a0ee651c013d8ed3b5d75bc1c4826dc94f9e3711c53be3bcdf361329544f2993a189b6431a6dda8b81cb1b7c3a69ba96c1ac84a55100c4985cc85ce272e8eb5a57c563a7ad782664d561c3400e779317122d425c9c4193414bebd4d6ad31ba916448f65e9a7ca1f856dacf9d88b695410c8491a759d2c0304e8368b39cf502d0abf7233b1ba9671bc422ce182833ead52597481ea3ea26a2cc35e12a8c83bbb263202d5e9e7a5bb8ab2a5a844d596c24fdac423981fc4dc4291944ae476d59c9a31a05f79f0fe4b39f145c4fba857fa553a17fb2d5f941d5dac8dd3dd07e48eabfe5b98f3c41e7a84986dc3a7a3bb0ed2d4232cffbc50f167d58fb52fa3b357e76430d7f0f1aff6e2d51ea8f5b98bb66c5b5687821f33b9b6c2e407ad0e8140c4d5d4f1739167f8d63284caf4a43688473cd97bab401218fbb2ea13be81bbc05a81042a02eb1843bd46ca535e778e207c79ce3fe511a55d21bf20b24bcdfb43fa62b3469fae2ebd61e362d5d663fb2c08cbde5df9d8cc1faaf6370b5328ffccde873849299d61477a9fcf7c5c96fbc51b24c0f2cc6e39a46f4a9c1f5214f2cd06db62a37e2c6f477ceac0b92eb882d7bcd1fc3721a89745473eb45eee9a4fe6ab323d28f4db957d0c5dc1ed25ebc194f744a2cfe98474868e6acf5641ad454b3626ac2a27a9db416eb1763935eaa5612e43c49e8959ebcf5133863e98c19618e936f33952699fc224f41a4e259b40228047ed6a17cf4f3a42dc76df1346acb0986984f1692b5ed28f0825a23094ddbb8c1750e9035b1df2166f142e7f179473098ed73acb9ffb8be530542f31de88d6677f805cc30ccc0b60787e61f9d195ceacf07a35d979f9c3521f23671f378a6cd58ed2b0620ff7e2def5025f878d70008744c6a53caf325293353747c94c5ddf12e3a42686f97adc4db62b5e2e91132345963b7292fc9d13b484f75d8da000000000000000000000000000000000a13171d2127" ++} +diff --git a/ethereum/core/src/test/resources/aere/pqtx/bad-signature-ml-dsa-3.json b/ethereum/core/src/test/resources/aere/pqtx/bad-signature-ml-dsa-3.json +new file mode 100755 +index 000000000..7131da266 +--- /dev/null ++++ b/ethereum/core/src/test/resources/aere/pqtx/bad-signature-ml-dsa-3.json +@@ -0,0 +1,6 @@ ++{ ++ "id": "bad-signature-ml-dsa-3", ++ "expect": "INVALID_SIGNATURE", ++ "alg": 3, ++ "raw": "0x50f91c64826d6180843b9aca00847735940083045f289400000000000000000000000000000000000000010180c003b90a20fd3c43f5fc7807747a73e1d8e78cd9e611ad6bbba9b264534594174b49082aeab827927b335526cc50c2f0368fc666efb79c34af8ee90427d1718736acaad47f5959b9b26457619c07c2dda553d55a7498e30a37c92909f6b37c246dca4e8fd24d4cccd811396d276e5d11f1e8d14bb9eb4d5ec49798202422edd57ea4d8c0e8c6388e9c93a0ff2e52f7ca2303e40300c50c926a7da22919be462e2dcbdb3ec235b14290a282ee76a04f4bf7c5e2ec5c8fc0aaac72ce4f8fcf0d42b9fc3c4049ab6dfdfa12623d89c0d96434a8e13cb6f79ad6f4a8102f9a0bfe582d967372d8928ff9d7d7393b22ec1386ee91dd196eb9c349a6a12f7504e66a096eec0a8ae98e62275f6b84fb40a81a390c6ef0102c8fd2c11d303745b8fc49e638e6f3004961724a977a18d81efc67e37d0d44558e28cf9015dfa08eff82004c4a53dfa40c2451fa03a987f70c855dbee37e36480863044165e6d46d4c43278091317beab6f301d61c8f54f94ee88fa358c9aed6e55b80b4196e79a3d7251ede25c4baef8129882a69789df3a8262941199ded7bd381de00c257199c18e9086d9ce3f72c6d95c0a4d109a3a15474805f0dc06649a16a7805e8d74bd1af7658f5f9ef726d560595ff9f072d4cfaaf61283087843b904b0f5670ca551079c759d4a2839a71505e007cb5d49b0437b87a080fab5e2efdedd1d01f0f1ab72f0e6129a2525fc6e9829b56ce9d9f287a5bd80be8b1556e4ca82eafc60ec22315448768a72e45d4d956c16ee48a0658e307f81ee0f695f1b1b747d1d533d8da720f088b64608b1edb2d4d27247b2cd35e3d94e1256a3c5c944bbafe07b55ca74df4ec0b869733ddb466717a16e98af4ec310d046c90046c024fb33fbb2ffbb105d41bdd0e0af27b1239aa4d92b6ef8d727e51372fefc3b5c8bc6eb585a0ae2dd9970009555c2c2ced5e5a1e97c659565b6139dc4f3bae788e21f9658affa8fdb6f4733f7fa83fefab91a030f8ec976b8263c17abcef46dc22456d5347eea7c30dd25bdc3caad7cb2010f06573d56b6e5d28e6e847d8df8f18d6a2655ea66fe3c8e7e4ef192b5a8aeb35f8469277657b77155142c7b1653d5eb9cf9c4d69bbc5746e0c1876e330f9016a2f5343ec60b2f35ea380169a2bdf102888e6aae7e797d0bce73834f418018e8e55492da8236a542a999d9eea55bd52ee2e59feb0329d716c78c0bfcd79dc27cd9639399d2fd99d2fc9c4900ad60bdb8221501038ec4baadd2ac4830b2bfe1e20804c818c8e089661e7d842bc4e566fa0bc2d7968a44f8df0d4e88e494fc7bebdb695d3a8edf6a2703817b0f8c1a55569ac4a8ff0931137f075797e9744135a3fec6c767af26d4592a9022271a4052b1f1f24f072bd5ceb1d19e02b0bbf31251a7bb8b723bd3da02f59ca1c0c91bc10896970a1e0f8e0fd5b755da58d70be8935f65661629a9f833b35810113ce80125f8ed7a031cfd5eb1475b597856b4ce7629a9429d840a3457bbd70c05c73c6ce84dd59f917a24d06ca8743225ab8c266e209fbadf57cdb587df96eedf54c04fb3f83ec0dddd1d8bf9051c6de4db3aa099f1779f3f30f61f432e5c8f6b7ac4ee6c6dd6fce667556df155ddb2002735fb7addd8acf74863ccfe04a3088317634eb47e56b18c79f31318ac5e448a9094b5c2c70b44dbbbff7c235cef4701d504723b032726d009c6d7f527e645a23fc1ab478e6e2699211694d575bd436d778e7ae53868c63dfb78f591d82b979585cec8bbcde61f375e783e97e8f787fb392fa4f0bc3e9010aa9ec3cf6471bb16355c3ed3fdf2ee5b95ade86371d60fb0699a851770a212d0e0eeb3f20df78ab180343fd79a9d04d8dc56d9fe3d820b27d98d12275a40d270f4181aa1a18305c8d3469a954b2e85405a394684cc234702a6cb01c04ccc941a4392cf64ba31bea700d339152fce6abb0dbe84823b8d3b18258a29414fd52c91ccbc198e101287b79d8bd0e5f4b05a14a07055e66ea37d781934840a204638bffda400113bb1344126fe1c2ac1e57bb861c3d35349b1fba0c11e12921262ae52fb2e0bee444f8197e2817b9356e10af67e794a7967055936bfcb51f4be6fc5602a8df97ddcd73afe83c02f00d4f1d8302bd2e826d4c50fd3ded9a74debd0e807b694e912a8a3a800f5b983e171898874b77ea305ede4abdf0db5ca84a8a60b184374c08066537b08d42a609b97039ff8905bccc4454188b051c5d1e574e1e8b74cc5cbded064a5374c55f17002ee9b17447fdc324a12e3f2f414b491cccf20c9f98820461f4c7d61355a30f1a6cbb2fe7403b90587298fcc9500519451e44225c750b8b32a649ea288eda389e596390a5386652dea5d0e096ee270bcab9da0c3c4ba464f44172732a748e339cbfc8f0c1ee3187225e2fb29e67ffbd84df8acf759c817d628e03a245e9ae0cf00939fa12964e8dc124659eea8798b097ba52480532fa11194e4623ff63f6cc1d17c7080bb2c1459fe8cc9b197b89ba91986b7bfa61e514bf5c60f981e63baffd9fce44af87ad73fba197e46790fc03f8a46aa6d7590f47e08daedd7f564685208ee87da84b04b11cdc0c020bb26b0abdad3c1340649130e26af9c7651533313301e8b517a84c807d86a4c0cff68566d34da4111d67bdc77ef5f49ec7d83af24a143bceaee020acce02499c725ebc341a86d91c7f63dd962ffcc09fc0b5673873ee55cea4bba69b5058e4b364fb3802832d5118774790dd12eed78330412dc877cc034309a7e292ebee955a431fd46faf7884cf3bd5ab03d02b8784af2a84bb907ba5f021fc332f638a2d4b6451ea0a2cdee6ff1621d698eadfc99d6f405003f300e321a9e80ed3b577a899fa7dd891b53cab8acaa9fd3ce02640eaa75646f134c190817675b9e4a688d0668528fa3b378749bb6b1a0bd16873e89dfc927dcf568fc3cf94ac49d289ba1fb9c58a74574c8fbac321db256bf919e9432d6a8ae9a0dae900901f436b13d0c2b753a5eb6fcef03301d7faa15ff32ec0730335b13394bff66ea83a7043f9e1df5962671ff2d2759db234a4a4fa47fd3a9359bb14ef5b5fc398836d42afcc5d5f252b1b061afc6a0815b3a7586d25eed5ee52c56d2389b8f2438649ee652b431d3cc4262c6e5ea1c0ccec21aa6123e88b4bc933098576bd78cf5bcbbc278ee682029e05e91aac2043ad3ec1d3d97f6ebc6bd5faffa5bc71328cd4351bcb9a0d8d1ea9253b97b08c8c53b20e29703568d5d7dfa2bfa6aeb3a93043e4123116a125311695a11c8538658210971d7763ed7870c1309893909e306df22fe968612531e58a91d19dfe2c658ade19c8e0900c779af8ec333d19552e42cdedbe13ff85d99b10baf8daf70951290db56bc7ea049e6b468b6546a7f3ae77c2e14770eed1ea345f0d139336af4eee686ac2e73bd4262cb802e0173fb872a77cd2c0f3421c497b0260be79ae29d04d9a9da9d7bc101ae3ec9dace1133332f05b7b82aaee577c06c0d2a8eaa8a96db8b899cb3240cc4dca4f8eab513ad5b3529063b5df3664dde0db1e0ae5c29e69f011076fc7edc4261d043f38f1c68645466c9a5e685a07393b7e2f8f74db86742ba5ac089b45a42df3f943842747674c1653aa2a8ef282f64e7ec2ce1fada994288a5f9b95da4bc083ea008308b91213c64a08868ea0fdad9fbb1b485b793bce6e4fa945668b2351232a7c8a084fb2d87a5c83e7dced6bde3743617a7c4babcbf9381694b64d7e0feeb6615440e927a63293c6b695900b1e7afad1ae612c324a88e6761c3b6fdc41d18f514be1ba3a1ca53fdbcf8c19f95bcd60223abe52d07fd7c4114dad9f347cf122f6241429da7e598959c22692910323a0a699595398f518c58deea7159920d89e3e65702bc2cabea7e9ca0bbb7002fb7be5243bba6a08a12238ead443ff4394510d25eb279885d9112c44529f294c5a2bf493a74ba8f4e38744e632680b3ce055a897d2a5468ef52e6e70dc5af55a9fb83739f6cfc6994871662b8263f2e07e21a6514624909369e0138755165dd2af5a791da63c0604b68ff160fad82a578a06e76f62bb7e951b81d8fde2dade6c3a314eec5913151c16a93a5abd5ceb59ad96197a349721ab123b42f799dbde36d7965e0e5dfcf43d8d1ae13f4a00afd8dde36137d3de87f4e2c0366c18f4b3a449ffd9afc7c742c50456f6e642f7d09e86d5aef224b20ce5d8a2bf164484df8fc87daf69e459fc78c6b974cdb8fb1dccbd5290f560e3114302886e2828ad73ce71b06186254206743076c6af0c3d1facc36695dc28db1d026686b141247e1c7a9323d544b0079023cd2adedc0479ceefc8e0c34782603d0c27d18768eb5485a85bc385ffdfd25f735688a3b9a0c6f532b2a7989c751804de0163582293a17f635fdde14e9067f12929b079f89a53dfc87b986f89f718320ede83cf4086d0d095b24b66d5e3f1bfdb08b79cd5b25657cf2b20faff1d199c16bb7f631b33970b34c12997637447e4127db1bf580b339807637a86e0248cbcf8e1d30ae6fe053e2f9b7b3d0f9e3f4813af908b633560779012f9e06af00e252bb0df9f0caf7d1e82f8deb2ede21d13f406dd9ea69445d50844c95c921d8d1cbc8afbc5ddcd4157e65cbfbbafe2670e9146a5643db049a8d696f0eae07c3d4099447ef78bdb90fc05d7cc97bf9b9dee8719464ec6b9e6de866f73914a80396dea3900a9f7b43a47e648e20a9504da92c47ac65368813e5453e82020f8861aba2534346e6137e71adb28a5ff080365d6ea26dfc30ef5500d0760749d6e4e0f93aaebed544832308b19f72e92ebb8eaec2492c6ea7972fd359a8b67a1f4053bfb1a3c6fb720b5925a7a827e59b3bd0651b8014c77b25d61db17eb169cc2fa2442c82f68fb42200c59baa468a9fc92f13273ff071124f8b31e2b4b2517ee622d29489997ecb5676fb52e3002371c01004c214757b1f510091c2377a08d058209d18feb9b524a41712b6df4b92f2b14c583d3196749f367b3bbc8ab5e9e2615c511f23ba5fb1bac2ee9436dd8d162bd96f1fb663d2b520ba7e8ddfc2a4d549b63714654cfbb37ee1dca5f723139187ada8f8d5b42ad09ff196b70720d2527b4466d44f33cce486ef2e86311c75ead31a17b16989dc63bacf0d465379d9138df7ea2ebc71f4cc3b6662e24895050e2553dbc160a567d6722964a904d43bf2684206cee3562256e98addf3038e968cf6c09c921e2c89c9c249f6ba7afd20dd27587b4143f704f2738315a91c2d40c467111b4f3bae4f77bae402145e742ba83a527aeffca13ee2290988df882feaf5a3c24b416d59e06cf4f8832a016ce55419b7bd2028545557528a0af23f17942433e13fad089fdb79449bd9d99b0324b1828791249b6789a09cdfb6ec40c0fbaf8b28ea4f72d3df5159bea7a7965b94beb1bb01d87d76ae7ce47fc333fc152038ab4e7ca64a802f96f2e92bcc53ad9cae0242c622c99b2daf005e554cd6ba127991d84841ffe3ba0674eb7930509045dd00ab91a7c6acd2f4a40e7ffc9959929851e40d417f488a29f7bb5af3847a8caa5daa0682aea870ac8b866ccfdc435971e3d862c2f651cc6eb8a33fca313a56ab8228ee9ac7dce14ea3c3fba4f58c70888ae558d8cd632a2723ba54a3d538f4e20936a2ee543e9b2141e06095c72d4ee19908febf7c248c03a78655420da644746e2d508c31ceb0b648800d95dceec72f7576502046e5dce54b5a097f625001bfc92d6e4633b293cdfa63b1fca5de9e26d03ba2f1cf3647dbdad5359ed76bf702f0621c057dea3617af678016aa13360ebb02c6cb2fa0d3f137a1a3c639c989e12383841ab7ca5a84f84c08af4b85416ce7ec3ed91636a79add207bedf9dfb2ff626387f20cb3315502b2b06f361cbf6a51fb507598af41c5a5b9cd95ba1894ca372c5e1851e62a0d3d78dab6f92bd43ba6fcd71230b6cfd843d89268872b75e3c5b471db530b7ab5451dbe63fa0bdaa7d74907cb376643e129febbe6ce10b5dffbf323dc973f097d2081e06aad65e674c5a41bb81b80556fd78691fe6b5bfad47fd1b64db577f27f22f17973cbff3a7c4aa43569bbe6843d773a2f933f472650e88e0ec6643f87dfd37e7fba915c7386a130b8d4f0ff4d0e99a633d1f863f3e8ce4e1ac5e31438d89c3b6d5ace3aa6e14e7073164ea01e853958bece389316023127f9916297a47c5d6ab5513bab874c1ae3bf2850cfab9018cbf50a7febe03ff3e8e4e55ae5632812b6a147edf051b461bfe56f01a6ab571e0553e37a57a3a9811f4fda2e189d79b74215d6fe199595d18c6543c747572f8cdc61a53ce09d9ca96b43a5af5b56b53148dd4fdfc1e22c9958caa783f2d112d8d5fd39e6955df75d35c59a1967fc0d8d3293e39a6cdf82d6846b39a74b1a538372c266b3f12d7ccf464ff7e32fe54342994f51d344302b3b2e11eed2187a741eec99ef0a79a19a854c03d8bd3fcd557b9d8bfb35f0976da327fc7e8d95fb43db8e66be5b373385fc1b68099566c7e88aefe92c7c521d084c15c8d403d87bbc975e8d687e2a51e5ce53191e3db5bdc50f475853cda7cc5a7365c41035aecb61444c2a48492a600b58300b2805be97b4e9e30526e9e62bb3248a17c3809219307ca85351495c943a865359c783cf615a809c739cce0786c6045d9c8071797001434cbae571d42cef415e28ed6adab09ed10e0713dc1412ed80b8d26d549964321559137483c81b19203080b36abcd68e841f4c78b5302da79fd6e555f390322ae67e54c55749491889d79eb2982fc4428086bd1060c4113137ddb4dd7c9b690b802e805999d4f5127f66f6b1d397dc3509504e2196513b5787407b45b928cfc3c437523daa4f876dfe061e351cb3bad99ea205ab276c4f2a5ccee1027104f09adaf55cd282d4e9e44ac2b65f7e423da1cebb0ac70691347e7a7bd0de8bc9fb2e7a08afc0075a0e85902316cd6ec9a5802c51a6d65bf3251ae712ae0f66de6cba1e51efbad53ad7e769f851799caf5fb81ef35b4da814915dcfa67553f1c67158154707d16804be27af2115559e4758b313b9da4a1131718b29a75970b3f0a5e2f2c11a0a1ed47f238c145dc4fe9e8e963d8395c67dfa94a232b8133a0d953b05455f79e0bdff0bc3de9c04d93d97084ab99229622cfe3958ac5fda6757de1696b575f6250b441e6368386751b1d2f6007d254d152fe76578fbbe164af49cda805b9325f7ea47f5e18f3de1ce38006ae1bf027d3e354d6f923d8bc6286aaf9b9a7b9e7a7fbd36a31db0d88166dc68e7eb567d8164bed1e279a8810fc21656621ff7cb42a21ee04e6086924bb62dddd16c3343db69250f7ed95aba953202640bef18dfb6c00f1ee03c670a5cc6c0f3cd7cb006590dddffa6f19d450860e289f85edb4fb0dbbd2f992eebba09cddc1b20a3c98d8739ad1875a514b09b1f23cf406c4c55cc4266e0b8b283fa445849316c30bd19254e9d035756d2773cab7f5fd56d6d3a094a717ccca43251201cfddf80ba1aa0dad34f5a3acf8476b784fd53a1aff1307a7838705112650bd25dfea21b991d801f018a5dae26edb9dd5a0ab940a94b7bbb90fd1f30f9d1dd2ea3aad6d0a2a3c42fd2641233c7d624314c3a54ce616d2ca5683d04f6fc0924f0b667be32f4de8820f9cfab963a287a1c708857ebabed9b5bcc35d539ef9bc170636cd528ba86a620195134cacbc2b8e742376124b67ed8155ab53979e281f97b184c60355b008f15f45c8b7a506b1c1f511e4aaf0bc32fee27b656b01ed6afaa4053ed674a60175b6c89a77d42963111d0940730db02ff67534fe6a296e0e01e2db6b3f0fe9648286a0f3420cd4393163662751deed6457abbb370f83ed92e9da86a0181df13b8ef33e5718ce524a700ba5f2b40f3f973a67c1c412dc251c19e6b7ceb279dc1dfc36a3567e3a98a9d96ef52b0d0734f57bd9edebedd925167f230b1c81ca938e94ceebe43d2573735873a436331a8b76c1169c6a869889cb74e5d33721bbb99ec87f160c025f570f1d11c2e3ae6a52580785d854790139510179b003418dbcee1f4fd4fa39fc134521ffd7f29cee650472638dd04d8331ee439eaa32c57b56c4fd9c9ba87516a8236dc99b8d6e4c2a48e53a6779d5e13bbd73cc7917f0cfb8e231218bf33bc3c213599eb76476558f0e38a3979cdaad6ac6b03bd0ef0aefec634fbe0e1e87bd78123a19b7352326b9def856ecfc02b06622f94e8cb2a1cb6cf10d020f789a7db775063030f15b463f08878219636f2bb94ed11a3ed9689d797a3253809c7f2e0c60672e8476a9e272075c7f808258901e611c4d535a11bfe01daccaec3a26a187ab45a9fd0bb9a7ecf2e9c4aa3177ddaca1449f19f78ebe26be2e25bca012a10649b56899339f5418925648f3253bd716336dcf65e6b4f16d2ae51fed1acbc8335c103f721f46023cc2043f568ddb033aa5f2157e52b89eed4571cb523fcc66ef5cddc90679633c2a46f45f1a86c975db4f6f37f9fcfe54809ccf75305bc9f242f26f30a0ad5f3cd71cbd31ac45ceed2dc8571142a09b7d3091a4838727b9a067927e17b7559076649b405dfd480627881489d52339d4a8943ca396e92b92066e45d525acf7785a944345d25d721d6a354b1a9f3e2afd379d1c341badd0dd913a3471a5b78a81c0b338a2984e115d003b1deec1b44fd19824279d742017248fb4434cf47ad07fc87df86e634b2d8f6e0aa572088678b9a9f767404f06c84ea8cbf65d46effbde7de1f090d3d4b9e7c7c3b2b1dfa9e591efe12ffe0fc82c9eeb7fcd9f25fe438b39c46614438d0be7e1dad11bce8b91396f867531e8c45d8c01119abc2b20be253779b5e651bd80536854e2aabccddc5f4fbfb3497111fe9a8f8a417ab4dd806745c82126524ec325c931ac1f31a3cea19e159e1dbfbfcff761cccabefad38062872fe8a45a14781ba12551befb31c21150a56ccacf9815d8f458b1f591c71ba7b0459e7c09a4eac5c9e5b2984314ecd2f7054ed055ba082204f7ed62b1d513cd0992bc10c28bc745e30b27f6ca28ac84dec6cf0ef73c971ce12b2ddcc0d6264d8f7dfbba0a20f8ff1d7387cd9d9872f4b345b409950d66ababa9852f7f5ee634dad6085666956b97e4c9142dbdce96bf42d1334c61d04ffed5ac8f12a5075ac87a7416fd52579a876c89e22008759a93ebc57279215e178662a064bca65670089ec688532468c07161f56bbafbfa5655768123e713c7aff52852978e9d6004ab3a434d9b5c73f0d90dd1fe742a169a84f0e96a189bfc5265d8c5e2d9e42f5503d04ce01f9a912bf8b9bd8c282a4d40bbeca55d14971d1d305a16eb85d02b4edd8d35baea56156ba12f4e344dc4cb34509fc780ee97d24173a75e49960ec32055710bf8119d005447c6c391365737a0cff68d9f44868b97b2357dc051ed7fcf590b32292624545935e87f25e56b3e4a2b9c39c4936b5be9030b4711f246623264e0370291b675e75e1ce9cb425e5fabcfb55e25f084ea8a1b84870135ad997f7b700876691939cf268204669790a10ddd3d38cd22252226a09fddd395482deb561ee834ba452750d5b8e0de63c77c85a7446dd5bb29df2620f98e07bdac4cb59af8b9dc64d427dab780257e252e8d6793fd1b99f7c3162b4233a418526ab227d40b9deadc943d0f108a38d7a6b075465a44c920c0146338504eadf826cae345e864452e94706c8dceac5cca87469f2183a9c918295c09eef65f747b7a3d7fb3d259c4f531ffe48a5c338717ceb45efb557a47a593fac85cfaa4c263c109d385c01a3193a6ad1be5923d6a53e6c369a2b5e704cd6ac3ac255954c3fca1079895c596596873c1309fdcc5f36fe039d06806dd16cb00bd028edd6b44fb1e9ce4a020362824bef6a59502cce4e1d3d3884d7d865823deb092916a215ee73bdae1d4baf7f294caac7f63a36c2a15a40517438b8cecb93a5da3e11c25ff228ef3e7281360a2ce3ac66e69a2f136a83fbdbc44dd1737e9a31bdce16df9e118345ec1d9a9c47bfa60502722c9813d31c9f1b0364e2e25d77a0816db52f7e86bade9aff644df50b1cea9a339e76e869fa1fa50101f8182a2a6aee2e90d242d4b4d6b999d1c358a8d9297b6e0e2ec141c2b3692d70b141a213d477e87c52a2c87a5c6d1f0fb091f35f50915878ac70000000000000000000000000000000009111b212a32363b" ++} +diff --git a/ethereum/core/src/test/resources/aere/pqtx/short-signature-ml-dsa-1.json b/ethereum/core/src/test/resources/aere/pqtx/short-signature-ml-dsa-1.json +new file mode 100755 +index 000000000..414ca8087 +--- /dev/null ++++ b/ethereum/core/src/test/resources/aere/pqtx/short-signature-ml-dsa-1.json +@@ -0,0 +1,6 @@ ++{ ++ "id": "short-signature-ml-dsa-1", ++ "expect": "INVALID (length)", ++ "alg": 1, ++ "raw": "0x50f90ec4826d6180843b9aca0084773594008302d5709400000000000000000000000000000000000000010180c001b90520372c0f4874450fb92ace74d41bc0326c7f739589f9b86a97afa87ef65d5939e608c81a44909e41f41931349f744b7aedff8d5270b91c4a2f61192e2bb6e3e6d12231deab235359e78ccada386becb6a6a1105afc0999243ab8b363f7cd2c19b2b741c9969dcf20252f20d130dc05d9244f58d6de9b4fb4b6f0125c43b60ebe3c7272b6ae7578f1abb4bb5e2dcb31eebf4aa7089aa2bf40a0ba5eb73aaa90e45472168738cb0e866547e079075d0341c4728ed794cd449d0cdeceaf64d09173a2677e3f2621b56014df58ee8de2bbcf8cb2bf369e57a28e40b24df6d0b15d3417b7b7fce3c49230b3bb7c7080c4f1344380db37299d8c0c29a7c9e95bfc4f3ec70b767895be6ad798449f94c92f320696f8f9ec844fc9451f5ab30874e6c78673b932e24a918d5bc03f4fa647c8679631ad8b30ba4f9a1a790a535104f5079f781fa4c4aad807777327e08bd6e14b957e527650d39e2de7d30da7b2b27457d3539c2f99903054bd1a6d6301b2f83249db80dedc911059b047b5552407ed32b79b57ea4968198decf978fed532277fb189841d0c17f261f1a7c4618c33b258a71cb6b2b921c48735c1fbc375a80fe2d702e9f70db34547147f6d65153f1c508ff90aaf57dfde59ce6e56c5c52049b10fbc2640cd937bf5027d755444cd5e84aad66be4ccf04f710f0b970cbe7fc5e4e7a44f5d8fc2af94ec6df87862dabe0d85dd17e43728800d5dc8ff9e9e1fd349b9d6431f264836a447646a1ee2d73e6a4d1803d71b98510d94d53b5d07764400f413b7837f463e273980296ad5e78a62c83fb91354c25c2e28bfa19480ca41b0ca2d8beed456029eabc0ba69cf3504cf1207ab21bed0e3a60795f5b066019132212a9bd20e98b43bb32484454b7693bc63764d0ba8768da4336ad16a979b1d3e8b06e89afc4caafaddead83ad861a4596814062468d8cb2e131e71cf38c4e335bb3d1d9629251fd50a4273999a20e11d9a881fa2465c2ef32301fa8805ccb38b4656e3c815b54ef42f292d05847c0e5149b0a2ffd1719fcad10730b763406e9e453d2e4a556c397a984a7d8622466522e79f715f980279cb2d1d74d5fd15f8beb8df9c0a1fb93f0d5494ab701cd60cf2b613b484a191d873449c4815f44eebb62658ef90f7033a5e48d40c8f4ec9a25d4f1b73a150e5b03a4feec9142362e77d74d364bb8a40f9873efa2d9f458d84277fdb0063642d9ebc794356368a0b4e53f6ab0d9acf4418fa42d05c11db12cb67d6c4cbbfde4d85358abaf8042510db53c1540fcb40045b334bc54a17608e82433d1e9e53c99769b86b69b8cf1914584cdef479061718a2cc5bec5c31272e0463139721c0c3523a6ea31832b5c15aa59819304c217fbc51a2636bfff331e6dc1037e89be15c17d6708762976329353612f312b828e525c09b81afafa5ae5c25d82b70b6e55f892429a93e5eb4048ade675583e828da988f9d253123a06c0ed047adf4d93d20cc6c865f5a761fec27811305165cd4bda05f563215d0612944b88ade9d64477ae8fb441978720e70b0d77e2edafd3a2c571cd28a42cca2f289d157cec813c24e7f56051b4f28c153adfbb2c1ff90ea1fbc03cb89603387169072442f5d3081140e2256ebadcd775d2f1da92c15a9ff1ce6e980b3b1e91a7e379081ed46a4259fada21536af318eb2043ea16cece60b73acd415d239389cad9212e15bad1b1ae38f93c756635a9b87417faa844e4f43bae1f1ae68641a04f0ff1d55f9045cf7e3ed59fde5c07219b9113329356d4300885ee19c9955c5757db33c6ae75b49d035003171d02ccbd7321d6a14db0bd011d4877f04070dbda3b67bf22d083eb90973664be581b59cd495740a8a9aaaf7c6801a8bcedb531ace47da1be56ab30f1c4ccdaad6e81aa97352baca12b6c69754123bdee38c0d57bf15f0850572dc9b456234eaf37eb478f9117f952ae95a0d165d12d948b51672a03c9610a98cf332d6ad7ab8a87defb3ef7249cf8c3a815129ecf5600500702b7916ffe570cef0714e0c083a581c2971c45c4aa6b1c5226400e5fd594ea3ae97fe94a0dc7f1e0979b11a2f3d15ad54fea957992ada816f97cd76168a53ebffae36e5787b0f69e94b3e08eadefe4937ce8b383b5022e57ba7135cccd5b48e38916f1f0bdcbc5858bf93cad75195d4329ab41a518d820262b1b610f7bdbe1d6749d006a0dfe18d0be1a19fb3d8e37795b5bc37ebcd884f91459164cd2536b3496c4b25a4c74282ece5d64ab861184e942cf97f0edbe6e1293c4504d861e60d98c4f6523df27a1e80843005ad21665b3adcf46f349fb0d905d70631d2ee51c0086b252778eac27346eee65c0fc88c6db5936337b0afa7d8cec656612c81e11cc55abf421df770e6c2588c743d5540e1280be76309defaffde2b82c808976c337ee13735a21900a4686423625a0ba4819581edbe574e6d7053c5fd98d288db4104ea64fbc0e7ad11d50eb1470a882aa00ebfd983cdda14db34f7f968ccc93097cf037dceeb1375f2df4de09c8785f5fef8fbbe284566e5c9a2d38f5d2f9145b7f5087f6a89a56679680ca4180ad7da3ee45da685f0255a45db806e1379c1ed59bfe63feb7d74a239334c80ab9c1859d205ba798810adf8afc569170f94167439d137215d2ce0b2fa983ac138a3ce201cec4927c62b740912e7287417f86beb07af9c83fff843df1abd6e6f81865b82b3168528d1c473b4999b0179f1de8ae22e8ed7f698ee8463eb93fcfd4e017f29ab3f15a2e061de6deb09459d4ecc62741ffd8514d5c1f10eb3b1525ad60512ffad00e360e0d6add8ed6d6df48d63cf1e36f306bd76b1a245cdd09951727367e45b3a02d748dbfcbeea342be4cb0e1e3108e57993581409f8c6b34e2d38becfcf15c653b92c88412c087eddb0bc21eeb620317f49a9bf5d6164418a26322e1964fd676a547862c2a7ce5f13898e3fadec79a638f257532cfe977fe8ba5e96782f9e1e3e8b5f896a80d816141f0c988ea59ea321cffd23f298b76e49e92189e7f65718d14091daeb82d432470a2e472a39a154226a43c543db2bb933adad08344259ba08a65c543d2ee07b67328ca2d4e60e550b85215ea5873fedb505cc1e4db49e129249fb632ba22f11b897b9423d08f96ceaa863cd29dc16af6ac76c4388c2795250c04377277626992b085ed7be2cf1d63a2eda4b65e287081fdc4e9a3224ff57d81b3ba8d04eb88430a01c5eba35fad028e8de1812920cee2576d0bd291407c5fb70c5ce07104b9af7b98c4c709c7f019d4a66b136120b92612ad5e73dfcfc1da0fec3a7928e57d7d4769eecbaa4c269f2859d31d47d1ae40e4da5bfa6592074c5bf54b04c707b8d97455a0a606686e06bf5ef7617da2532f305267f61c5f627216b06ecc3da6c6f7b420e2dfecaa41657eab20c89927ad6eb5c4fcd513da89a637755f4a98c45cfa108f37415bbf49ee6cacfda8f22f7101a9798d844709a38f9168685f105157a937bb10d4f5b342deab8b8be8231490da52cc0c407bf942270ad93d4a9bed5c06499584835dbba75577dbbc47fd3d397f0417ade99549de4412c8893cf3e1a2ed74c234a7b60461efad57d9c9fed9eec3ebd652282b045174e19355d8cfcc10add9192ffffd428150dc3f48e0417e47d6407008c62486cd4057535a5a9ffc45ef559a51fbbece64306f64da9740108d0b51d01423379bb0a72fe050d2abf5f5acd25cff57f8e3bcdf938749c7bbf77437424ada88c167bcb6addc309b949a8e200b9a5b6797522635731b250e2405f98d054aaff4179ccb4b24159e4c033329100cadbef3da262203622d27899166f6c3c79a35daf7840853b9a3ce494bf035235cada8c31e28be56f0976ef9b96f15de57e6896a5aed57a7b5ce77cacd1c806ecf83566bee5d577f7414a106ce16ca6e8cbc144f571b1a37647041694a6409f68d15604960417a60bc4f1a878eb01e89159e701bb8fd96be45395a8b8c4f0cc5b982582741c4ccaff8961ea892f084b10646c7a27687a2d3c92324cc3a19a5b38ca2d5c573167ad54486ab3adfe9219882f947cc6a93a3827f4b51d76e90ffd1587646d921482022db8b5a22519be3a1e62e9a13ff9926e75c42e07aa295c8486df69dc96168e920e13c85eeda86dc560b9f5d4c77cb8ff001d390dc6c5a911c573ab1711df534930ef28832936c80cafcde731e999ce90741e669d93f653efba12466726fa997e7ad24d34395a02b8a4286f3604f120bf47e8ec41954e6def2de896be477ef7916f18214d28f58ae91ed8d7eae9e2df600d313b6cdb952195e9d030e0b8fc98a68cced3070f4abd2d252727320eb54b602ec861ba630ed3c5d6fc8c8b39f4f1e9c198a6dc8d5f825159ce15fcd7bea5701a225ac41bb152d1a077adc03ecf06870375f2d2618094a648b40244e4a981e7efeadc608c082dc1824c4ffdbc39afcc8c464ed5ca8dfc1540ae3a6c034364c356bf13f880abe8633fa041db78f4be0d17a4bb00959a697935c844c6ddc02222f7aebb1af2c96a757536159dc1dbc31131aefe4c198ccd2123eb5d0ca6147bbc9ab43e993508ef4ca383e2847d9fcb4c975dcb240bcfd164daa835311cf7dbde0f256deb5f3748b55c312a29a042725ecf07caa904513e71b4c0ebd3b31c2938e710e7c6be819bb3125e638ee4b54b4c8fe16df1cee7587e6bdfdcccf2e3b2aa4a54c86b0ae711f056c229c644f519f60570aed60fd3d0638dc341625fb4949d355e7d84cb478133d25730162623acbf5d36b9ed98dbd88b6bf7bf1f55006f5ba2d16297557f966cc01baca56e3f6435b724c0b6fa4cc0074efb0bb2cf0cba9a1c73d3337db7dc0c78b87746db7bbe5fc003edf608ce5d91dbd8b3cec0f738de14c8e2b6df47df91805eab5635402ff26245430641abd9511cffa6bf8d352a1f0d97ce2b08561c97a5f07a07e0ac42a2a73366442e0bf125a718553794b021640ef9f9f07040068cce2337606af46f897b7ad8ced05e14f7ab3e2bd847f810058d665a0c3b6903efd8f398c31e356b48216111d30348acf93d28dbec8e603cd16ea5d3b24f38367c52f5629fd51bec34e86ff21d4f5466a53aea7048babf3afaecec2c9de099bcb2845f66689e02306760ef5274e65f36fc61edf8112b3b414a878b8dbdc2ced0d7dae0e95c5d6e7c80a3b4cbccfe13293e454b6c757a8d9496999fb0c4d4dfe5e7f3f40e11172324323556687e95a4a6bfc4dadbec000000000000000000000000000000101a2f" ++} +diff --git a/ethereum/core/src/test/resources/aere/pqtx/short-signature-ml-dsa-2.json b/ethereum/core/src/test/resources/aere/pqtx/short-signature-ml-dsa-2.json +new file mode 100755 +index 000000000..27b867d52 +--- /dev/null ++++ b/ethereum/core/src/test/resources/aere/pqtx/short-signature-ml-dsa-2.json +@@ -0,0 +1,6 @@ ++{ ++ "id": "short-signature-ml-dsa-2", ++ "expect": "INVALID (length)", ++ "alg": 2, ++ "raw": "0x50f914bd826d6180843b9aca008477359400830383209400000000000000000000000000000000000000010180c002b907a05424000ae9a496294dd6b99046cfdbb235ae71031b379ce274c13b0c96411740cb61021a5390fbe8fb56b642b4e5e5abb7aeab143e0037f8db283b9f2febd650265ed6b05f1b867c124aef245c1811422f92414447fd0b20844e0f1b1cb4eec052400aad52b9d773aae0015c0c390bfdde3d367bd8ef9c3c73bca3164b9d418ecbb2d25303bbd12739a858c5fac4cbfa0203b9f3c319e9bc89d17a7a244b57d6613b5caabdb528f077eff9727015b9c266d777c0ba11920118c7ec4dd59b380c40e361a5b23ce04be4c61e12a1c21e091fa0d890687cbf588944db3e34253c75cdf797378de79162e0b4ca309f550365a289f652dd6d559478912cff70a5417589f73f14640970694d7b5a6bd8f000a65c632f0326ed3d128fda96dbea68cb812634dd75e9f63813a5645336681378b6a63220c066550aba43a5b937131d41f08752d13949a115a74bf96fa1925ff4338e10df0301abe40aaa49e14a1a1d58eef005a0f1d0299267002cebeda20cadcea7419cd0c365c53aede1aa36efaf764d3d36169747cdc8ee9a09e0a0ac6bcaeaeded0cd303c965d705d1b91a8b35f408ad8a033b02ff57610bddbfa60e2114e53f20f8fcad15dffac6a494c3f4f631590f2be3422af14cba7c900c9c38796952e20c3069bea2ab54bf0edf9315903aa5cf68549f878eafcc77561b36f8b78d950871ed4914417316f868a2bc54c72d3c9929f0138a2f0cbccb7c4141e94bb8a7ae11f660861cbbe498697d7327709271460a75bd4e024de3d4d6fe5af066944c96ba87459fa948a95eeb61abbaf643190ccaa9cd18591eb6f249a6eb92db89fc95616d21b41d7d15424b80a750938a12cd57d50ef95aaf222025dc4d81df320d9bc32c608b1c5a1aac8bd14c0f7a4c4380cdd65c793a6287050ff4d609102fd91e47e7365fb55513aeca273618528c199f7bb151e9c11a0bf202147734d1be9d5d0e9318096fa567317ed9fa6a8efe201b915b9aa05ca0d19e15c9a80449cc130b398ebbac3d938f55eea8f74e024b0e33dfecdbea02b920ddc5b277146685916194bfa1f8ac7784c7ed76923d4ff9ed97d3ceac283f9b26977df0133c5fa4fb5fcec577bc152e926529f06ff50c204c083c0b4d6b8d474cf5cf7cfcf7acf76921225443eb0abf5e4c76d0a3f524243ee4e372e80ff0f76dd9d135f240114a2ebe6d69609b6c60d275f251d7dceadc600d18e0cf8bc8fa8392e61fa32b8f7481c4883e19d820b51af0f87d3f7faae44e3b908fc759c6de59361487d8e424ffbd662d57a68801110e9cb099214143253949cc479eaae0e321fecab90c63b06819ce3cf56fc2fc1419408075c658024a28950aa47d549bcdb8b5bc9aeab463466c7834ca7d73058c206a95a023f0c8ac95badd383e63041e1f0ef69f51634f5ce2f48d80f8f2e92f4a1fb5322930141fc50e1e20bd235a8b8d768d473b46711257d028e710f82f0b6bd923da05ca64ae805f708577e125b3fca2feb5656f61ded034eaae481a195628801c82db85345f9ffdc72c860cffe2264ba4be2a08fdb37a4f5f80276baba4ea637847335c89dbd35504389f68b2d61ea66d4eac5fd0057d9e757d929214a7d83daf2fc2b3f840e083792d20d772584b252ba955ae8ae8c36af648c59ae0b25d3bca19413fbd749077b6969db5847409d13e4a67387e59116fe3836a733f3253d47f9919f49618fac8edf5fd711a4239447655d6c40f479f86ef3c9d4dc1a6f9683a3f1bc5c9730025e76ea1588d43494f59823f42c207e34412f73c7e2a126501b8fb810f12995c213063d909a5a9c2653d3511ceb809efa69c638f4c977d46a2a305cc831ecc0feebf5d9356ede6c780da78fe2ad683b3312282d5142612c0d1f85108de04de5c3d5414b62826d5b75f8887dc2d932c40f306c08e5d4f69ef4d344fe4317552df9890d0961e3d857bd9b2c5b44536d8606f62f66d14ddd43b0e5d3379cf66d664616df12b1c62385a57f6a562c34e120b79d64ca39d8b23e1e07b2c03910d711f09df5b54cb0e133dd751bea7832fdb38de2f1dafae69fbdf82b6887ff0a92940299020a835b958bb01b25b8699a0846614cb256a6621ef92e4a0947df9141ed4f91eca527aef25862158a8fa712dd60dda947f6a16e077810e29542a56f9244c34a73454b9ad0d8e8c40bd503efe12c4e5bf7d0ff8e6ab642a039f4e8b575d33c0c3de77a1abfa60b0252d7a549dd791d9630c8b848988157608448d77938bfb27beaea8f9b4a259263797c5b2fbdc7f469b2769f7dcb7b9478cdf5f109322eb2f0a83163469b72d8121fe8e01df227563cbd941a1f63961e2a010c6322a812a014279583a0066492a17d1a15c07024f6b8b1561041b3db608ec1c7475ce4d086e60523e85d7a955841558a35535d6e52cb166669a69049646b94d2fdb901ffbb1446a2d7f58696bce000ccbbb6942b0ad6480d006602c228218d3a2f0bc6a5d5c73acda9fc4b0b496a7d527050f8d77b71641f030368e9494bfe8e9dbf6dd4040426da1b0423739b40450273f4a8d551637e5fe3ca8d54fc476e6278e75cebf4a4ac3ac8ef3807041c4b274568797c8caaffae7df08c6c158d92411db981ea02cc212064c3ad6a9cd1cc02087acce5ceb8d7cddaec429d199c3d90670288be6e88e6826d1f6c417df5f35139c71a62c0267adfce7cb3aa49e827165b28c8cc157a691cb52ef103d8d7ca0ebfc628904d0b2d365dd3401b620d6d93cef670e2fb90cec33fc1486a0e70aeb6fef62ce5a0dfb51fcec12164e4024b0c49ce8b65e01356d80a9571af8f69a9c7256ed549d699a31ceff21c13c482ee0c5759f96125b0099495feacf22a001b2c887b107a9ca9e84de461fbc6e143566de56c710156de5b5f4b7ffe29c0124c1fbb6a792110284c9ad713d89bbb5ed444d09408c65a9f74c5f6c4fd281d31738db82913dcd1ac1fccbba24dc933fb9a8e6c4e426fb98590d7da0252c34d866c114526ded1c9d96daea12ecbd8f2a44531dfb72f02c02b267edf0560b1fb06b087bb6ce02b2922d92f81bb7da99b90e4a2dd68aa858759584490be602369dd606a510b6228e3b941421a3006a697b79866cae011d8aa496de4f3bfffdc0a087d01323db849f6237d7ac476d18bb417f26a697388b4c0a78533f3dd763398719c9ee5bf0299786fb8c4143af8c1195103390d4494b735c06b5815c454dba2fcdd27d214c48712609ffe7ad7c082e835d7477de2358fb3c4b5162027f816ca85aa4a46c25105175e77c8e5394db6fb3999c25ce578f066329182b28fd5b68769cb07ebc67e47a537f6cb62bfd18af162113a4df7f8908e0ce953b6a9637c850f24de0fda239c25fe4352f3b381fc270384d51d1af1ef0dea8501f98ea71f26de97e98799c4b2a316a216bd4af4861ad83fa4570532029564581bb35ac543355ba3aa246b59c6a5d9a91b938f3c4f0ad6d6f567fffdb6fb78402ded5fdc544a0f5fd87e4677326ca003dffdba8fd390487a6469991506a18f376b12db5be3bdcf4a4bc2f5b34dd8f7066bec3b40a6502f414eac3cd1ed43431b033757e86cca57691b0e1f7eec68fad9423f8b0620aa94798546bc878cf29d74ae74af5244ed9b97107fb83abe25dc160ec05cda1c50b76db981aca781868a7913a3e7c98aa5a3ea8bc81270e6f565138dba33c9c7e887641fd3dafd458b9c64600151038db05725a976950307c662c2eb668e3067f943b9bfd805307729b39ed77309fd5e67ecfc56cc231057a6628fb6f528fb969378529dea2c58fa0b4d791ca591ced733e8cf93a8e19907538f45c2618612648d3e1a78492afc154b7bb0285aea97d618e74a74530b492c66c6bd31557cdc55476b3f163196ec8e87f110834ffb8c232dc77cbf2f7b490f520b3b328af44fecca860a19bee9af6a5f69f63487d7dc6b7bf1233c4735522fedd475f905f044a0a7f658a7a378958cca56a163a343a010f4b65d1bc38cd57d08e0f887293fbc106ad7b15aacca6d51558cb8478b119acde04a19b007802a8e9578121304622aee7858d1794ade88973d6fdf981ebabc98fc417d765c075bf8f76b9781dff7c60071878027c057aade8eaf820186035499e9e5fbf8791e6fc0a0e49047af312a8b1b1e80628dc4db4f45842e19096a54733d28028203aa668bd8655631a01c000c956c32ff10572fdbc95b2f26d09fca84c093ddaa122ad93598c03de9273873ece5096648a6251a27e993ec7b6ea4f2181c09bb06852799323f3e993a4b7461439f702cb9df183fcaab11b8bd2ad391924d5e6ef261fea01c91cdc10d9345c290dfc3c040318d11419563d407c8365fef3dd31d8f9d7d338f5f2cd00433d1d06547ae9c650d14d9a7e5b683f0458e7a909d90d4a57201ca5b8753c13bda3591f9cadffd0987d1c7fca1c5a0cec1e13c612d0390d7c0e2f422c97cda29959cd7b714fbeaa5da7b0bcf911c2744a9a25c7d49e93342f61e10a33fda80fadfed2668bfab8767ce80480093b4845cd5ec83082e8f9ad63236743e72842d1a3226640a549c2c1d002a176e2427b2ae612dc272e3d9fa80a904eb8f93f494a52706346c08b06c9d1d8a61a9c7112c0933910882f0da6eea3c389f4faeea51c03394eab4cc02056d704b8839afd76f0839be5ffdc873b88f9a755be6f0229b0843839a25d63c18da89be1c5da930a101913ffba01fd00f2a447d5f58ae8f21554feb47f42d40c32158219dbbb1e5e96ab3dc26cd6a435525c265b65b0bf48d68e9b3c9178a509833aa47c23e63aa9a6f74bef1e4ef863556b11c014b85ac7c157f1e64531b676eb28c68e82e0089d2bd798213c836cc354d4883dc0b607456ab7f894f0a1f73356fc05a5c97a9cf3b231890c39105b751f6fc54063a08f3d3a264ab90cb7de1d0e2f6acb92302cc9b4bb512e720e6ff8343004028ad223a70d395c5f974d3ddd380f798e80edb4b13555c3be0cb1a500e19a8b1b05f9cae85d7eed756ebcfa57828f1054296ffa48d426ceb83c69ebe1b983421f42316ca6f50bf8df0f4d4aeef85005638a304d88764ec26366a1437867c89800aee55ba428e0c07542e123d748c795560153007f768b20bb057fc2643b7db992c3b9cbfd88657b17fbe8a783da2050affb518cab4226cdf28fa97f48651bd142932835f061f8642e79d1f7ae8ff8665aa85aba24a850b2042dd37e61e665a451f17cbe9d57ed5ecb1dce9ec34c576762929dbf46964de7c44c9f6b10fcf52b48649ded2d491bbe4bb6ad578cd31cbdea6df8342dbb7f33857617250b014fc6d887a448f7a5cef25ff69dbc94733bfac1d5f8593af7fbe4759d23ba8da1f405ba179c96d8ae49eb760446d842f0d61ab08508a3e31e4953e13a1b197d00644303e8a9513199583709f41757740a0dff347c3859d3a4ec391481af349019f75ccfdba111a4c338fd8283733115ce7c114111abb157c1a5a84f7b39779a54f000827a250bff00dbe9cb7162d81f6c942b719f9eb973ac7d4652b911622396c517c6e9c7e5f839c049855567fb53ae84579388e3c87af420272558c686209fd92fc339a553f873995932d9dce52f5e60cc0889c3283f94a0d2d5d011154d9935e1dfbb7e747b393bdd8a84f601adca56d4ac5e8e6f28100a7d5522de50dbf1bd7f5604fdfa660a58dc681b4b230abbc4f594a972a73456de1b7f48975cd03313a80074f5d6701db61860f33dfcc9f06e313e8881c385a50de28ced014daf702f01c45368419b73383008b9191293f59fa87a1ceb0cee64d486842ed7c289dca3b3f2e261df644de0f739079ba5ea195468f0693a995dae8e0f7e4611196824f329fc803bc3588d7d941d1fe86a3ecdb3eb8f6d4066ecee371f9a14e5bd46a40b53e87325bab7255099b4fe7f7b09eddaefc6582feecb8dec02b5e58d735a927d052e263027fa879fceb852b9e08db4636c7152f272a43ae4037c50210e90caa3e008fbfa98ed42cd425259096fc06644edab9b768779dcd892747f18a9cecc6aa31dc27d3f12a540447653edd5bed68f50da71b7992434b74d0209746f93ea429691fcb99da0ca32df822ec4332f7eb4bbe6d2111ac84a55932b3e173e388ab96a5a8a17c7d8b8131cf566e0335b0c4a37c5ae24e73c81146031295b69ce3032fa8e4c7616558179cc561f14864190d458cb82370749d595966047de698256126a4ca23a6bbd39201b8649fbcd387474ecf5b9f732e65938fe393a2a1269b06b9b49c5d8882d70776fc823da5842d812be2a65d2f5ee0d64f8258e761e6f2d4c1a825963b367d70b9e81b46fc25e4804fe9decf3aa0e47f1eba573a24f0e4f7b8cf8a886c7ef4175bd317709feeabe46df9719e7d7a4bc2c1bbc15a58b5332ea558e160f5d3b7aa84fe54ea7a24bd8e394ab794e72db7946780272556be33b7f32a0ee651c013d8ed3b5d75bc1c4826dc94f9e3711c53be3bcdf361329544f2993a189b6431a6dda8b81cb1b7c3a69ba96c1ac84a55100c4985cc85ce272e8eb5a57c563a7ad782664d561c3400e779317122d425c9c4193414bebd4d6ad31ba916448f65e9a7ca1f856dacf9d88b695410c8491a759d2c0304e8368b39cf502d0abf7233b1ba9671bc422ce182833ead52597481ea3ea26a2cc35e12a8c83bbb263202d5e9e7a5bb8ab2a5a844d596c24fdac423981fc4dc4291944ae476d59c9a31a05f79f0fe4b39f145c4fba857fa553a17fb2d5f941d5dac8dd3dd07e48eabfe5b98f3c41e7a84986dc3a7a3bb0ed2d4232cffbc50f167d58fb52fa3b357e76430d7f0f1aff6e2d51ea8f5b98bb66c5b5687821f33b9b6c2e407ad0e8140c4d5d4f1739167f8d63284caf4a43688473cd97bab401218fbb2ea13be81bbc05a81042a02eb1843bd46ca535e778e207c79ce3fe511a55d21bf20b24bcdfb43fa62b3469fae2ebd61e362d5d663fb2c08cbde5df9d8cc1faaf6370b5328ffccde873849299d61477a9fcf7c5c96fbc51b24c0f2cc6e39a46f4a9c1f5214f2cd06db62a37e2c6f477ceac0b92eb882d7bcd1fc3721a89745473eb45eee9a4fe6ab323d28f4db957d0c5dc1ed25ebc194f744a2cfe98474868e6acf5641ad454b3626ac2a27a9db416eb1763935eaa5612e43c49e8959ebcf5133863e98c19618e936f33952699fc224f41a4e259b40228047ed6a17cf4f3a42dc76df1346acb0986984f1692b5ed28f0825a23094ddbb8c1750e9035b1df2166f142e7f179473098ed73acb9ffb8be530542f31de88d6677f805cc30ccc0b60787e61f9d195ceacf07a35d979f9c3521f23671f378a6cd58ed2b0620ff7e2def5025f878d70008744c6a53caf325293353747c94c5ddf12e3a42686f97adc4db62b5e2e91132345963b7292fc9d13b484f75d8da000000000000000000000000000000000a13171d21" ++} +diff --git a/ethereum/core/src/test/resources/aere/pqtx/short-signature-ml-dsa-3.json b/ethereum/core/src/test/resources/aere/pqtx/short-signature-ml-dsa-3.json +new file mode 100755 +index 000000000..25f29d47e +--- /dev/null ++++ b/ethereum/core/src/test/resources/aere/pqtx/short-signature-ml-dsa-3.json +@@ -0,0 +1,6 @@ ++{ ++ "id": "short-signature-ml-dsa-3", ++ "expect": "INVALID (length)", ++ "alg": 3, ++ "raw": "0x50f91c63826d6180843b9aca00847735940083045f289400000000000000000000000000000000000000010180c003b90a20fd3c43f5fc7807747a73e1d8e78cd9e611ad6bbba9b264534594174b49082aeab827927b335526cc50c2f0368fc666efb79c34af8ee90427d1718736acaad47f5959b9b26457619c07c2dda553d55a7498e30a37c92909f6b37c246dca4e8fd24d4cccd811396d276e5d11f1e8d14bb9eb4d5ec49798202422edd57ea4d8c0e8c6388e9c93a0ff2e52f7ca2303e40300c50c926a7da22919be462e2dcbdb3ec235b14290a282ee76a04f4bf7c5e2ec5c8fc0aaac72ce4f8fcf0d42b9fc3c4049ab6dfdfa12623d89c0d96434a8e13cb6f79ad6f4a8102f9a0bfe582d967372d8928ff9d7d7393b22ec1386ee91dd196eb9c349a6a12f7504e66a096eec0a8ae98e62275f6b84fb40a81a390c6ef0102c8fd2c11d303745b8fc49e638e6f3004961724a977a18d81efc67e37d0d44558e28cf9015dfa08eff82004c4a53dfa40c2451fa03a987f70c855dbee37e36480863044165e6d46d4c43278091317beab6f301d61c8f54f94ee88fa358c9aed6e55b80b4196e79a3d7251ede25c4baef8129882a69789df3a8262941199ded7bd381de00c257199c18e9086d9ce3f72c6d95c0a4d109a3a15474805f0dc06649a16a7805e8d74bd1af7658f5f9ef726d560595ff9f072d4cfaaf61283087843b904b0f5670ca551079c759d4a2839a71505e007cb5d49b0437b87a080fab5e2efdedd1d01f0f1ab72f0e6129a2525fc6e9829b56ce9d9f287a5bd80be8b1556e4ca82eafc60ec22315448768a72e45d4d956c16ee48a0658e307f81ee0f695f1b1b747d1d533d8da720f088b64608b1edb2d4d27247b2cd35e3d94e1256a3c5c944bbafe07b55ca74df4ec0b869733ddb466717a16e98af4ec310d046c90046c024fb33fbb2ffbb105d41bdd0e0af27b1239aa4d92b6ef8d727e51372fefc3b5c8bc6eb585a0ae2dd9970009555c2c2ced5e5a1e97c659565b6139dc4f3bae788e21f9658affa8fdb6f4733f7fa83fefab91a030f8ec976b8263c17abcef46dc22456d5347eea7c30dd25bdc3caad7cb2010f06573d56b6e5d28e6e847d8df8f18d6a2655ea66fe3c8e7e4ef192b5a8aeb35f8469277657b77155142c7b1653d5eb9cf9c4d69bbc5746e0c1876e330f9016a2f5343ec60b2f35ea380169a2bdf102888e6aae7e797d0bce73834f418018e8e55492da8236a542a999d9eea55bd52ee2e59feb0329d716c78c0bfcd79dc27cd9639399d2fd99d2fc9c4900ad60bdb8221501038ec4baadd2ac4830b2bfe1e20804c818c8e089661e7d842bc4e566fa0bc2d7968a44f8df0d4e88e494fc7bebdb695d3a8edf6a2703817b0f8c1a55569ac4a8ff0931137f075797e9744135a3fec6c767af26d4592a9022271a4052b1f1f24f072bd5ceb1d19e02b0bbf31251a7bb8b723bd3da02f59ca1c0c91bc10896970a1e0f8e0fd5b755da58d70be8935f65661629a9f833b35810113ce80125f8ed7a031cfd5eb1475b597856b4ce7629a9429d840a3457bbd70c05c73c6ce84dd59f917a24d06ca8743225ab8c266e209fbadf57cdb587df96eedf54c04fb3f83ec0dddd1d8bf9051c6de4db3aa099f1779f3f30f61f432e5c8f6b7ac4ee6c6dd6fce667556df155ddb2002735fb7addd8acf74863ccfe04a3088317634eb47e56b18c79f31318ac5e448a9094b5c2c70b44dbbbff7c235cef4701d504723b032726d009c6d7f527e645a23fc1ab478e6e2699211694d575bd436d778e7ae53868c63dfb78f591d82b979585cec8bbcde61f375e783e97e8f787fb392fa4f0bc3e9010aa9ec3cf6471bb16355c3ed3fdf2ee5b95ade86371d60fb0699a851770a212d0e0eeb3f20df78ab180343fd79a9d04d8dc56d9fe3d820b27d98d12275a40d270f4181aa1a18305c8d3469a954b2e85405a394684cc234702a6cb01c04ccc941a4392cf64ba31bea700d339152fce6abb0dbe84823b8d3b18258a29414fd52c91ccbc198e101287b79d8bd0e5f4b05a14a07055e66ea37d781934840a204638bffda400113bb1344126fe1c2ac1e57bb861c3d35349b1fba0c11e12921262ae52fb2e0bee444f8197e2817b9356e10af67e794a7967055936bfcb51f4be6fc5602a8df97ddcd73afe83c02f00d4f1d8302bd2e826d4c50fd3ded9a74debd0e807b694e912a8a3a800f5b983e171898874b77ea305ede4abdf0db5ca84a8a60b184374c08066537b08d42a609b97039ff8905bccc4454188b051c5d1e574e1e8b74cc5cbded064a5374c55f17002ee9b17447fdc324a12e3f2f414b491cccf20c9f98820461f4c7d61355a30f1a6cbb2fe7403b90587298fcc9500519451e44225c750b8b32a649ea288eda389e596390a5386652dea5d0e096ee270bcab9da0c3c4ba464f44172732a748e339cbfc8f0c1ee3187225e2fb29e67ffbd84df8acf759c817d628e03a245e9ae0cf00939fa12964e8dc124659eea8798b097ba52480532fa11194e4623ff63f6cc1d17c7080bb2c1459fe8cc9b197b89ba91986b7bfa61e514bf5c60f981e63baffd9fce44af87ad73fba197e46790fc03f8a46aa6d7590f47e08daedd7f564685208ee87da84b04b11cdc0c020bb26b0abdad3c1340649130e26af9c7651533313301e8b517a84c807d86a4c0cff68566d34da4111d67bdc77ef5f49ec7d83af24a143bceaee020acce02499c725ebc341a86d91c7f63dd962ffcc09fc0b5673873ee55cea4bba69b5058e4b364fb3802832d5118774790dd12eed78330412dc877cc034309a7e292ebee955a431fd46faf7884cf3bd5ab03d02b8784af2a84bb907ba5f021fc332f638a2d4b6451ea0a2cdee6ff1621d698eadfc99d6f405003f300e321a9e80ed3b577a899fa7dd891b53cab8acaa9fd3ce02640eaa75646f134c190817675b9e4a688d0668528fa3b378749bb6b1a0bd16873e89dfc927dcf568fc3cf94ac49d289ba1fb9c58a74574c8fbac321db256bf919e9432d6a8ae9a0dae900901f436b13d0c2b753a5eb6fcef03301d7faa15ff32ec0730335b13394bff66ea83a7043f9e1df5962671ff2d2759db234a4a4fa47fd3a9359bb14ef5b5fc398836d42afcc5d5f252b1b061afc6a0815b3a7586d25eed5ee52c56d2389b8f2438649ee652b431d3cc4262c6e5ea1c0ccec21aa6123e88b4bc933098576bd78cf5bcbbc278ee682029e05e91aac2043ad3ec1d3d97f6ebc6bd5faffa5bc71328cd4351bcb9a0d8d1ea9253b97b08c8c53b20e29703568d5d7dfa2bfa6aeb3a93043e4123116a125311695a11c8538658210971d7763ed7870c1309893909e306df22fe968612531e58a91d19dfe2c658ade19c8e0900c779af8ec333d19552e42cdedbe13ff85d99b10baf8daf70951290db56bc7ea049e6b468b6546a7f3ae77c2e14770eed1ea345f0d139336af4eee686ac2e73bd4262cb802e0173fb872a77cd2c0f3421c497b0260be79ae29d04d9a9da9d7bc101ae3ec9dace1133332f05b7b82aaee577c06c0d2a8eaa8a96db8b899cb3240cc4dca4f8eab513ad5b3529063b5df3664dde0db1e0ae5c29e69f011076fc7edc4261d043f38f1c68645466c9a5e685a07393b7e2f8f74db86742ba5ac089b45a42df3f943842747674c1653aa2a8ef282f64e7ec2ce1fada994288a5f9b95da4bc083ea008308b91212c74a08868ea0fdad9fbb1b485b793bce6e4fa945668b2351232a7c8a084fb2d87a5c83e7dced6bde3743617a7c4babcbf9381694b64d7e0feeb6615440e927a63293c6b695900b1e7afad1ae612c324a88e6761c3b6fdc41d18f514be1ba3a1ca53fdbcf8c19f95bcd60223abe52d07fd7c4114dad9f347cf122f6241429da7e598959c22692910323a0a699595398f518c58deea7159920d89e3e65702bc2cabea7e9ca0bbb7002fb7be5243bba6a08a12238ead443ff4394510d25eb279885d9112c44529f294c5a2bf493a74ba8f4e38744e632680b3ce055a897d2a5468ef52e6e70dc5af55a9fb83739f6cfc6994871662b8263f2e07e21a6514624909369e0138755165dd2af5a791da63c0604b68ff160fad82a578a06e76f62bb7e951b81d8fde2dade6c3a314eec5913151c16a93a5abd5ceb59ad96197a349721ab123b42f799dbde36d7965e0e5dfcf43d8d1ae13f4a00afd8dde36137d3de87f4e2c0366c18f4b3a449ffd9afc7c742c50456f6e642f7d09e86d5aef224b20ce5d8a2bf164484df8fc87daf69e459fc78c6b974cdb8fb1dccbd5290f560e3114302886e2828ad73ce71b06186254206743076c6af0c3d1facc36695dc28db1d026686b141247e1c7a9323d544b0079023cd2adedc0479ceefc8e0c34782603d0c27d18768eb5485a85bc385ffdfd25f735688a3b9a0c6f532b2a7989c751804de0163582293a17f635fdde14e9067f12929b079f89a53dfc87b986f89f718320ede83cf4086d0d095b24b66d5e3f1bfdb08b79cd5b25657cf2b20faff1d199c16bb7f631b33970b34c12997637447e4127db1bf580b339807637a86e0248cbcf8e1d30ae6fe053e2f9b7b3d0f9e3f4813af908b633560779012f9e06af00e252bb0df9f0caf7d1e82f8deb2ede21d13f406dd9ea69445d50844c95c921d8d1cbc8afbc5ddcd4157e65cbfbbafe2670e9146a5643db049a8d696f0eae07c3d4099447ef78bdb90fc05d7cc97bf9b9dee8719464ec6b9e6de866f73914a80396dea3900a9f7b43a47e648e20a9504da92c47ac65368813e5453e82020f8861aba2534346e6137e71adb28a5ff080365d6ea26dfc30ef5500d0760749d6e4e0f93aaebed544832308b19f72e92ebb8eaec2492c6ea7972fd359a8b67a1f4053bfb1a3c6fb720b5925a7a827e59b3bd0651b8014c77b25d61db17eb169cc2fa2442c82f68fb42200c59baa468a9fc92f13273ff071124f8b31e2b4b2517ee622d29489997ecb5676fb52e3002371c01004c214757b1f510091c2377a08d058209d18feb9b524a41712b6df4b92f2b14c583d3196749f367b3bbc8ab5e9e2615c511f23ba5fb1bac2ee9436dd8d162bd96f1fb663d2b520ba7e8ddfc2a4d549b63714654cfbb37ee1dca5f723139187ada8f8d5b42ad09ff196b70720d2527b4466d44f33cce486ef2e86311c75ead31a17b16989dc63bacf0d465379d9138df7ea2ebc71f4cc3b6662e24895050e2553dbc160a567d6722964a904d43bf2684206cee3562256e98addf3038e968cf6c09c921e2c89c9c249f6ba7afd20dd27587b4143f704f2738315a91c2d40c467111b4f3bae4f77bae402145e742ba83a527aeffca13ee2290988df882feaf5a3c24b416d59e06cf4f8832a016ce55419b7bd2028545557528a0af23f17942433e13fad089fdb79449bd9d99b0324b1828791249b6789a09cdfb6ec40c0fbaf8b28ea4f72d3df5159bea7a7965b94beb1bb01d87d76ae7ce47fc333fc152038ab4e7ca64a802f96f2e92bcc53ad9cae0242c622c99b2daf005e554cd6ba127991d84841ffe3ba0674eb7930509045dd00ab91a7c6acd2f4a40e7ffc9959929851e40d417f488a29f7bb5af3847a8caa5daa0682aea870ac8b866ccfdc435971e3d862c2f651cc6eb8a33fca313a56ab8228ee9ac7dce14ea3c3fba4f58c70888ae558d8cd632a2723ba54a3d538f4e20936a2ee543e9b2141e06095c72d4ee19908febf7c248c03a78655420da644746e2d508c31ceb0b648800d95dceec72f7576502046e5dce54b5a097f625001bfc92d6e4633b293cdfa63b1fca5de9e26d03ba2f1cf3647dbdad5359ed76bf702f0621c057dea3617af678016aa13360ebb02c6cb2fa0d3f137a1a3c639c989e12383841ab7ca5a84f84c08af4b85416ce7ec3ed91636a79add207bedf9dfb2ff626387f20cb3315502b2b06f361cbf6a51fb507598af41c5a5b9cd95ba1894ca372c5e1851e62a0d3d78dab6f92bd43ba6fcd71230b6cfd843d89268872b75e3c5b471db530b7ab5451dbe63fa0bdaa7d74907cb376643e129febbe6ce10b5dffbf323dc973f097d2081e06aad65e674c5a41bb81b80556fd78691fe6b5bfad47fd1b64db577f27f22f17973cbff3a7c4aa43569bbe6843d773a2f933f472650e88e0ec6643f87dfd37e7fba915c7386a130b8d4f0ff4d0e99a633d1f863f3e8ce4e1ac5e31438d89c3b6d5ace3aa6e14e7073164ea01e853958bece389316023127f9916297a47c5d6ab5513bab874c1ae3bf2850cfab9018cbf50a7febe03ff3e8e4e55ae5632812b6a147edf051b461bfe56f01a6ab571e0553e37a57a3a9811f4fda2e189d79b74215d6fe199595d18c6543c747572f8cdc61a53ce09d9ca96b43a5af5b56b53148dd4fdfc1e22c9958caa783f2d112d8d5fd39e6955df75d35c59a1967fc0d8d3293e39a6cdf82d6846b39a74b1a538372c266b3f12d7ccf464ff7e32fe54342994f51d344302b3b2e11eed2187a741eec99ef0a79a19a854c03d8bd3fcd557b9d8bfb35f0976da327fc7e8d95fb43db8e66be5b373385fc1b68099566c7e88aefe92c7c521d084c15c8d403d87bbc975e8d687e2a51e5ce53191e3db5bdc50f475853cda7cc5a7365c41035aecb61444c2a48492a600b58300b2805be97b4e9e30526e9e62bb3248a17c3809219307ca85351495c943a865359c783cf615a809c739cce0786c6045d9c8071797001434cbae571d42cef415e28ed6adab09ed10e0713dc1412ed80b8d26d549964321559137483c81b19203080b36abcd68e841f4c78b5302da79fd6e555f390322ae67e54c55749491889d79eb2982fc4428086bd1060c4113137ddb4dd7c9b690b802e805999d4f5127f66f6b1d397dc3509504e2196513b5787407b45b928cfc3c437523daa4f876dfe061e351cb3bad99ea205ab276c4f2a5ccee1027104f09adaf55cd282d4e9e44ac2b65f7e423da1cebb0ac70691347e7a7bd0de8bc9fb2e7a08afc0075a0e85902316cd6ec9a5802c51a6d65bf3251ae712ae0f66de6cba1e51efbad53ad7e769f851799caf5fb81ef35b4da814915dcfa67553f1c67158154707d16804be27af2115559e4758b313b9da4a1131718b29a75970b3f0a5e2f2c11a0a1ed47f238c145dc4fe9e8e963d8395c67dfa94a232b8133a0d953b05455f79e0bdff0bc3de9c04d93d97084ab99229622cfe3958ac5fda6757de1696b575f6250b441e6368386751b1d2f6007d254d152fe76578fbbe164af49cda805b9325f7ea47f5e18f3de1ce38006ae1bf027d3e354d6f923d8bc6286aaf9b9a7b9e7a7fbd36a31db0d88166dc68e7eb567d8164bed1e279a8810fc21656621ff7cb42a21ee04e6086924bb62dddd16c3343db69250f7ed95aba953202640bef18dfb6c00f1ee03c670a5cc6c0f3cd7cb006590dddffa6f19d450860e289f85edb4fb0dbbd2f992eebba09cddc1b20a3c98d8739ad1875a514b09b1f23cf406c4c55cc4266e0b8b283fa445849316c30bd19254e9d035756d2773cab7f5fd56d6d3a094a717ccca43251201cfddf80ba1aa0dad34f5a3acf8476b784fd53a1aff1307a7838705112650bd25dfea21b991d801f018a5dae26edb9dd5a0ab940a94b7bbb90fd1f30f9d1dd2ea3aad6d0a2a3c42fd2641233c7d624314c3a54ce616d2ca5683d04f6fc0924f0b667be32f4de8820f9cfab963a287a1c708857ebabed9b5bcc35d539ef9bc170636cd528ba86a620195134cacbc2b8e742376124b67ed8155ab53979e281f97b184c60355b008f15f45c8b7a506b1c1f511e4aaf0bc32fee27b656b01ed6afaa4053ed674a60175b6c89a77d42963111d0940730db02ff67534fe6a296e0e01e2db6b3f0fe9648286a0f3420cd4393163662751deed6457abbb370f83ed92e9da86a0181df13b8ef33e5718ce524a700ba5f2b40f3f973a67c1c412dc251c19e6b7ceb279dc1dfc36a3567e3a98a9d96ef52b0d0734f57bd9edebedd925167f230b1c81ca938e94ceebe43d2573735873a436331a8b76c1169c6a869889cb74e5d33721bbb99ec87f160c025f570f1d11c2e3ae6a52580785d854790139510179b003418dbcee1f4fd4fa39fc134521ffd7f29cee650472638dd04d8331ee439eaa32c57b56c4fd9c9ba87516a8236dc99b8d6e4c2a48e53a6779d5e13bbd73cc7917f0cfb8e231218bf33bc3c213599eb76476558f0e38a3979cdaad6ac6b03bd0ef0aefec634fbe0e1e87bd78123a19b7352326b9def856ecfc02b06622f94e8cb2a1cb6cf10d020f789a7db775063030f15b463f08878219636f2bb94ed11a3ed9689d797a3253809c7f2e0c60672e8476a9e272075c7f808258901e611c4d535a11bfe01daccaec3a26a187ab45a9fd0bb9a7ecf2e9c4aa3177ddaca1449f19f78ebe26be2e25bca012a10649b56899339f5418925648f3253bd716336dcf65e6b4f16d2ae51fed1acbc8335c103f721f46023cc2043f568ddb033aa5f2157e52b89eed4571cb523fcc66ef5cddc90679633c2a46f45f1a86c975db4f6f37f9fcfe54809ccf75305bc9f242f26f30a0ad5f3cd71cbd31ac45ceed2dc8571142a09b7d3091a4838727b9a067927e17b7559076649b405dfd480627881489d52339d4a8943ca396e92b92066e45d525acf7785a944345d25d721d6a354b1a9f3e2afd379d1c341badd0dd913a3471a5b78a81c0b338a2984e115d003b1deec1b44fd19824279d742017248fb4434cf47ad07fc87df86e634b2d8f6e0aa572088678b9a9f767404f06c84ea8cbf65d46effbde7de1f090d3d4b9e7c7c3b2b1dfa9e591efe12ffe0fc82c9eeb7fcd9f25fe438b39c46614438d0be7e1dad11bce8b91396f867531e8c45d8c01119abc2b20be253779b5e651bd80536854e2aabccddc5f4fbfb3497111fe9a8f8a417ab4dd806745c82126524ec325c931ac1f31a3cea19e159e1dbfbfcff761cccabefad38062872fe8a45a14781ba12551befb31c21150a56ccacf9815d8f458b1f591c71ba7b0459e7c09a4eac5c9e5b2984314ecd2f7054ed055ba082204f7ed62b1d513cd0992bc10c28bc745e30b27f6ca28ac84dec6cf0ef73c971ce12b2ddcc0d6264d8f7dfbba0a20f8ff1d7387cd9d9872f4b345b409950d66ababa9852f7f5ee634dad6085666956b97e4c9142dbdce96bf42d1334c61d04ffed5ac8f12a5075ac87a7416fd52579a876c89e22008759a93ebc57279215e178662a064bca65670089ec688532468c07161f56bbafbfa5655768123e713c7aff52852978e9d6004ab3a434d9b5c73f0d90dd1fe742a169a84f0e96a189bfc5265d8c5e2d9e42f5503d04ce01f9a912bf8b9bd8c282a4d40bbeca55d14971d1d305a16eb85d02b4edd8d35baea56156ba12f4e344dc4cb34509fc780ee97d24173a75e49960ec32055710bf8119d005447c6c391365737a0cff68d9f44868b97b2357dc051ed7fcf590b32292624545935e87f25e56b3e4a2b9c39c4936b5be9030b4711f246623264e0370291b675e75e1ce9cb425e5fabcfb55e25f084ea8a1b84870135ad997f7b700876691939cf268204669790a10ddd3d38cd22252226a09fddd395482deb561ee834ba452750d5b8e0de63c77c85a7446dd5bb29df2620f98e07bdac4cb59af8b9dc64d427dab780257e252e8d6793fd1b99f7c3162b4233a418526ab227d40b9deadc943d0f108a38d7a6b075465a44c920c0146338504eadf826cae345e864452e94706c8dceac5cca87469f2183a9c918295c09eef65f747b7a3d7fb3d259c4f531ffe48a5c338717ceb45efb557a47a593fac85cfaa4c263c109d385c01a3193a6ad1be5923d6a53e6c369a2b5e704cd6ac3ac255954c3fca1079895c596596873c1309fdcc5f36fe039d06806dd16cb00bd028edd6b44fb1e9ce4a020362824bef6a59502cce4e1d3d3884d7d865823deb092916a215ee73bdae1d4baf7f294caac7f63a36c2a15a40517438b8cecb93a5da3e11c25ff228ef3e7281360a2ce3ac66e69a2f136a83fbdbc44dd1737e9a31bdce16df9e118345ec1d9a9c47bfa60502722c9813d31c9f1b0364e2e25d77a0816db52f7e86bade9aff644df50b1cea9a339e76e869fa1fa50101f8182a2a6aee2e90d242d4b4d6b999d1c358a8d9297b6e0e2ec141c2b3692d70b141a213d477e87c52a2c87a5c6d1f0fb091f35f50915878ac70000000000000000000000000000000009111b212a3236" ++} +diff --git a/ethereum/core/src/test/resources/aere/pqtx/unknown-alg-0x10.json b/ethereum/core/src/test/resources/aere/pqtx/unknown-alg-0x10.json +new file mode 100755 +index 000000000..397c978c2 +--- /dev/null ++++ b/ethereum/core/src/test/resources/aere/pqtx/unknown-alg-0x10.json +@@ -0,0 +1,6 @@ ++{ ++ "id": "unknown-alg-0x10", ++ "expect": "INVALID (unknown alg_id)", ++ "alg": 16, ++ "raw": "0x50f90ec5826d6180843b9aca0084773594008302d5709400000000000000000000000000000000000000010180c010b90520caf597b4848d085dd620001a7707a9e0c120226d4551a49d4b6e0cbf82ad3dd96f26ac5e395a26aa1f3b569e89fc4914cec8fb3acd07350d1b8e0f6b66f7ac3e4f07395d148f5ba65789908df3cced072e5a56934264354556f4655afb86d9e892fa9926e44f74a340ec5a92bff6211740ecb879366826987e5556a513d8c2f585eb5ad5baffe6cf50aed2b96bb1f963bb124f2cc3b95ba6ec5c4f272e7e457bb1c6547e284ffd1c7b149b877c1a9856a3303f9aa5c4005c376767af6d7f00097e97d7a5bee0c196a12293ff2cef34a09f0f594fc12a2e40a4d7652fa8db49c02d76fb76320e29a0d6b15e97a65147429fdc70e4838ba9f60e0318132aebca93b564960caeef51770e45d8a6ea2171f9e9482441610de9fc8c56d80e952fe7c2ef0dfccdc0a337bcceebc20a8a2df53b4f61f289ebdfa2f134437e21fddcfa394bcd16562e41d1715724afb398f8312eacb3ad606fbafc00954407f83cdeee3bb5a3c565f50f9d54ebc8215817deae80e42c66dd0960ba23c3b1e99d710da8e1efecc89c76f769bb5e40b9ac5ae998216c3eef8f039fee88e43dbfd056be1c43145690c1c9d3775446c80d93214a89ff60892b4ad7c55bf56926b834cd21a3e09de040e2ad9b6499c0840957b6661ab1f149cf79ebee095b8c481e0feb761164b4fa5360e79987a0dd601a60de09ace752b5ab69fd73ff111e1410957ec95bedb2a9dcf307037463248e4b4d8c0fd1b455969145d2a77ccec9e101f5e9142e4f1629e19662269a9f422cd64f1ddc6d7bcaa30c949a7c67bd6e14fbf8dd7244ba465875df788b1d932631d6b749c37bccb00b04efb9a5c8d05dc73564bbeafe507c7e36eecd957c2054696602ec27892c88953fb9a5019899bc3ce475c4a2931ac49a4bc206b27b40ce3021096729ee2482bea5001985bb22a0f62dc36cbb38b8fad11d6a7c055312e69f8966a59b5c9b6237d7a8e59f43d94bfd5dd8e1589f3143ed4252f6fecf4bd40ee30b1382256fcd3218d4864dd281cf99d83c1ed4a766b26698a914f044d45405db8d7745b3d7aebac37e668c6e8c5ca09a0e4ec2190300c19990d96742ce7f539e05b8bfaacd2d3bbfc07faefe22d5e8795fc8a6e8cbaec0ed1489ce625ce9727ff52660025002ea692dfb39935ed30280363cd818a324d9bc782b46ff730e2e5955640bcbcbd285d2f2fa773154d3fef5bf13cfe81b857bbb576506ed5dafe4477e487171ee64c9e37d49df5ca7529dae30916b72965a391f4d3437303f7cce8c45dc187febd2a949fa7278f7f8775dd2bdd66b794945b0e96a0b58b6cd934deff1f4ccf156e8942c7533c8a45314ef5e3210fd5a3f063982211a122f9a3f1f99c253c37d981f223dccdda18a34bf3bfdf9e02ff607ab7c92ec4388dbb7fa210c7106bf570d60e5321ac1e1ed5fe32b4053759bdc3b87b1fb7920371b6c206d6f4f871d1667b41bb8e661f48144a312e3d7e0d372322d0a3dd8641574419c49f2ca4228328a089e97e86c4294b9b60af0998495379248325299e3846741ebaaa69994ce536201665cf6cd23de4958fff41f0601722df54d2111d845806c013b336d73c8461f9bf5a16eb05b08140c1a2291f78747c74cc2568e956f307e97b01bb3a67eb9cd51be8fd7ee8cbcdcc5b2b2c27950f5ff0f7e8330fc2f95e3a4ae49fb7512a6106c5cbbe4a5fda693dea445fc7e792676b5fded44d7da271c63e76e310ae988ff6885685868fa72e57e38d1b3c80318dbd5e075b29db1e038c56dabf7ba39fdf525f08aa69baba0817a0e289d5c12448eb4d07cd397fa39c312ecafeb9a846fd0b20667933c4c34bc5adcca7df159bfd5b909740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" ++} +diff --git a/ethereum/core/src/test/resources/aere/pqtx/valid-ml-dsa-1.json b/ethereum/core/src/test/resources/aere/pqtx/valid-ml-dsa-1.json +new file mode 100755 +index 000000000..d38c774bb +--- /dev/null ++++ b/ethereum/core/src/test/resources/aere/pqtx/valid-ml-dsa-1.json +@@ -0,0 +1,28 @@ ++{ ++ "id": "valid-ml-dsa-1", ++ "expect": "valid", ++ "alg": 1, ++ "chainId": 28001, ++ "publicKey": "0x372c0f4874450fb92ace74d41bc0326c7f739589f9b86a97afa87ef65d5939e608c81a44909e41f41931349f744b7aedff8d5270b91c4a2f61192e2bb6e3e6d12231deab235359e78ccada386becb6a6a1105afc0999243ab8b363f7cd2c19b2b741c9969dcf20252f20d130dc05d9244f58d6de9b4fb4b6f0125c43b60ebe3c7272b6ae7578f1abb4bb5e2dcb31eebf4aa7089aa2bf40a0ba5eb73aaa90e45472168738cb0e866547e079075d0341c4728ed794cd449d0cdeceaf64d09173a2677e3f2621b56014df58ee8de2bbcf8cb2bf369e57a28e40b24df6d0b15d3417b7b7fce3c49230b3bb7c7080c4f1344380db37299d8c0c29a7c9e95bfc4f3ec70b767895be6ad798449f94c92f320696f8f9ec844fc9451f5ab30874e6c78673b932e24a918d5bc03f4fa647c8679631ad8b30ba4f9a1a790a535104f5079f781fa4c4aad807777327e08bd6e14b957e527650d39e2de7d30da7b2b27457d3539c2f99903054bd1a6d6301b2f83249db80dedc911059b047b5552407ed32b79b57ea4968198decf978fed532277fb189841d0c17f261f1a7c4618c33b258a71cb6b2b921c48735c1fbc375a80fe2d702e9f70db34547147f6d65153f1c508ff90aaf57dfde59ce6e56c5c52049b10fbc2640cd937bf5027d755444cd5e84aad66be4ccf04f710f0b970cbe7fc5e4e7a44f5d8fc2af94ec6df87862dabe0d85dd17e43728800d5dc8ff9e9e1fd349b9d6431f264836a447646a1ee2d73e6a4d1803d71b98510d94d53b5d07764400f413b7837f463e273980296ad5e78a62c83fb91354c25c2e28bfa19480ca41b0ca2d8beed456029eabc0ba69cf3504cf1207ab21bed0e3a60795f5b066019132212a9bd20e98b43bb32484454b7693bc63764d0ba8768da4336ad16a979b1d3e8b06e89afc4caafaddead83ad861a4596814062468d8cb2e131e71cf38c4e335bb3d1d9629251fd50a4273999a20e11d9a881fa2465c2ef32301fa8805ccb38b4656e3c815b54ef42f292d05847c0e5149b0a2ffd1719fcad10730b763406e9e453d2e4a556c397a984a7d8622466522e79f715f980279cb2d1d74d5fd15f8beb8df9c0a1fb93f0d5494ab701cd60cf2b613b484a191d873449c4815f44eebb62658ef90f7033a5e48d40c8f4ec9a25d4f1b73a150e5b03a4feec9142362e77d74d364bb8a40f9873efa2d9f458d84277fdb0063642d9ebc794356368a0b4e53f6ab0d9acf4418fa42d05c11db12cb67d6c4cbbfde4d85358abaf8042510db53c1540fcb40045b334bc54a17608e82433d1e9e53c99769b86b69b8cf1914584cdef479061718a2cc5bec5c31272e0463139721c0c3523a6ea31832b5c15aa59819304c217fbc51a2636bfff331e6dc1037e89be15c17d6708762976329353612f312b828e525c09b81afafa5ae5c25d82b70b6e55f892429a93e5eb4048ade675583e828da988f9d253123a06c0ed047adf4d93d20cc6c865f5a761fec27811305165cd4bda05f563215d0612944b88ade9d64477ae8fb441978720e70b0d77e2edafd3a2c571cd28a42cca2f289d157cec813c24e7f56051b4f28c153adfbb2c1ff90ea1fbc03cb89603387169072442f5d3081140e2256ebadcd775d2f1da92c15a9ff1ce6e980b3b1e91a7e379081ed46a4259fada21536af318eb2043ea16cece60b73acd415d239389cad9212e15bad1b1ae38f93c756635a9b87417faa844e4f43bae1f1ae68641a04f0ff1d55f9045cf7e3ed59fde5c07219b9113329356d4300885ee19c9955c5757db33c6ae75b49d035003171d02ccbd7321d6a14db0bd011d4877f04070dbda3b67bf22d083e", ++ "seed": "0xfa8e8bc1e5b9aa3d7e7c7f7a5975ab8ee22b1f9bc118a3c20ca50f4f0e737aca", ++ "tx": { ++ "alg": 1, ++ "publicKey": "0x372c0f4874450fb92ace74d41bc0326c7f739589f9b86a97afa87ef65d5939e608c81a44909e41f41931349f744b7aedff8d5270b91c4a2f61192e2bb6e3e6d12231deab235359e78ccada386becb6a6a1105afc0999243ab8b363f7cd2c19b2b741c9969dcf20252f20d130dc05d9244f58d6de9b4fb4b6f0125c43b60ebe3c7272b6ae7578f1abb4bb5e2dcb31eebf4aa7089aa2bf40a0ba5eb73aaa90e45472168738cb0e866547e079075d0341c4728ed794cd449d0cdeceaf64d09173a2677e3f2621b56014df58ee8de2bbcf8cb2bf369e57a28e40b24df6d0b15d3417b7b7fce3c49230b3bb7c7080c4f1344380db37299d8c0c29a7c9e95bfc4f3ec70b767895be6ad798449f94c92f320696f8f9ec844fc9451f5ab30874e6c78673b932e24a918d5bc03f4fa647c8679631ad8b30ba4f9a1a790a535104f5079f781fa4c4aad807777327e08bd6e14b957e527650d39e2de7d30da7b2b27457d3539c2f99903054bd1a6d6301b2f83249db80dedc911059b047b5552407ed32b79b57ea4968198decf978fed532277fb189841d0c17f261f1a7c4618c33b258a71cb6b2b921c48735c1fbc375a80fe2d702e9f70db34547147f6d65153f1c508ff90aaf57dfde59ce6e56c5c52049b10fbc2640cd937bf5027d755444cd5e84aad66be4ccf04f710f0b970cbe7fc5e4e7a44f5d8fc2af94ec6df87862dabe0d85dd17e43728800d5dc8ff9e9e1fd349b9d6431f264836a447646a1ee2d73e6a4d1803d71b98510d94d53b5d07764400f413b7837f463e273980296ad5e78a62c83fb91354c25c2e28bfa19480ca41b0ca2d8beed456029eabc0ba69cf3504cf1207ab21bed0e3a60795f5b066019132212a9bd20e98b43bb32484454b7693bc63764d0ba8768da4336ad16a979b1d3e8b06e89afc4caafaddead83ad861a4596814062468d8cb2e131e71cf38c4e335bb3d1d9629251fd50a4273999a20e11d9a881fa2465c2ef32301fa8805ccb38b4656e3c815b54ef42f292d05847c0e5149b0a2ffd1719fcad10730b763406e9e453d2e4a556c397a984a7d8622466522e79f715f980279cb2d1d74d5fd15f8beb8df9c0a1fb93f0d5494ab701cd60cf2b613b484a191d873449c4815f44eebb62658ef90f7033a5e48d40c8f4ec9a25d4f1b73a150e5b03a4feec9142362e77d74d364bb8a40f9873efa2d9f458d84277fdb0063642d9ebc794356368a0b4e53f6ab0d9acf4418fa42d05c11db12cb67d6c4cbbfde4d85358abaf8042510db53c1540fcb40045b334bc54a17608e82433d1e9e53c99769b86b69b8cf1914584cdef479061718a2cc5bec5c31272e0463139721c0c3523a6ea31832b5c15aa59819304c217fbc51a2636bfff331e6dc1037e89be15c17d6708762976329353612f312b828e525c09b81afafa5ae5c25d82b70b6e55f892429a93e5eb4048ade675583e828da988f9d253123a06c0ed047adf4d93d20cc6c865f5a761fec27811305165cd4bda05f563215d0612944b88ade9d64477ae8fb441978720e70b0d77e2edafd3a2c571cd28a42cca2f289d157cec813c24e7f56051b4f28c153adfbb2c1ff90ea1fbc03cb89603387169072442f5d3081140e2256ebadcd775d2f1da92c15a9ff1ce6e980b3b1e91a7e379081ed46a4259fada21536af318eb2043ea16cece60b73acd415d239389cad9212e15bad1b1ae38f93c756635a9b87417faa844e4f43bae1f1ae68641a04f0ff1d55f9045cf7e3ed59fde5c07219b9113329356d4300885ee19c9955c5757db33c6ae75b49d035003171d02ccbd7321d6a14db0bd011d4877f04070dbda3b67bf22d083e", ++ "chainId": 28001, ++ "nonce": 0, ++ "maxPriorityFeePerGas": "1000000000", ++ "maxFeePerGas": "2000000000", ++ "to": "0x0000000000000000000000000000000000000001", ++ "value": "1", ++ "data": "0x", ++ "accessList": [] ++ }, ++ "algName": "ML-DSA-44", ++ "sender": "0xa4fe6cb101b79c0531f265f6b2584b37073087a5", ++ "signingPreimage": "0x50f9054e826d6180843b9aca0084773594008302d5709400000000000000000000000000000000000000010180c001b90520372c0f4874450fb92ace74d41bc0326c7f739589f9b86a97afa87ef65d5939e608c81a44909e41f41931349f744b7aedff8d5270b91c4a2f61192e2bb6e3e6d12231deab235359e78ccada386becb6a6a1105afc0999243ab8b363f7cd2c19b2b741c9969dcf20252f20d130dc05d9244f58d6de9b4fb4b6f0125c43b60ebe3c7272b6ae7578f1abb4bb5e2dcb31eebf4aa7089aa2bf40a0ba5eb73aaa90e45472168738cb0e866547e079075d0341c4728ed794cd449d0cdeceaf64d09173a2677e3f2621b56014df58ee8de2bbcf8cb2bf369e57a28e40b24df6d0b15d3417b7b7fce3c49230b3bb7c7080c4f1344380db37299d8c0c29a7c9e95bfc4f3ec70b767895be6ad798449f94c92f320696f8f9ec844fc9451f5ab30874e6c78673b932e24a918d5bc03f4fa647c8679631ad8b30ba4f9a1a790a535104f5079f781fa4c4aad807777327e08bd6e14b957e527650d39e2de7d30da7b2b27457d3539c2f99903054bd1a6d6301b2f83249db80dedc911059b047b5552407ed32b79b57ea4968198decf978fed532277fb189841d0c17f261f1a7c4618c33b258a71cb6b2b921c48735c1fbc375a80fe2d702e9f70db34547147f6d65153f1c508ff90aaf57dfde59ce6e56c5c52049b10fbc2640cd937bf5027d755444cd5e84aad66be4ccf04f710f0b970cbe7fc5e4e7a44f5d8fc2af94ec6df87862dabe0d85dd17e43728800d5dc8ff9e9e1fd349b9d6431f264836a447646a1ee2d73e6a4d1803d71b98510d94d53b5d07764400f413b7837f463e273980296ad5e78a62c83fb91354c25c2e28bfa19480ca41b0ca2d8beed456029eabc0ba69cf3504cf1207ab21bed0e3a60795f5b066019132212a9bd20e98b43bb32484454b7693bc63764d0ba8768da4336ad16a979b1d3e8b06e89afc4caafaddead83ad861a4596814062468d8cb2e131e71cf38c4e335bb3d1d9629251fd50a4273999a20e11d9a881fa2465c2ef32301fa8805ccb38b4656e3c815b54ef42f292d05847c0e5149b0a2ffd1719fcad10730b763406e9e453d2e4a556c397a984a7d8622466522e79f715f980279cb2d1d74d5fd15f8beb8df9c0a1fb93f0d5494ab701cd60cf2b613b484a191d873449c4815f44eebb62658ef90f7033a5e48d40c8f4ec9a25d4f1b73a150e5b03a4feec9142362e77d74d364bb8a40f9873efa2d9f458d84277fdb0063642d9ebc794356368a0b4e53f6ab0d9acf4418fa42d05c11db12cb67d6c4cbbfde4d85358abaf8042510db53c1540fcb40045b334bc54a17608e82433d1e9e53c99769b86b69b8cf1914584cdef479061718a2cc5bec5c31272e0463139721c0c3523a6ea31832b5c15aa59819304c217fbc51a2636bfff331e6dc1037e89be15c17d6708762976329353612f312b828e525c09b81afafa5ae5c25d82b70b6e55f892429a93e5eb4048ade675583e828da988f9d253123a06c0ed047adf4d93d20cc6c865f5a761fec27811305165cd4bda05f563215d0612944b88ade9d64477ae8fb441978720e70b0d77e2edafd3a2c571cd28a42cca2f289d157cec813c24e7f56051b4f28c153adfbb2c1ff90ea1fbc03cb89603387169072442f5d3081140e2256ebadcd775d2f1da92c15a9ff1ce6e980b3b1e91a7e379081ed46a4259fada21536af318eb2043ea16cece60b73acd415d239389cad9212e15bad1b1ae38f93c756635a9b87417faa844e4f43bae1f1ae68641a04f0ff1d55f9045cf7e3ed59fde5c07219b9113329356d4300885ee19c9955c5757db33c6ae75b49d035003171d02ccbd7321d6a14db0bd011d4877f04070dbda3b67bf22d083e", ++ "signingHash": "0xa998fed4c5f710305a6944f263d3d26edd3f4e3949d404f547eece43c10afd94", ++ "signature": "0x664be581b59cd495740a8a9aaaf7c6801a8bcedb531ace47da1be56ab30f1c4ccdaad6e81aa97352baca12b6c69754123bdee38c0d57bf15f0850572dc9b456234eaf37eb478f9117f952ae95a0d165d12d948b51672a03c9610a98cf332d6ad7ab8a87defb3ef7249cf8c3a815129ecf5600500702b7916ffe570cef0714e0c083a581c2971c45c4aa6b1c5226400e5fd594ea3ae97fe94a0dc7f1e0979b11a2f3d15ad54fea957992ada816f97cd76168a53ebffae36e5787b0f69e94b3e08eadefe4937ce8b383b5022e57ba7135cccd5b48e38916f1f0bdcbc5858bf93cad75195d4329ab41a518d820262b1b610f7bdbe1d6749d006a0dfe18d0be1a19fb3d8e37795b5bc37ebcd884f91459164cd2536b3496c4b25a4c74282ece5d64ab861184e942cf97f0edbe6e1293c4504d861e60d98c4f6523df27a1e80843005ad21665b3adcf46f349fb0d905d70631d2ee51c0086b252778eac27346eee65c0fc88c6db5936337b0afa7d8cec656612c81e11cc55abf421df770e6c2588c743d5540e1280be76309defaffde2b82c808976c337ee13735a21900a4686423625a0ba4819581edbe574e6d7053c5fd98d288db4104ea64fbc0e7ad11d50eb1470a882aa00ebfd983cdda14db34f7f968ccc93097cf037dceeb1375f2df4de09c8785f5fef8fbbe284566e5c9a2d38f5d2f9145b7f5087f6a89a56679680ca4180ad7da3ee45da685f0255a45db806e1379c1ed59bfe63feb7d74a239334c80ab9c1859d205ba798810adf8afc569170f94167439d137215d2ce0b2fa983ac138a3ce201cec4927c62b740912e7287417f86beb07af9c83fff843df1abd6e6f81865b82b3168528d1c473b4999b0179f1de8ae22e8ed7f698ee8463eb93fcfd4e017f29ab3f15a2e061de6deb09459d4ecc62741ffd8514d5c1f10eb3b1525ad60512ffad00e360e0d6add8ed6d6df48d63cf1e36f306bd76b1a245cdd09951727367e45b3a02d748dbfcbeea342be4cb0e1e3108e57993581409f8c6b34e2d38becfcf15c653b92c88412c087eddb0bc21eeb620317f49a9bf5d6164418a26322e1964fd676a547862c2a7ce5f13898e3fadec79a638f257532cfe977fe8ba5e96782f9e1e3e8b5f896a80d816141f0c988ea59ea321cffd23f298b76e49e92189e7f65718d14091daeb82d432470a2e472a39a154226a43c543db2bb933adad08344259ba08a65c543d2ee07b67328ca2d4e60e550b85215ea5873fedb505cc1e4db49e129249fb632ba22f11b897b9423d08f96ceaa863cd29dc16af6ac76c4388c2795250c04377277626992b085ed7be2cf1d63a2eda4b65e287081fdc4e9a3224ff57d81b3ba8d04eb88430a01c5eba35fad028e8de1812920cee2576d0bd291407c5fb70c5ce07104b9af7b98c4c709c7f019d4a66b136120b92612ad5e73dfcfc1da0fec3a7928e57d7d4769eecbaa4c269f2859d31d47d1ae40e4da5bfa6592074c5bf54b04c707b8d97455a0a606686e06bf5ef7617da2532f305267f61c5f627216b06ecc3da6c6f7b420e2dfecaa41657eab20c89927ad6eb5c4fcd513da89a637755f4a98c45cfa108f37415bbf49ee6cacfda8f22f7101a9798d844709a38f9168685f105157a937bb10d4f5b342deab8b8be8231490da52cc0c407bf942270ad93d4a9bed5c06499584835dbba75577dbbc47fd3d397f0417ade99549de4412c8893cf3e1a2ed74c234a7b60461efad57d9c9fed9eec3ebd652282b045174e19355d8cfcc10add9192ffffd428150dc3f48e0417e47d6407008c62486cd4057535a5a9ffc45ef559a51fbbece64306f64da9740108d0b51d01423379bb0a72fe050d2abf5f5acd25cff57f8e3bcdf938749c7bbf77437424ada88c167bcb6addc309b949a8e200b9a5b6797522635731b250e2405f98d054aaff4179ccb4b24159e4c033329100cadbef3da262203622d27899166f6c3c79a35daf7840853b9a3ce494bf035235cada8c31e28be56f0976ef9b96f15de57e6896a5aed57a7b5ce77cacd1c806ecf83566bee5d577f7414a106ce16ca6e8cbc144f571b1a37647041694a6409f68d15604960417a60bc4f1a878eb01e89159e701bb8fd96be45395a8b8c4f0cc5b982582741c4ccaff8961ea892f084b10646c7a27687a2d3c92324cc3a19a5b38ca2d5c573167ad54486ab3adfe9219882f947cc6a93a3827f4b51d76e90ffd1587646d921482022db8b5a22519be3a1e62e9a13ff9926e75c42e07aa295c8486df69dc96168e920e13c85eeda86dc560b9f5d4c77cb8ff001d390dc6c5a911c573ab1711df534930ef28832936c80cafcde731e999ce90741e669d93f653efba12466726fa997e7ad24d34395a02b8a4286f3604f120bf47e8ec41954e6def2de896be477ef7916f18214d28f58ae91ed8d7eae9e2df600d313b6cdb952195e9d030e0b8fc98a68cced3070f4abd2d252727320eb54b602ec861ba630ed3c5d6fc8c8b39f4f1e9c198a6dc8d5f825159ce15fcd7bea5701a225ac41bb152d1a077adc03ecf06870375f2d2618094a648b40244e4a981e7efeadc608c082dc1824c4ffdbc39afcc8c464ed5ca8dfc1540ae3a6c034364c356bf13f880abe8633fa041db78f4be0d17a4bb00959a697935c844c6ddc02222f7aebb1af2c96a757536159dc1dbc31131aefe4c198ccd2123eb5d0ca6147bbc9ab43e993508ef4ca383e2847d9fcb4c975dcb240bcfd164daa835311cf7dbde0f256deb5f3748b55c312a29a042725ecf07caa904513e71b4c0ebd3b31c2938e710e7c6be819bb3125e638ee4b54b4c8fe16df1cee7587e6bdfdcccf2e3b2aa4a54c86b0ae711f056c229c644f519f60570aed60fd3d0638dc341625fb4949d355e7d84cb478133d25730162623acbf5d36b9ed98dbd88b6bf7bf1f55006f5ba2d16297557f966cc01baca56e3f6435b724c0b6fa4cc0074efb0bb2cf0cba9a1c73d3337db7dc0c78b87746db7bbe5fc003edf608ce5d91dbd8b3cec0f738de14c8e2b6df47df91805eab5635402ff26245430641abd9511cffa6bf8d352a1f0d97ce2b08561c97a5f07a07e0ac42a2a73366442e0bf125a718553794b021640ef9f9f07040068cce2337606af46f897b7ad8ced05e14f7ab3e2bd847f810058d665a0c3b6903efd8f398c31e356b48216111d30348acf93d28dbec8e603cd16ea5d3b24f38367c52f5629fd51bec34e86ff21d4f5466a53aea7048babf3afaecec2c9de099bcb2845f66689e02306760ef5274e65f36fc61edf8112b3b414a878b8dbdc2ced0d7dae0e95c5d6e7c80a3b4cbccfe13293e454b6c757a8d9496999fb0c4d4dfe5e7f3f40e11172324323556687e95a4a6bfc4dadbec000000000000000000000000000000101a2f41", ++ "raw": "0x50f90ec5826d6180843b9aca0084773594008302d5709400000000000000000000000000000000000000010180c001b90520372c0f4874450fb92ace74d41bc0326c7f739589f9b86a97afa87ef65d5939e608c81a44909e41f41931349f744b7aedff8d5270b91c4a2f61192e2bb6e3e6d12231deab235359e78ccada386becb6a6a1105afc0999243ab8b363f7cd2c19b2b741c9969dcf20252f20d130dc05d9244f58d6de9b4fb4b6f0125c43b60ebe3c7272b6ae7578f1abb4bb5e2dcb31eebf4aa7089aa2bf40a0ba5eb73aaa90e45472168738cb0e866547e079075d0341c4728ed794cd449d0cdeceaf64d09173a2677e3f2621b56014df58ee8de2bbcf8cb2bf369e57a28e40b24df6d0b15d3417b7b7fce3c49230b3bb7c7080c4f1344380db37299d8c0c29a7c9e95bfc4f3ec70b767895be6ad798449f94c92f320696f8f9ec844fc9451f5ab30874e6c78673b932e24a918d5bc03f4fa647c8679631ad8b30ba4f9a1a790a535104f5079f781fa4c4aad807777327e08bd6e14b957e527650d39e2de7d30da7b2b27457d3539c2f99903054bd1a6d6301b2f83249db80dedc911059b047b5552407ed32b79b57ea4968198decf978fed532277fb189841d0c17f261f1a7c4618c33b258a71cb6b2b921c48735c1fbc375a80fe2d702e9f70db34547147f6d65153f1c508ff90aaf57dfde59ce6e56c5c52049b10fbc2640cd937bf5027d755444cd5e84aad66be4ccf04f710f0b970cbe7fc5e4e7a44f5d8fc2af94ec6df87862dabe0d85dd17e43728800d5dc8ff9e9e1fd349b9d6431f264836a447646a1ee2d73e6a4d1803d71b98510d94d53b5d07764400f413b7837f463e273980296ad5e78a62c83fb91354c25c2e28bfa19480ca41b0ca2d8beed456029eabc0ba69cf3504cf1207ab21bed0e3a60795f5b066019132212a9bd20e98b43bb32484454b7693bc63764d0ba8768da4336ad16a979b1d3e8b06e89afc4caafaddead83ad861a4596814062468d8cb2e131e71cf38c4e335bb3d1d9629251fd50a4273999a20e11d9a881fa2465c2ef32301fa8805ccb38b4656e3c815b54ef42f292d05847c0e5149b0a2ffd1719fcad10730b763406e9e453d2e4a556c397a984a7d8622466522e79f715f980279cb2d1d74d5fd15f8beb8df9c0a1fb93f0d5494ab701cd60cf2b613b484a191d873449c4815f44eebb62658ef90f7033a5e48d40c8f4ec9a25d4f1b73a150e5b03a4feec9142362e77d74d364bb8a40f9873efa2d9f458d84277fdb0063642d9ebc794356368a0b4e53f6ab0d9acf4418fa42d05c11db12cb67d6c4cbbfde4d85358abaf8042510db53c1540fcb40045b334bc54a17608e82433d1e9e53c99769b86b69b8cf1914584cdef479061718a2cc5bec5c31272e0463139721c0c3523a6ea31832b5c15aa59819304c217fbc51a2636bfff331e6dc1037e89be15c17d6708762976329353612f312b828e525c09b81afafa5ae5c25d82b70b6e55f892429a93e5eb4048ade675583e828da988f9d253123a06c0ed047adf4d93d20cc6c865f5a761fec27811305165cd4bda05f563215d0612944b88ade9d64477ae8fb441978720e70b0d77e2edafd3a2c571cd28a42cca2f289d157cec813c24e7f56051b4f28c153adfbb2c1ff90ea1fbc03cb89603387169072442f5d3081140e2256ebadcd775d2f1da92c15a9ff1ce6e980b3b1e91a7e379081ed46a4259fada21536af318eb2043ea16cece60b73acd415d239389cad9212e15bad1b1ae38f93c756635a9b87417faa844e4f43bae1f1ae68641a04f0ff1d55f9045cf7e3ed59fde5c07219b9113329356d4300885ee19c9955c5757db33c6ae75b49d035003171d02ccbd7321d6a14db0bd011d4877f04070dbda3b67bf22d083eb90974664be581b59cd495740a8a9aaaf7c6801a8bcedb531ace47da1be56ab30f1c4ccdaad6e81aa97352baca12b6c69754123bdee38c0d57bf15f0850572dc9b456234eaf37eb478f9117f952ae95a0d165d12d948b51672a03c9610a98cf332d6ad7ab8a87defb3ef7249cf8c3a815129ecf5600500702b7916ffe570cef0714e0c083a581c2971c45c4aa6b1c5226400e5fd594ea3ae97fe94a0dc7f1e0979b11a2f3d15ad54fea957992ada816f97cd76168a53ebffae36e5787b0f69e94b3e08eadefe4937ce8b383b5022e57ba7135cccd5b48e38916f1f0bdcbc5858bf93cad75195d4329ab41a518d820262b1b610f7bdbe1d6749d006a0dfe18d0be1a19fb3d8e37795b5bc37ebcd884f91459164cd2536b3496c4b25a4c74282ece5d64ab861184e942cf97f0edbe6e1293c4504d861e60d98c4f6523df27a1e80843005ad21665b3adcf46f349fb0d905d70631d2ee51c0086b252778eac27346eee65c0fc88c6db5936337b0afa7d8cec656612c81e11cc55abf421df770e6c2588c743d5540e1280be76309defaffde2b82c808976c337ee13735a21900a4686423625a0ba4819581edbe574e6d7053c5fd98d288db4104ea64fbc0e7ad11d50eb1470a882aa00ebfd983cdda14db34f7f968ccc93097cf037dceeb1375f2df4de09c8785f5fef8fbbe284566e5c9a2d38f5d2f9145b7f5087f6a89a56679680ca4180ad7da3ee45da685f0255a45db806e1379c1ed59bfe63feb7d74a239334c80ab9c1859d205ba798810adf8afc569170f94167439d137215d2ce0b2fa983ac138a3ce201cec4927c62b740912e7287417f86beb07af9c83fff843df1abd6e6f81865b82b3168528d1c473b4999b0179f1de8ae22e8ed7f698ee8463eb93fcfd4e017f29ab3f15a2e061de6deb09459d4ecc62741ffd8514d5c1f10eb3b1525ad60512ffad00e360e0d6add8ed6d6df48d63cf1e36f306bd76b1a245cdd09951727367e45b3a02d748dbfcbeea342be4cb0e1e3108e57993581409f8c6b34e2d38becfcf15c653b92c88412c087eddb0bc21eeb620317f49a9bf5d6164418a26322e1964fd676a547862c2a7ce5f13898e3fadec79a638f257532cfe977fe8ba5e96782f9e1e3e8b5f896a80d816141f0c988ea59ea321cffd23f298b76e49e92189e7f65718d14091daeb82d432470a2e472a39a154226a43c543db2bb933adad08344259ba08a65c543d2ee07b67328ca2d4e60e550b85215ea5873fedb505cc1e4db49e129249fb632ba22f11b897b9423d08f96ceaa863cd29dc16af6ac76c4388c2795250c04377277626992b085ed7be2cf1d63a2eda4b65e287081fdc4e9a3224ff57d81b3ba8d04eb88430a01c5eba35fad028e8de1812920cee2576d0bd291407c5fb70c5ce07104b9af7b98c4c709c7f019d4a66b136120b92612ad5e73dfcfc1da0fec3a7928e57d7d4769eecbaa4c269f2859d31d47d1ae40e4da5bfa6592074c5bf54b04c707b8d97455a0a606686e06bf5ef7617da2532f305267f61c5f627216b06ecc3da6c6f7b420e2dfecaa41657eab20c89927ad6eb5c4fcd513da89a637755f4a98c45cfa108f37415bbf49ee6cacfda8f22f7101a9798d844709a38f9168685f105157a937bb10d4f5b342deab8b8be8231490da52cc0c407bf942270ad93d4a9bed5c06499584835dbba75577dbbc47fd3d397f0417ade99549de4412c8893cf3e1a2ed74c234a7b60461efad57d9c9fed9eec3ebd652282b045174e19355d8cfcc10add9192ffffd428150dc3f48e0417e47d6407008c62486cd4057535a5a9ffc45ef559a51fbbece64306f64da9740108d0b51d01423379bb0a72fe050d2abf5f5acd25cff57f8e3bcdf938749c7bbf77437424ada88c167bcb6addc309b949a8e200b9a5b6797522635731b250e2405f98d054aaff4179ccb4b24159e4c033329100cadbef3da262203622d27899166f6c3c79a35daf7840853b9a3ce494bf035235cada8c31e28be56f0976ef9b96f15de57e6896a5aed57a7b5ce77cacd1c806ecf83566bee5d577f7414a106ce16ca6e8cbc144f571b1a37647041694a6409f68d15604960417a60bc4f1a878eb01e89159e701bb8fd96be45395a8b8c4f0cc5b982582741c4ccaff8961ea892f084b10646c7a27687a2d3c92324cc3a19a5b38ca2d5c573167ad54486ab3adfe9219882f947cc6a93a3827f4b51d76e90ffd1587646d921482022db8b5a22519be3a1e62e9a13ff9926e75c42e07aa295c8486df69dc96168e920e13c85eeda86dc560b9f5d4c77cb8ff001d390dc6c5a911c573ab1711df534930ef28832936c80cafcde731e999ce90741e669d93f653efba12466726fa997e7ad24d34395a02b8a4286f3604f120bf47e8ec41954e6def2de896be477ef7916f18214d28f58ae91ed8d7eae9e2df600d313b6cdb952195e9d030e0b8fc98a68cced3070f4abd2d252727320eb54b602ec861ba630ed3c5d6fc8c8b39f4f1e9c198a6dc8d5f825159ce15fcd7bea5701a225ac41bb152d1a077adc03ecf06870375f2d2618094a648b40244e4a981e7efeadc608c082dc1824c4ffdbc39afcc8c464ed5ca8dfc1540ae3a6c034364c356bf13f880abe8633fa041db78f4be0d17a4bb00959a697935c844c6ddc02222f7aebb1af2c96a757536159dc1dbc31131aefe4c198ccd2123eb5d0ca6147bbc9ab43e993508ef4ca383e2847d9fcb4c975dcb240bcfd164daa835311cf7dbde0f256deb5f3748b55c312a29a042725ecf07caa904513e71b4c0ebd3b31c2938e710e7c6be819bb3125e638ee4b54b4c8fe16df1cee7587e6bdfdcccf2e3b2aa4a54c86b0ae711f056c229c644f519f60570aed60fd3d0638dc341625fb4949d355e7d84cb478133d25730162623acbf5d36b9ed98dbd88b6bf7bf1f55006f5ba2d16297557f966cc01baca56e3f6435b724c0b6fa4cc0074efb0bb2cf0cba9a1c73d3337db7dc0c78b87746db7bbe5fc003edf608ce5d91dbd8b3cec0f738de14c8e2b6df47df91805eab5635402ff26245430641abd9511cffa6bf8d352a1f0d97ce2b08561c97a5f07a07e0ac42a2a73366442e0bf125a718553794b021640ef9f9f07040068cce2337606af46f897b7ad8ced05e14f7ab3e2bd847f810058d665a0c3b6903efd8f398c31e356b48216111d30348acf93d28dbec8e603cd16ea5d3b24f38367c52f5629fd51bec34e86ff21d4f5466a53aea7048babf3afaecec2c9de099bcb2845f66689e02306760ef5274e65f36fc61edf8112b3b414a878b8dbdc2ced0d7dae0e95c5d6e7c80a3b4cbccfe13293e454b6c757a8d9496999fb0c4d4dfe5e7f3f40e11172324323556687e95a4a6bfc4dadbec000000000000000000000000000000101a2f41", ++ "hash": "0x5ee00afa7d595efaffab2cf573a82e50c7d0f37bc1c2a53a11f7120155bf57b6", ++ "intrinsicGasEstimate": "135712" ++} +diff --git a/ethereum/core/src/test/resources/aere/pqtx/valid-ml-dsa-2.json b/ethereum/core/src/test/resources/aere/pqtx/valid-ml-dsa-2.json +new file mode 100755 +index 000000000..3a93c378b +--- /dev/null ++++ b/ethereum/core/src/test/resources/aere/pqtx/valid-ml-dsa-2.json +@@ -0,0 +1,28 @@ ++{ ++ "id": "valid-ml-dsa-2", ++ "expect": "valid", ++ "alg": 2, ++ "chainId": 28001, ++ "publicKey": "0x5424000ae9a496294dd6b99046cfdbb235ae71031b379ce274c13b0c96411740cb61021a5390fbe8fb56b642b4e5e5abb7aeab143e0037f8db283b9f2febd650265ed6b05f1b867c124aef245c1811422f92414447fd0b20844e0f1b1cb4eec052400aad52b9d773aae0015c0c390bfdde3d367bd8ef9c3c73bca3164b9d418ecbb2d25303bbd12739a858c5fac4cbfa0203b9f3c319e9bc89d17a7a244b57d6613b5caabdb528f077eff9727015b9c266d777c0ba11920118c7ec4dd59b380c40e361a5b23ce04be4c61e12a1c21e091fa0d890687cbf588944db3e34253c75cdf797378de79162e0b4ca309f550365a289f652dd6d559478912cff70a5417589f73f14640970694d7b5a6bd8f000a65c632f0326ed3d128fda96dbea68cb812634dd75e9f63813a5645336681378b6a63220c066550aba43a5b937131d41f08752d13949a115a74bf96fa1925ff4338e10df0301abe40aaa49e14a1a1d58eef005a0f1d0299267002cebeda20cadcea7419cd0c365c53aede1aa36efaf764d3d36169747cdc8ee9a09e0a0ac6bcaeaeded0cd303c965d705d1b91a8b35f408ad8a033b02ff57610bddbfa60e2114e53f20f8fcad15dffac6a494c3f4f631590f2be3422af14cba7c900c9c38796952e20c3069bea2ab54bf0edf9315903aa5cf68549f878eafcc77561b36f8b78d950871ed4914417316f868a2bc54c72d3c9929f0138a2f0cbccb7c4141e94bb8a7ae11f660861cbbe498697d7327709271460a75bd4e024de3d4d6fe5af066944c96ba87459fa948a95eeb61abbaf643190ccaa9cd18591eb6f249a6eb92db89fc95616d21b41d7d15424b80a750938a12cd57d50ef95aaf222025dc4d81df320d9bc32c608b1c5a1aac8bd14c0f7a4c4380cdd65c793a6287050ff4d609102fd91e47e7365fb55513aeca273618528c199f7bb151e9c11a0bf202147734d1be9d5d0e9318096fa567317ed9fa6a8efe201b915b9aa05ca0d19e15c9a80449cc130b398ebbac3d938f55eea8f74e024b0e33dfecdbea02b920ddc5b277146685916194bfa1f8ac7784c7ed76923d4ff9ed97d3ceac283f9b26977df0133c5fa4fb5fcec577bc152e926529f06ff50c204c083c0b4d6b8d474cf5cf7cfcf7acf76921225443eb0abf5e4c76d0a3f524243ee4e372e80ff0f76dd9d135f240114a2ebe6d69609b6c60d275f251d7dceadc600d18e0cf8bc8fa8392e61fa32b8f7481c4883e19d820b51af0f87d3f7faae44e3b908fc759c6de59361487d8e424ffbd662d57a68801110e9cb099214143253949cc479eaae0e321fecab90c63b06819ce3cf56fc2fc1419408075c658024a28950aa47d549bcdb8b5bc9aeab463466c7834ca7d73058c206a95a023f0c8ac95badd383e63041e1f0ef69f51634f5ce2f48d80f8f2e92f4a1fb5322930141fc50e1e20bd235a8b8d768d473b46711257d028e710f82f0b6bd923da05ca64ae805f708577e125b3fca2feb5656f61ded034eaae481a195628801c82db85345f9ffdc72c860cffe2264ba4be2a08fdb37a4f5f80276baba4ea637847335c89dbd35504389f68b2d61ea66d4eac5fd0057d9e757d929214a7d83daf2fc2b3f840e083792d20d772584b252ba955ae8ae8c36af648c59ae0b25d3bca19413fbd749077b6969db5847409d13e4a67387e59116fe3836a733f3253d47f9919f49618fac8edf5fd711a4239447655d6c40f479f86ef3c9d4dc1a6f9683a3f1bc5c9730025e76ea1588d43494f59823f42c207e34412f73c7e2a126501b8fb810f12995c213063d909a5a9c2653d3511ceb809efa69c638f4c977d46a2a305cc831ecc0feebf5d9356ede6c780da78fe2ad683b3312282d5142612c0d1f85108de04de5c3d5414b62826d5b75f8887dc2d932c40f306c08e5d4f69ef4d344fe4317552df9890d0961e3d857bd9b2c5b44536d8606f62f66d14ddd43b0e5d3379cf66d664616df12b1c62385a57f6a562c34e120b79d64ca39d8b23e1e07b2c03910d711f09df5b54cb0e133dd751bea7832fdb38de2f1dafae69fbdf82b6887ff0a92940299020a835b958bb01b25b8699a0846614cb256a6621ef92e4a0947df9141ed4f91eca527aef25862158a8fa712dd60dda947f6a16e077810e29542a56f9244c34a73454b9ad0d8e8c40bd503efe12c4e5bf7d0ff8e6ab642a039f4e8b575d33c0c3de77a1abfa60b0252d7a549dd791d9630c8b848988157608448d77938bfb27beaea8f9b4a259263797c5b2fbdc7f469b2769f7dcb7b9478cdf5f109322eb2f0a83163469b72d8121fe8e01df227563cbd941a1f63961e2a010c6322a812a014279583a0066492a17d1a15c07024f6b8b1561041b3db608ec1c7475ce4d086e60523e85d7a955841558a35535d6e52cb166669a69049646b94d2fdb901ffbb1446a2d7f58696bce000ccbbb6942b0ad6480d006602c228218d3a2f0bc6a5d5c73acda9fc4b0b496a7d527050f8d77b71641f030368e9494bfe8e9dbf6dd4040426da1b0423739b40450273f4a8d551637e5fe3ca8d54fc476e6278e75cebf4a4ac3ac8ef3807041c4b274568797c8caaffae7df08c6c158d92411db981ea02cc212064c3ad6a9cd1cc02087acce5ceb8d7cddaec429d199c3d90670288be6e88e6826d1f6c417df5f35139c71a62c0267adfce7cb3aa49e827165b28c8cc157a691cb52ef103d8d7ca0ebfc628904d0b2d365dd3401b620d6d93cef670e2f", ++ "seed": "0xdaaaf3ca4a40b10b8d8329296d5a0eb0ea604486c8bb582e6d7cd3d7f7dacc63", ++ "tx": { ++ "alg": 2, ++ "publicKey": "0x5424000ae9a496294dd6b99046cfdbb235ae71031b379ce274c13b0c96411740cb61021a5390fbe8fb56b642b4e5e5abb7aeab143e0037f8db283b9f2febd650265ed6b05f1b867c124aef245c1811422f92414447fd0b20844e0f1b1cb4eec052400aad52b9d773aae0015c0c390bfdde3d367bd8ef9c3c73bca3164b9d418ecbb2d25303bbd12739a858c5fac4cbfa0203b9f3c319e9bc89d17a7a244b57d6613b5caabdb528f077eff9727015b9c266d777c0ba11920118c7ec4dd59b380c40e361a5b23ce04be4c61e12a1c21e091fa0d890687cbf588944db3e34253c75cdf797378de79162e0b4ca309f550365a289f652dd6d559478912cff70a5417589f73f14640970694d7b5a6bd8f000a65c632f0326ed3d128fda96dbea68cb812634dd75e9f63813a5645336681378b6a63220c066550aba43a5b937131d41f08752d13949a115a74bf96fa1925ff4338e10df0301abe40aaa49e14a1a1d58eef005a0f1d0299267002cebeda20cadcea7419cd0c365c53aede1aa36efaf764d3d36169747cdc8ee9a09e0a0ac6bcaeaeded0cd303c965d705d1b91a8b35f408ad8a033b02ff57610bddbfa60e2114e53f20f8fcad15dffac6a494c3f4f631590f2be3422af14cba7c900c9c38796952e20c3069bea2ab54bf0edf9315903aa5cf68549f878eafcc77561b36f8b78d950871ed4914417316f868a2bc54c72d3c9929f0138a2f0cbccb7c4141e94bb8a7ae11f660861cbbe498697d7327709271460a75bd4e024de3d4d6fe5af066944c96ba87459fa948a95eeb61abbaf643190ccaa9cd18591eb6f249a6eb92db89fc95616d21b41d7d15424b80a750938a12cd57d50ef95aaf222025dc4d81df320d9bc32c608b1c5a1aac8bd14c0f7a4c4380cdd65c793a6287050ff4d609102fd91e47e7365fb55513aeca273618528c199f7bb151e9c11a0bf202147734d1be9d5d0e9318096fa567317ed9fa6a8efe201b915b9aa05ca0d19e15c9a80449cc130b398ebbac3d938f55eea8f74e024b0e33dfecdbea02b920ddc5b277146685916194bfa1f8ac7784c7ed76923d4ff9ed97d3ceac283f9b26977df0133c5fa4fb5fcec577bc152e926529f06ff50c204c083c0b4d6b8d474cf5cf7cfcf7acf76921225443eb0abf5e4c76d0a3f524243ee4e372e80ff0f76dd9d135f240114a2ebe6d69609b6c60d275f251d7dceadc600d18e0cf8bc8fa8392e61fa32b8f7481c4883e19d820b51af0f87d3f7faae44e3b908fc759c6de59361487d8e424ffbd662d57a68801110e9cb099214143253949cc479eaae0e321fecab90c63b06819ce3cf56fc2fc1419408075c658024a28950aa47d549bcdb8b5bc9aeab463466c7834ca7d73058c206a95a023f0c8ac95badd383e63041e1f0ef69f51634f5ce2f48d80f8f2e92f4a1fb5322930141fc50e1e20bd235a8b8d768d473b46711257d028e710f82f0b6bd923da05ca64ae805f708577e125b3fca2feb5656f61ded034eaae481a195628801c82db85345f9ffdc72c860cffe2264ba4be2a08fdb37a4f5f80276baba4ea637847335c89dbd35504389f68b2d61ea66d4eac5fd0057d9e757d929214a7d83daf2fc2b3f840e083792d20d772584b252ba955ae8ae8c36af648c59ae0b25d3bca19413fbd749077b6969db5847409d13e4a67387e59116fe3836a733f3253d47f9919f49618fac8edf5fd711a4239447655d6c40f479f86ef3c9d4dc1a6f9683a3f1bc5c9730025e76ea1588d43494f59823f42c207e34412f73c7e2a126501b8fb810f12995c213063d909a5a9c2653d3511ceb809efa69c638f4c977d46a2a305cc831ecc0feebf5d9356ede6c780da78fe2ad683b3312282d5142612c0d1f85108de04de5c3d5414b62826d5b75f8887dc2d932c40f306c08e5d4f69ef4d344fe4317552df9890d0961e3d857bd9b2c5b44536d8606f62f66d14ddd43b0e5d3379cf66d664616df12b1c62385a57f6a562c34e120b79d64ca39d8b23e1e07b2c03910d711f09df5b54cb0e133dd751bea7832fdb38de2f1dafae69fbdf82b6887ff0a92940299020a835b958bb01b25b8699a0846614cb256a6621ef92e4a0947df9141ed4f91eca527aef25862158a8fa712dd60dda947f6a16e077810e29542a56f9244c34a73454b9ad0d8e8c40bd503efe12c4e5bf7d0ff8e6ab642a039f4e8b575d33c0c3de77a1abfa60b0252d7a549dd791d9630c8b848988157608448d77938bfb27beaea8f9b4a259263797c5b2fbdc7f469b2769f7dcb7b9478cdf5f109322eb2f0a83163469b72d8121fe8e01df227563cbd941a1f63961e2a010c6322a812a014279583a0066492a17d1a15c07024f6b8b1561041b3db608ec1c7475ce4d086e60523e85d7a955841558a35535d6e52cb166669a69049646b94d2fdb901ffbb1446a2d7f58696bce000ccbbb6942b0ad6480d006602c228218d3a2f0bc6a5d5c73acda9fc4b0b496a7d527050f8d77b71641f030368e9494bfe8e9dbf6dd4040426da1b0423739b40450273f4a8d551637e5fe3ca8d54fc476e6278e75cebf4a4ac3ac8ef3807041c4b274568797c8caaffae7df08c6c158d92411db981ea02cc212064c3ad6a9cd1cc02087acce5ceb8d7cddaec429d199c3d90670288be6e88e6826d1f6c417df5f35139c71a62c0267adfce7cb3aa49e827165b28c8cc157a691cb52ef103d8d7ca0ebfc628904d0b2d365dd3401b620d6d93cef670e2f", ++ "chainId": 28001, ++ "nonce": 0, ++ "maxPriorityFeePerGas": "1000000000", ++ "maxFeePerGas": "2000000000", ++ "to": "0x0000000000000000000000000000000000000001", ++ "value": "1", ++ "data": "0x", ++ "accessList": [] ++ }, ++ "algName": "ML-DSA-65", ++ "sender": "0x0602be26ec9e01dd88121f7c203ead9eecb9ad3e", ++ "signingPreimage": "0x50f907ce826d6180843b9aca008477359400830383209400000000000000000000000000000000000000010180c002b907a05424000ae9a496294dd6b99046cfdbb235ae71031b379ce274c13b0c96411740cb61021a5390fbe8fb56b642b4e5e5abb7aeab143e0037f8db283b9f2febd650265ed6b05f1b867c124aef245c1811422f92414447fd0b20844e0f1b1cb4eec052400aad52b9d773aae0015c0c390bfdde3d367bd8ef9c3c73bca3164b9d418ecbb2d25303bbd12739a858c5fac4cbfa0203b9f3c319e9bc89d17a7a244b57d6613b5caabdb528f077eff9727015b9c266d777c0ba11920118c7ec4dd59b380c40e361a5b23ce04be4c61e12a1c21e091fa0d890687cbf588944db3e34253c75cdf797378de79162e0b4ca309f550365a289f652dd6d559478912cff70a5417589f73f14640970694d7b5a6bd8f000a65c632f0326ed3d128fda96dbea68cb812634dd75e9f63813a5645336681378b6a63220c066550aba43a5b937131d41f08752d13949a115a74bf96fa1925ff4338e10df0301abe40aaa49e14a1a1d58eef005a0f1d0299267002cebeda20cadcea7419cd0c365c53aede1aa36efaf764d3d36169747cdc8ee9a09e0a0ac6bcaeaeded0cd303c965d705d1b91a8b35f408ad8a033b02ff57610bddbfa60e2114e53f20f8fcad15dffac6a494c3f4f631590f2be3422af14cba7c900c9c38796952e20c3069bea2ab54bf0edf9315903aa5cf68549f878eafcc77561b36f8b78d950871ed4914417316f868a2bc54c72d3c9929f0138a2f0cbccb7c4141e94bb8a7ae11f660861cbbe498697d7327709271460a75bd4e024de3d4d6fe5af066944c96ba87459fa948a95eeb61abbaf643190ccaa9cd18591eb6f249a6eb92db89fc95616d21b41d7d15424b80a750938a12cd57d50ef95aaf222025dc4d81df320d9bc32c608b1c5a1aac8bd14c0f7a4c4380cdd65c793a6287050ff4d609102fd91e47e7365fb55513aeca273618528c199f7bb151e9c11a0bf202147734d1be9d5d0e9318096fa567317ed9fa6a8efe201b915b9aa05ca0d19e15c9a80449cc130b398ebbac3d938f55eea8f74e024b0e33dfecdbea02b920ddc5b277146685916194bfa1f8ac7784c7ed76923d4ff9ed97d3ceac283f9b26977df0133c5fa4fb5fcec577bc152e926529f06ff50c204c083c0b4d6b8d474cf5cf7cfcf7acf76921225443eb0abf5e4c76d0a3f524243ee4e372e80ff0f76dd9d135f240114a2ebe6d69609b6c60d275f251d7dceadc600d18e0cf8bc8fa8392e61fa32b8f7481c4883e19d820b51af0f87d3f7faae44e3b908fc759c6de59361487d8e424ffbd662d57a68801110e9cb099214143253949cc479eaae0e321fecab90c63b06819ce3cf56fc2fc1419408075c658024a28950aa47d549bcdb8b5bc9aeab463466c7834ca7d73058c206a95a023f0c8ac95badd383e63041e1f0ef69f51634f5ce2f48d80f8f2e92f4a1fb5322930141fc50e1e20bd235a8b8d768d473b46711257d028e710f82f0b6bd923da05ca64ae805f708577e125b3fca2feb5656f61ded034eaae481a195628801c82db85345f9ffdc72c860cffe2264ba4be2a08fdb37a4f5f80276baba4ea637847335c89dbd35504389f68b2d61ea66d4eac5fd0057d9e757d929214a7d83daf2fc2b3f840e083792d20d772584b252ba955ae8ae8c36af648c59ae0b25d3bca19413fbd749077b6969db5847409d13e4a67387e59116fe3836a733f3253d47f9919f49618fac8edf5fd711a4239447655d6c40f479f86ef3c9d4dc1a6f9683a3f1bc5c9730025e76ea1588d43494f59823f42c207e34412f73c7e2a126501b8fb810f12995c213063d909a5a9c2653d3511ceb809efa69c638f4c977d46a2a305cc831ecc0feebf5d9356ede6c780da78fe2ad683b3312282d5142612c0d1f85108de04de5c3d5414b62826d5b75f8887dc2d932c40f306c08e5d4f69ef4d344fe4317552df9890d0961e3d857bd9b2c5b44536d8606f62f66d14ddd43b0e5d3379cf66d664616df12b1c62385a57f6a562c34e120b79d64ca39d8b23e1e07b2c03910d711f09df5b54cb0e133dd751bea7832fdb38de2f1dafae69fbdf82b6887ff0a92940299020a835b958bb01b25b8699a0846614cb256a6621ef92e4a0947df9141ed4f91eca527aef25862158a8fa712dd60dda947f6a16e077810e29542a56f9244c34a73454b9ad0d8e8c40bd503efe12c4e5bf7d0ff8e6ab642a039f4e8b575d33c0c3de77a1abfa60b0252d7a549dd791d9630c8b848988157608448d77938bfb27beaea8f9b4a259263797c5b2fbdc7f469b2769f7dcb7b9478cdf5f109322eb2f0a83163469b72d8121fe8e01df227563cbd941a1f63961e2a010c6322a812a014279583a0066492a17d1a15c07024f6b8b1561041b3db608ec1c7475ce4d086e60523e85d7a955841558a35535d6e52cb166669a69049646b94d2fdb901ffbb1446a2d7f58696bce000ccbbb6942b0ad6480d006602c228218d3a2f0bc6a5d5c73acda9fc4b0b496a7d527050f8d77b71641f030368e9494bfe8e9dbf6dd4040426da1b0423739b40450273f4a8d551637e5fe3ca8d54fc476e6278e75cebf4a4ac3ac8ef3807041c4b274568797c8caaffae7df08c6c158d92411db981ea02cc212064c3ad6a9cd1cc02087acce5ceb8d7cddaec429d199c3d90670288be6e88e6826d1f6c417df5f35139c71a62c0267adfce7cb3aa49e827165b28c8cc157a691cb52ef103d8d7ca0ebfc628904d0b2d365dd3401b620d6d93cef670e2f", ++ "signingHash": "0xce8c919ee46badc976518a728e7720c6afe65322bb25ff83a28c364c7ba1999e", ++ "signature": "0x33fc1486a0e70aeb6fef62ce5a0dfb51fcec12164e4024b0c49ce8b65e01356d80a9571af8f69a9c7256ed549d699a31ceff21c13c482ee0c5759f96125b0099495feacf22a001b2c887b107a9ca9e84de461fbc6e143566de56c710156de5b5f4b7ffe29c0124c1fbb6a792110284c9ad713d89bbb5ed444d09408c65a9f74c5f6c4fd281d31738db82913dcd1ac1fccbba24dc933fb9a8e6c4e426fb98590d7da0252c34d866c114526ded1c9d96daea12ecbd8f2a44531dfb72f02c02b267edf0560b1fb06b087bb6ce02b2922d92f81bb7da99b90e4a2dd68aa858759584490be602369dd606a510b6228e3b941421a3006a697b79866cae011d8aa496de4f3bfffdc0a087d01323db849f6237d7ac476d18bb417f26a697388b4c0a78533f3dd763398719c9ee5bf0299786fb8c4143af8c1195103390d4494b735c06b5815c454dba2fcdd27d214c48712609ffe7ad7c082e835d7477de2358fb3c4b5162027f816ca85aa4a46c25105175e77c8e5394db6fb3999c25ce578f066329182b28fd5b68769cb07ebc67e47a537f6cb62bfd18af162113a4df7f8908e0ce953b6a9637c850f24de0fda239c25fe4352f3b381fc270384d51d1af1ef0dea8501f98ea71f26de97e98799c4b2a316a216bd4af4861ad83fa4570532029564581bb35ac543355ba3aa246b59c6a5d9a91b938f3c4f0ad6d6f567fffdb6fb78402ded5fdc544a0f5fd87e4677326ca003dffdba8fd390487a6469991506a18f376b12db5be3bdcf4a4bc2f5b34dd8f7066bec3b40a6502f414eac3cd1ed43431b033757e86cca57691b0e1f7eec68fad9423f8b0620aa94798546bc878cf29d74ae74af5244ed9b97107fb83abe25dc160ec05cda1c50b76db981aca781868a7913a3e7c98aa5a3ea8bc81270e6f565138dba33c9c7e887641fd3dafd458b9c64600151038db05725a976950307c662c2eb668e3067f943b9bfd805307729b39ed77309fd5e67ecfc56cc231057a6628fb6f528fb969378529dea2c58fa0b4d791ca591ced733e8cf93a8e19907538f45c2618612648d3e1a78492afc154b7bb0285aea97d618e74a74530b492c66c6bd31557cdc55476b3f163196ec8e87f110834ffb8c232dc77cbf2f7b490f520b3b328af44fecca860a19bee9af6a5f69f63487d7dc6b7bf1233c4735522fedd475f905f044a0a7f658a7a378958cca56a163a343a010f4b65d1bc38cd57d08e0f887293fbc106ad7b15aacca6d51558cb8478b119acde04a19b007802a8e9578121304622aee7858d1794ade88973d6fdf981ebabc98fc417d765c075bf8f76b9781dff7c60071878027c057aade8eaf820186035499e9e5fbf8791e6fc0a0e49047af312a8b1b1e80628dc4db4f45842e19096a54733d28028203aa668bd8655631a01c000c956c32ff10572fdbc95b2f26d09fca84c093ddaa122ad93598c03de9273873ece5096648a6251a27e993ec7b6ea4f2181c09bb06852799323f3e993a4b7461439f702cb9df183fcaab11b8bd2ad391924d5e6ef261fea01c91cdc10d9345c290dfc3c040318d11419563d407c8365fef3dd31d8f9d7d338f5f2cd00433d1d06547ae9c650d14d9a7e5b683f0458e7a909d90d4a57201ca5b8753c13bda3591f9cadffd0987d1c7fca1c5a0cec1e13c612d0390d7c0e2f422c97cda29959cd7b714fbeaa5da7b0bcf911c2744a9a25c7d49e93342f61e10a33fda80fadfed2668bfab8767ce80480093b4845cd5ec83082e8f9ad63236743e72842d1a3226640a549c2c1d002a176e2427b2ae612dc272e3d9fa80a904eb8f93f494a52706346c08b06c9d1d8a61a9c7112c0933910882f0da6eea3c389f4faeea51c03394eab4cc02056d704b8839afd76f0839be5ffdc873b88f9a755be6f0229b0843839a25d63c18da89be1c5da930a101913ffba01fd00f2a447d5f58ae8f21554feb47f42d40c32158219dbbb1e5e96ab3dc26cd6a435525c265b65b0bf48d68e9b3c9178a509833aa47c23e63aa9a6f74bef1e4ef863556b11c014b85ac7c157f1e64531b676eb28c68e82e0089d2bd798213c836cc354d4883dc0b607456ab7f894f0a1f73356fc05a5c97a9cf3b231890c39105b751f6fc54063a08f3d3a264ab90cb7de1d0e2f6acb92302cc9b4bb512e720e6ff8343004028ad223a70d395c5f974d3ddd380f798e80edb4b13555c3be0cb1a500e19a8b1b05f9cae85d7eed756ebcfa57828f1054296ffa48d426ceb83c69ebe1b983421f42316ca6f50bf8df0f4d4aeef85005638a304d88764ec26366a1437867c89800aee55ba428e0c07542e123d748c795560153007f768b20bb057fc2643b7db992c3b9cbfd88657b17fbe8a783da2050affb518cab4226cdf28fa97f48651bd142932835f061f8642e79d1f7ae8ff8665aa85aba24a850b2042dd37e61e665a451f17cbe9d57ed5ecb1dce9ec34c576762929dbf46964de7c44c9f6b10fcf52b48649ded2d491bbe4bb6ad578cd31cbdea6df8342dbb7f33857617250b014fc6d887a448f7a5cef25ff69dbc94733bfac1d5f8593af7fbe4759d23ba8da1f405ba179c96d8ae49eb760446d842f0d61ab08508a3e31e4953e13a1b197d00644303e8a9513199583709f41757740a0dff347c3859d3a4ec391481af349019f75ccfdba111a4c338fd8283733115ce7c114111abb157c1a5a84f7b39779a54f000827a250bff00dbe9cb7162d81f6c942b719f9eb973ac7d4652b911622396c517c6e9c7e5f839c049855567fb53ae84579388e3c87af420272558c686209fd92fc339a553f873995932d9dce52f5e60cc0889c3283f94a0d2d5d011154d9935e1dfbb7e747b393bdd8a84f601adca56d4ac5e8e6f28100a7d5522de50dbf1bd7f5604fdfa660a58dc681b4b230abbc4f594a972a73456de1b7f48975cd03313a80074f5d6701db61860f33dfcc9f06e313e8881c385a50de28ced014daf702f01c45368419b73383008b9191293f59fa87a1ceb0cee64d486842ed7c289dca3b3f2e261df644de0f739079ba5ea195468f0693a995dae8e0f7e4611196824f329fc803bc3588d7d941d1fe86a3ecdb3eb8f6d4066ecee371f9a14e5bd46a40b53e87325bab7255099b4fe7f7b09eddaefc6582feecb8dec02b5e58d735a927d052e263027fa879fceb852b9e08db4636c7152f272a43ae4037c50210e90caa3e008fbfa98ed42cd425259096fc06644edab9b768779dcd892747f18a9cecc6aa31dc27d3f12a540447653edd5bed68f50da71b7992434b74d0209746f93ea429691fcb99da0ca32df822ec4332f7eb4bbe6d2111ac84a55932b3e173e388ab96a5a8a17c7d8b8131cf566e0335b0c4a37c5ae24e73c81146031295b69ce3032fa8e4c7616558179cc561f14864190d458cb82370749d595966047de698256126a4ca23a6bbd39201b8649fbcd387474ecf5b9f732e65938fe393a2a1269b06b9b49c5d8882d70776fc823da5842d812be2a65d2f5ee0d64f8258e761e6f2d4c1a825963b367d70b9e81b46fc25e4804fe9decf3aa0e47f1eba573a24f0e4f7b8cf8a886c7ef4175bd317709feeabe46df9719e7d7a4bc2c1bbc15a58b5332ea558e160f5d3b7aa84fe54ea7a24bd8e394ab794e72db7946780272556be33b7f32a0ee651c013d8ed3b5d75bc1c4826dc94f9e3711c53be3bcdf361329544f2993a189b6431a6dda8b81cb1b7c3a69ba96c1ac84a55100c4985cc85ce272e8eb5a57c563a7ad782664d561c3400e779317122d425c9c4193414bebd4d6ad31ba916448f65e9a7ca1f856dacf9d88b695410c8491a759d2c0304e8368b39cf502d0abf7233b1ba9671bc422ce182833ead52597481ea3ea26a2cc35e12a8c83bbb263202d5e9e7a5bb8ab2a5a844d596c24fdac423981fc4dc4291944ae476d59c9a31a05f79f0fe4b39f145c4fba857fa553a17fb2d5f941d5dac8dd3dd07e48eabfe5b98f3c41e7a84986dc3a7a3bb0ed2d4232cffbc50f167d58fb52fa3b357e76430d7f0f1aff6e2d51ea8f5b98bb66c5b5687821f33b9b6c2e407ad0e8140c4d5d4f1739167f8d63284caf4a43688473cd97bab401218fbb2ea13be81bbc05a81042a02eb1843bd46ca535e778e207c79ce3fe511a55d21bf20b24bcdfb43fa62b3469fae2ebd61e362d5d663fb2c08cbde5df9d8cc1faaf6370b5328ffccde873849299d61477a9fcf7c5c96fbc51b24c0f2cc6e39a46f4a9c1f5214f2cd06db62a37e2c6f477ceac0b92eb882d7bcd1fc3721a89745473eb45eee9a4fe6ab323d28f4db957d0c5dc1ed25ebc194f744a2cfe98474868e6acf5641ad454b3626ac2a27a9db416eb1763935eaa5612e43c49e8959ebcf5133863e98c19618e936f33952699fc224f41a4e259b40228047ed6a17cf4f3a42dc76df1346acb0986984f1692b5ed28f0825a23094ddbb8c1750e9035b1df2166f142e7f179473098ed73acb9ffb8be530542f31de88d6677f805cc30ccc0b60787e61f9d195ceacf07a35d979f9c3521f23671f378a6cd58ed2b0620ff7e2def5025f878d70008744c6a53caf325293353747c94c5ddf12e3a42686f97adc4db62b5e2e91132345963b7292fc9d13b484f75d8da000000000000000000000000000000000a13171d2127", ++ "raw": "0x50f914be826d6180843b9aca008477359400830383209400000000000000000000000000000000000000010180c002b907a05424000ae9a496294dd6b99046cfdbb235ae71031b379ce274c13b0c96411740cb61021a5390fbe8fb56b642b4e5e5abb7aeab143e0037f8db283b9f2febd650265ed6b05f1b867c124aef245c1811422f92414447fd0b20844e0f1b1cb4eec052400aad52b9d773aae0015c0c390bfdde3d367bd8ef9c3c73bca3164b9d418ecbb2d25303bbd12739a858c5fac4cbfa0203b9f3c319e9bc89d17a7a244b57d6613b5caabdb528f077eff9727015b9c266d777c0ba11920118c7ec4dd59b380c40e361a5b23ce04be4c61e12a1c21e091fa0d890687cbf588944db3e34253c75cdf797378de79162e0b4ca309f550365a289f652dd6d559478912cff70a5417589f73f14640970694d7b5a6bd8f000a65c632f0326ed3d128fda96dbea68cb812634dd75e9f63813a5645336681378b6a63220c066550aba43a5b937131d41f08752d13949a115a74bf96fa1925ff4338e10df0301abe40aaa49e14a1a1d58eef005a0f1d0299267002cebeda20cadcea7419cd0c365c53aede1aa36efaf764d3d36169747cdc8ee9a09e0a0ac6bcaeaeded0cd303c965d705d1b91a8b35f408ad8a033b02ff57610bddbfa60e2114e53f20f8fcad15dffac6a494c3f4f631590f2be3422af14cba7c900c9c38796952e20c3069bea2ab54bf0edf9315903aa5cf68549f878eafcc77561b36f8b78d950871ed4914417316f868a2bc54c72d3c9929f0138a2f0cbccb7c4141e94bb8a7ae11f660861cbbe498697d7327709271460a75bd4e024de3d4d6fe5af066944c96ba87459fa948a95eeb61abbaf643190ccaa9cd18591eb6f249a6eb92db89fc95616d21b41d7d15424b80a750938a12cd57d50ef95aaf222025dc4d81df320d9bc32c608b1c5a1aac8bd14c0f7a4c4380cdd65c793a6287050ff4d609102fd91e47e7365fb55513aeca273618528c199f7bb151e9c11a0bf202147734d1be9d5d0e9318096fa567317ed9fa6a8efe201b915b9aa05ca0d19e15c9a80449cc130b398ebbac3d938f55eea8f74e024b0e33dfecdbea02b920ddc5b277146685916194bfa1f8ac7784c7ed76923d4ff9ed97d3ceac283f9b26977df0133c5fa4fb5fcec577bc152e926529f06ff50c204c083c0b4d6b8d474cf5cf7cfcf7acf76921225443eb0abf5e4c76d0a3f524243ee4e372e80ff0f76dd9d135f240114a2ebe6d69609b6c60d275f251d7dceadc600d18e0cf8bc8fa8392e61fa32b8f7481c4883e19d820b51af0f87d3f7faae44e3b908fc759c6de59361487d8e424ffbd662d57a68801110e9cb099214143253949cc479eaae0e321fecab90c63b06819ce3cf56fc2fc1419408075c658024a28950aa47d549bcdb8b5bc9aeab463466c7834ca7d73058c206a95a023f0c8ac95badd383e63041e1f0ef69f51634f5ce2f48d80f8f2e92f4a1fb5322930141fc50e1e20bd235a8b8d768d473b46711257d028e710f82f0b6bd923da05ca64ae805f708577e125b3fca2feb5656f61ded034eaae481a195628801c82db85345f9ffdc72c860cffe2264ba4be2a08fdb37a4f5f80276baba4ea637847335c89dbd35504389f68b2d61ea66d4eac5fd0057d9e757d929214a7d83daf2fc2b3f840e083792d20d772584b252ba955ae8ae8c36af648c59ae0b25d3bca19413fbd749077b6969db5847409d13e4a67387e59116fe3836a733f3253d47f9919f49618fac8edf5fd711a4239447655d6c40f479f86ef3c9d4dc1a6f9683a3f1bc5c9730025e76ea1588d43494f59823f42c207e34412f73c7e2a126501b8fb810f12995c213063d909a5a9c2653d3511ceb809efa69c638f4c977d46a2a305cc831ecc0feebf5d9356ede6c780da78fe2ad683b3312282d5142612c0d1f85108de04de5c3d5414b62826d5b75f8887dc2d932c40f306c08e5d4f69ef4d344fe4317552df9890d0961e3d857bd9b2c5b44536d8606f62f66d14ddd43b0e5d3379cf66d664616df12b1c62385a57f6a562c34e120b79d64ca39d8b23e1e07b2c03910d711f09df5b54cb0e133dd751bea7832fdb38de2f1dafae69fbdf82b6887ff0a92940299020a835b958bb01b25b8699a0846614cb256a6621ef92e4a0947df9141ed4f91eca527aef25862158a8fa712dd60dda947f6a16e077810e29542a56f9244c34a73454b9ad0d8e8c40bd503efe12c4e5bf7d0ff8e6ab642a039f4e8b575d33c0c3de77a1abfa60b0252d7a549dd791d9630c8b848988157608448d77938bfb27beaea8f9b4a259263797c5b2fbdc7f469b2769f7dcb7b9478cdf5f109322eb2f0a83163469b72d8121fe8e01df227563cbd941a1f63961e2a010c6322a812a014279583a0066492a17d1a15c07024f6b8b1561041b3db608ec1c7475ce4d086e60523e85d7a955841558a35535d6e52cb166669a69049646b94d2fdb901ffbb1446a2d7f58696bce000ccbbb6942b0ad6480d006602c228218d3a2f0bc6a5d5c73acda9fc4b0b496a7d527050f8d77b71641f030368e9494bfe8e9dbf6dd4040426da1b0423739b40450273f4a8d551637e5fe3ca8d54fc476e6278e75cebf4a4ac3ac8ef3807041c4b274568797c8caaffae7df08c6c158d92411db981ea02cc212064c3ad6a9cd1cc02087acce5ceb8d7cddaec429d199c3d90670288be6e88e6826d1f6c417df5f35139c71a62c0267adfce7cb3aa49e827165b28c8cc157a691cb52ef103d8d7ca0ebfc628904d0b2d365dd3401b620d6d93cef670e2fb90ced33fc1486a0e70aeb6fef62ce5a0dfb51fcec12164e4024b0c49ce8b65e01356d80a9571af8f69a9c7256ed549d699a31ceff21c13c482ee0c5759f96125b0099495feacf22a001b2c887b107a9ca9e84de461fbc6e143566de56c710156de5b5f4b7ffe29c0124c1fbb6a792110284c9ad713d89bbb5ed444d09408c65a9f74c5f6c4fd281d31738db82913dcd1ac1fccbba24dc933fb9a8e6c4e426fb98590d7da0252c34d866c114526ded1c9d96daea12ecbd8f2a44531dfb72f02c02b267edf0560b1fb06b087bb6ce02b2922d92f81bb7da99b90e4a2dd68aa858759584490be602369dd606a510b6228e3b941421a3006a697b79866cae011d8aa496de4f3bfffdc0a087d01323db849f6237d7ac476d18bb417f26a697388b4c0a78533f3dd763398719c9ee5bf0299786fb8c4143af8c1195103390d4494b735c06b5815c454dba2fcdd27d214c48712609ffe7ad7c082e835d7477de2358fb3c4b5162027f816ca85aa4a46c25105175e77c8e5394db6fb3999c25ce578f066329182b28fd5b68769cb07ebc67e47a537f6cb62bfd18af162113a4df7f8908e0ce953b6a9637c850f24de0fda239c25fe4352f3b381fc270384d51d1af1ef0dea8501f98ea71f26de97e98799c4b2a316a216bd4af4861ad83fa4570532029564581bb35ac543355ba3aa246b59c6a5d9a91b938f3c4f0ad6d6f567fffdb6fb78402ded5fdc544a0f5fd87e4677326ca003dffdba8fd390487a6469991506a18f376b12db5be3bdcf4a4bc2f5b34dd8f7066bec3b40a6502f414eac3cd1ed43431b033757e86cca57691b0e1f7eec68fad9423f8b0620aa94798546bc878cf29d74ae74af5244ed9b97107fb83abe25dc160ec05cda1c50b76db981aca781868a7913a3e7c98aa5a3ea8bc81270e6f565138dba33c9c7e887641fd3dafd458b9c64600151038db05725a976950307c662c2eb668e3067f943b9bfd805307729b39ed77309fd5e67ecfc56cc231057a6628fb6f528fb969378529dea2c58fa0b4d791ca591ced733e8cf93a8e19907538f45c2618612648d3e1a78492afc154b7bb0285aea97d618e74a74530b492c66c6bd31557cdc55476b3f163196ec8e87f110834ffb8c232dc77cbf2f7b490f520b3b328af44fecca860a19bee9af6a5f69f63487d7dc6b7bf1233c4735522fedd475f905f044a0a7f658a7a378958cca56a163a343a010f4b65d1bc38cd57d08e0f887293fbc106ad7b15aacca6d51558cb8478b119acde04a19b007802a8e9578121304622aee7858d1794ade88973d6fdf981ebabc98fc417d765c075bf8f76b9781dff7c60071878027c057aade8eaf820186035499e9e5fbf8791e6fc0a0e49047af312a8b1b1e80628dc4db4f45842e19096a54733d28028203aa668bd8655631a01c000c956c32ff10572fdbc95b2f26d09fca84c093ddaa122ad93598c03de9273873ece5096648a6251a27e993ec7b6ea4f2181c09bb06852799323f3e993a4b7461439f702cb9df183fcaab11b8bd2ad391924d5e6ef261fea01c91cdc10d9345c290dfc3c040318d11419563d407c8365fef3dd31d8f9d7d338f5f2cd00433d1d06547ae9c650d14d9a7e5b683f0458e7a909d90d4a57201ca5b8753c13bda3591f9cadffd0987d1c7fca1c5a0cec1e13c612d0390d7c0e2f422c97cda29959cd7b714fbeaa5da7b0bcf911c2744a9a25c7d49e93342f61e10a33fda80fadfed2668bfab8767ce80480093b4845cd5ec83082e8f9ad63236743e72842d1a3226640a549c2c1d002a176e2427b2ae612dc272e3d9fa80a904eb8f93f494a52706346c08b06c9d1d8a61a9c7112c0933910882f0da6eea3c389f4faeea51c03394eab4cc02056d704b8839afd76f0839be5ffdc873b88f9a755be6f0229b0843839a25d63c18da89be1c5da930a101913ffba01fd00f2a447d5f58ae8f21554feb47f42d40c32158219dbbb1e5e96ab3dc26cd6a435525c265b65b0bf48d68e9b3c9178a509833aa47c23e63aa9a6f74bef1e4ef863556b11c014b85ac7c157f1e64531b676eb28c68e82e0089d2bd798213c836cc354d4883dc0b607456ab7f894f0a1f73356fc05a5c97a9cf3b231890c39105b751f6fc54063a08f3d3a264ab90cb7de1d0e2f6acb92302cc9b4bb512e720e6ff8343004028ad223a70d395c5f974d3ddd380f798e80edb4b13555c3be0cb1a500e19a8b1b05f9cae85d7eed756ebcfa57828f1054296ffa48d426ceb83c69ebe1b983421f42316ca6f50bf8df0f4d4aeef85005638a304d88764ec26366a1437867c89800aee55ba428e0c07542e123d748c795560153007f768b20bb057fc2643b7db992c3b9cbfd88657b17fbe8a783da2050affb518cab4226cdf28fa97f48651bd142932835f061f8642e79d1f7ae8ff8665aa85aba24a850b2042dd37e61e665a451f17cbe9d57ed5ecb1dce9ec34c576762929dbf46964de7c44c9f6b10fcf52b48649ded2d491bbe4bb6ad578cd31cbdea6df8342dbb7f33857617250b014fc6d887a448f7a5cef25ff69dbc94733bfac1d5f8593af7fbe4759d23ba8da1f405ba179c96d8ae49eb760446d842f0d61ab08508a3e31e4953e13a1b197d00644303e8a9513199583709f41757740a0dff347c3859d3a4ec391481af349019f75ccfdba111a4c338fd8283733115ce7c114111abb157c1a5a84f7b39779a54f000827a250bff00dbe9cb7162d81f6c942b719f9eb973ac7d4652b911622396c517c6e9c7e5f839c049855567fb53ae84579388e3c87af420272558c686209fd92fc339a553f873995932d9dce52f5e60cc0889c3283f94a0d2d5d011154d9935e1dfbb7e747b393bdd8a84f601adca56d4ac5e8e6f28100a7d5522de50dbf1bd7f5604fdfa660a58dc681b4b230abbc4f594a972a73456de1b7f48975cd03313a80074f5d6701db61860f33dfcc9f06e313e8881c385a50de28ced014daf702f01c45368419b73383008b9191293f59fa87a1ceb0cee64d486842ed7c289dca3b3f2e261df644de0f739079ba5ea195468f0693a995dae8e0f7e4611196824f329fc803bc3588d7d941d1fe86a3ecdb3eb8f6d4066ecee371f9a14e5bd46a40b53e87325bab7255099b4fe7f7b09eddaefc6582feecb8dec02b5e58d735a927d052e263027fa879fceb852b9e08db4636c7152f272a43ae4037c50210e90caa3e008fbfa98ed42cd425259096fc06644edab9b768779dcd892747f18a9cecc6aa31dc27d3f12a540447653edd5bed68f50da71b7992434b74d0209746f93ea429691fcb99da0ca32df822ec4332f7eb4bbe6d2111ac84a55932b3e173e388ab96a5a8a17c7d8b8131cf566e0335b0c4a37c5ae24e73c81146031295b69ce3032fa8e4c7616558179cc561f14864190d458cb82370749d595966047de698256126a4ca23a6bbd39201b8649fbcd387474ecf5b9f732e65938fe393a2a1269b06b9b49c5d8882d70776fc823da5842d812be2a65d2f5ee0d64f8258e761e6f2d4c1a825963b367d70b9e81b46fc25e4804fe9decf3aa0e47f1eba573a24f0e4f7b8cf8a886c7ef4175bd317709feeabe46df9719e7d7a4bc2c1bbc15a58b5332ea558e160f5d3b7aa84fe54ea7a24bd8e394ab794e72db7946780272556be33b7f32a0ee651c013d8ed3b5d75bc1c4826dc94f9e3711c53be3bcdf361329544f2993a189b6431a6dda8b81cb1b7c3a69ba96c1ac84a55100c4985cc85ce272e8eb5a57c563a7ad782664d561c3400e779317122d425c9c4193414bebd4d6ad31ba916448f65e9a7ca1f856dacf9d88b695410c8491a759d2c0304e8368b39cf502d0abf7233b1ba9671bc422ce182833ead52597481ea3ea26a2cc35e12a8c83bbb263202d5e9e7a5bb8ab2a5a844d596c24fdac423981fc4dc4291944ae476d59c9a31a05f79f0fe4b39f145c4fba857fa553a17fb2d5f941d5dac8dd3dd07e48eabfe5b98f3c41e7a84986dc3a7a3bb0ed2d4232cffbc50f167d58fb52fa3b357e76430d7f0f1aff6e2d51ea8f5b98bb66c5b5687821f33b9b6c2e407ad0e8140c4d5d4f1739167f8d63284caf4a43688473cd97bab401218fbb2ea13be81bbc05a81042a02eb1843bd46ca535e778e207c79ce3fe511a55d21bf20b24bcdfb43fa62b3469fae2ebd61e362d5d663fb2c08cbde5df9d8cc1faaf6370b5328ffccde873849299d61477a9fcf7c5c96fbc51b24c0f2cc6e39a46f4a9c1f5214f2cd06db62a37e2c6f477ceac0b92eb882d7bcd1fc3721a89745473eb45eee9a4fe6ab323d28f4db957d0c5dc1ed25ebc194f744a2cfe98474868e6acf5641ad454b3626ac2a27a9db416eb1763935eaa5612e43c49e8959ebcf5133863e98c19618e936f33952699fc224f41a4e259b40228047ed6a17cf4f3a42dc76df1346acb0986984f1692b5ed28f0825a23094ddbb8c1750e9035b1df2166f142e7f179473098ed73acb9ffb8be530542f31de88d6677f805cc30ccc0b60787e61f9d195ceacf07a35d979f9c3521f23671f378a6cd58ed2b0620ff7e2def5025f878d70008744c6a53caf325293353747c94c5ddf12e3a42686f97adc4db62b5e2e91132345963b7292fc9d13b484f75d8da000000000000000000000000000000000a13171d2127", ++ "hash": "0xc7903f14f8aa0ffc09cdfcd433e20f07ceaa63ab9b56b84ba41fc8dab7204952", ++ "intrinsicGasEstimate": "180176" ++} +diff --git a/ethereum/core/src/test/resources/aere/pqtx/valid-ml-dsa-3.json b/ethereum/core/src/test/resources/aere/pqtx/valid-ml-dsa-3.json +new file mode 100755 +index 000000000..5ccfbf6e4 +--- /dev/null ++++ b/ethereum/core/src/test/resources/aere/pqtx/valid-ml-dsa-3.json +@@ -0,0 +1,28 @@ ++{ ++ "id": "valid-ml-dsa-3", ++ "expect": "valid", ++ "alg": 3, ++ "chainId": 28001, ++ "publicKey": "0xfd3c43f5fc7807747a73e1d8e78cd9e611ad6bbba9b264534594174b49082aeab827927b335526cc50c2f0368fc666efb79c34af8ee90427d1718736acaad47f5959b9b26457619c07c2dda553d55a7498e30a37c92909f6b37c246dca4e8fd24d4cccd811396d276e5d11f1e8d14bb9eb4d5ec49798202422edd57ea4d8c0e8c6388e9c93a0ff2e52f7ca2303e40300c50c926a7da22919be462e2dcbdb3ec235b14290a282ee76a04f4bf7c5e2ec5c8fc0aaac72ce4f8fcf0d42b9fc3c4049ab6dfdfa12623d89c0d96434a8e13cb6f79ad6f4a8102f9a0bfe582d967372d8928ff9d7d7393b22ec1386ee91dd196eb9c349a6a12f7504e66a096eec0a8ae98e62275f6b84fb40a81a390c6ef0102c8fd2c11d303745b8fc49e638e6f3004961724a977a18d81efc67e37d0d44558e28cf9015dfa08eff82004c4a53dfa40c2451fa03a987f70c855dbee37e36480863044165e6d46d4c43278091317beab6f301d61c8f54f94ee88fa358c9aed6e55b80b4196e79a3d7251ede25c4baef8129882a69789df3a8262941199ded7bd381de00c257199c18e9086d9ce3f72c6d95c0a4d109a3a15474805f0dc06649a16a7805e8d74bd1af7658f5f9ef726d560595ff9f072d4cfaaf61283087843b904b0f5670ca551079c759d4a2839a71505e007cb5d49b0437b87a080fab5e2efdedd1d01f0f1ab72f0e6129a2525fc6e9829b56ce9d9f287a5bd80be8b1556e4ca82eafc60ec22315448768a72e45d4d956c16ee48a0658e307f81ee0f695f1b1b747d1d533d8da720f088b64608b1edb2d4d27247b2cd35e3d94e1256a3c5c944bbafe07b55ca74df4ec0b869733ddb466717a16e98af4ec310d046c90046c024fb33fbb2ffbb105d41bdd0e0af27b1239aa4d92b6ef8d727e51372fefc3b5c8bc6eb585a0ae2dd9970009555c2c2ced5e5a1e97c659565b6139dc4f3bae788e21f9658affa8fdb6f4733f7fa83fefab91a030f8ec976b8263c17abcef46dc22456d5347eea7c30dd25bdc3caad7cb2010f06573d56b6e5d28e6e847d8df8f18d6a2655ea66fe3c8e7e4ef192b5a8aeb35f8469277657b77155142c7b1653d5eb9cf9c4d69bbc5746e0c1876e330f9016a2f5343ec60b2f35ea380169a2bdf102888e6aae7e797d0bce73834f418018e8e55492da8236a542a999d9eea55bd52ee2e59feb0329d716c78c0bfcd79dc27cd9639399d2fd99d2fc9c4900ad60bdb8221501038ec4baadd2ac4830b2bfe1e20804c818c8e089661e7d842bc4e566fa0bc2d7968a44f8df0d4e88e494fc7bebdb695d3a8edf6a2703817b0f8c1a55569ac4a8ff0931137f075797e9744135a3fec6c767af26d4592a9022271a4052b1f1f24f072bd5ceb1d19e02b0bbf31251a7bb8b723bd3da02f59ca1c0c91bc10896970a1e0f8e0fd5b755da58d70be8935f65661629a9f833b35810113ce80125f8ed7a031cfd5eb1475b597856b4ce7629a9429d840a3457bbd70c05c73c6ce84dd59f917a24d06ca8743225ab8c266e209fbadf57cdb587df96eedf54c04fb3f83ec0dddd1d8bf9051c6de4db3aa099f1779f3f30f61f432e5c8f6b7ac4ee6c6dd6fce667556df155ddb2002735fb7addd8acf74863ccfe04a3088317634eb47e56b18c79f31318ac5e448a9094b5c2c70b44dbbbff7c235cef4701d504723b032726d009c6d7f527e645a23fc1ab478e6e2699211694d575bd436d778e7ae53868c63dfb78f591d82b979585cec8bbcde61f375e783e97e8f787fb392fa4f0bc3e9010aa9ec3cf6471bb16355c3ed3fdf2ee5b95ade86371d60fb0699a851770a212d0e0eeb3f20df78ab180343fd79a9d04d8dc56d9fe3d820b27d98d12275a40d270f4181aa1a18305c8d3469a954b2e85405a394684cc234702a6cb01c04ccc941a4392cf64ba31bea700d339152fce6abb0dbe84823b8d3b18258a29414fd52c91ccbc198e101287b79d8bd0e5f4b05a14a07055e66ea37d781934840a204638bffda400113bb1344126fe1c2ac1e57bb861c3d35349b1fba0c11e12921262ae52fb2e0bee444f8197e2817b9356e10af67e794a7967055936bfcb51f4be6fc5602a8df97ddcd73afe83c02f00d4f1d8302bd2e826d4c50fd3ded9a74debd0e807b694e912a8a3a800f5b983e171898874b77ea305ede4abdf0db5ca84a8a60b184374c08066537b08d42a609b97039ff8905bccc4454188b051c5d1e574e1e8b74cc5cbded064a5374c55f17002ee9b17447fdc324a12e3f2f414b491cccf20c9f98820461f4c7d61355a30f1a6cbb2fe7403b90587298fcc9500519451e44225c750b8b32a649ea288eda389e596390a5386652dea5d0e096ee270bcab9da0c3c4ba464f44172732a748e339cbfc8f0c1ee3187225e2fb29e67ffbd84df8acf759c817d628e03a245e9ae0cf00939fa12964e8dc124659eea8798b097ba52480532fa11194e4623ff63f6cc1d17c7080bb2c1459fe8cc9b197b89ba91986b7bfa61e514bf5c60f981e63baffd9fce44af87ad73fba197e46790fc03f8a46aa6d7590f47e08daedd7f564685208ee87da84b04b11cdc0c020bb26b0abdad3c1340649130e26af9c7651533313301e8b517a84c807d86a4c0cff68566d34da4111d67bdc77ef5f49ec7d83af24a143bceaee020acce02499c725ebc341a86d91c7f63dd962ffcc09fc0b5673873ee55cea4bba69b5058e4b364fb3802832d5118774790dd12eed78330412dc877cc034309a7e292ebee955a431fd46faf7884cf3bd5ab03d02b8784af2a84bb907ba5f021fc332f638a2d4b6451ea0a2cdee6ff1621d698eadfc99d6f405003f300e321a9e80ed3b577a899fa7dd891b53cab8acaa9fd3ce02640eaa75646f134c190817675b9e4a688d0668528fa3b378749bb6b1a0bd16873e89dfc927dcf568fc3cf94ac49d289ba1fb9c58a74574c8fbac321db256bf919e9432d6a8ae9a0dae900901f436b13d0c2b753a5eb6fcef03301d7faa15ff32ec0730335b13394bff66ea83a7043f9e1df5962671ff2d2759db234a4a4fa47fd3a9359bb14ef5b5fc398836d42afcc5d5f252b1b061afc6a0815b3a7586d25eed5ee52c56d2389b8f2438649ee652b431d3cc4262c6e5ea1c0ccec21aa6123e88b4bc933098576bd78cf5bcbbc278ee682029e05e91aac2043ad3ec1d3d97f6ebc6bd5faffa5bc71328cd4351bcb9a0d8d1ea9253b97b08c8c53b20e29703568d5d7dfa2bfa6aeb3a93043e4123116a125311695a11c8538658210971d7763ed7870c1309893909e306df22fe968612531e58a91d19dfe2c658ade19c8e0900c779af8ec333d19552e42cdedbe13ff85d99b10baf8daf70951290db56bc7ea049e6b468b6546a7f3ae77c2e14770eed1ea345f0d139336af4eee686ac2e73bd4262cb802e0173fb872a77cd2c0f3421c497b0260be79ae29d04d9a9da9d7bc101ae3ec9dace1133332f05b7b82aaee577c06c0d2a8eaa8a96db8b899cb3240cc4dca4f8eab513ad5b3529063b5df3664dde0db1e0ae5c29e69f011076fc7edc4261d043f38f1c68645466c9a5e685a07393b7e2f8f74db86742ba5ac089b45a42df3f943842747674c1653aa2a8ef282f64e7ec2ce1fada994288a5f9b95da4bc083ea008308", ++ "seed": "0xa8af8968099758df7640e2c9f1713fe31d70766e2c2b18bc2ae82f5b1e1e8fa5", ++ "tx": { ++ "alg": 3, ++ "publicKey": "0xfd3c43f5fc7807747a73e1d8e78cd9e611ad6bbba9b264534594174b49082aeab827927b335526cc50c2f0368fc666efb79c34af8ee90427d1718736acaad47f5959b9b26457619c07c2dda553d55a7498e30a37c92909f6b37c246dca4e8fd24d4cccd811396d276e5d11f1e8d14bb9eb4d5ec49798202422edd57ea4d8c0e8c6388e9c93a0ff2e52f7ca2303e40300c50c926a7da22919be462e2dcbdb3ec235b14290a282ee76a04f4bf7c5e2ec5c8fc0aaac72ce4f8fcf0d42b9fc3c4049ab6dfdfa12623d89c0d96434a8e13cb6f79ad6f4a8102f9a0bfe582d967372d8928ff9d7d7393b22ec1386ee91dd196eb9c349a6a12f7504e66a096eec0a8ae98e62275f6b84fb40a81a390c6ef0102c8fd2c11d303745b8fc49e638e6f3004961724a977a18d81efc67e37d0d44558e28cf9015dfa08eff82004c4a53dfa40c2451fa03a987f70c855dbee37e36480863044165e6d46d4c43278091317beab6f301d61c8f54f94ee88fa358c9aed6e55b80b4196e79a3d7251ede25c4baef8129882a69789df3a8262941199ded7bd381de00c257199c18e9086d9ce3f72c6d95c0a4d109a3a15474805f0dc06649a16a7805e8d74bd1af7658f5f9ef726d560595ff9f072d4cfaaf61283087843b904b0f5670ca551079c759d4a2839a71505e007cb5d49b0437b87a080fab5e2efdedd1d01f0f1ab72f0e6129a2525fc6e9829b56ce9d9f287a5bd80be8b1556e4ca82eafc60ec22315448768a72e45d4d956c16ee48a0658e307f81ee0f695f1b1b747d1d533d8da720f088b64608b1edb2d4d27247b2cd35e3d94e1256a3c5c944bbafe07b55ca74df4ec0b869733ddb466717a16e98af4ec310d046c90046c024fb33fbb2ffbb105d41bdd0e0af27b1239aa4d92b6ef8d727e51372fefc3b5c8bc6eb585a0ae2dd9970009555c2c2ced5e5a1e97c659565b6139dc4f3bae788e21f9658affa8fdb6f4733f7fa83fefab91a030f8ec976b8263c17abcef46dc22456d5347eea7c30dd25bdc3caad7cb2010f06573d56b6e5d28e6e847d8df8f18d6a2655ea66fe3c8e7e4ef192b5a8aeb35f8469277657b77155142c7b1653d5eb9cf9c4d69bbc5746e0c1876e330f9016a2f5343ec60b2f35ea380169a2bdf102888e6aae7e797d0bce73834f418018e8e55492da8236a542a999d9eea55bd52ee2e59feb0329d716c78c0bfcd79dc27cd9639399d2fd99d2fc9c4900ad60bdb8221501038ec4baadd2ac4830b2bfe1e20804c818c8e089661e7d842bc4e566fa0bc2d7968a44f8df0d4e88e494fc7bebdb695d3a8edf6a2703817b0f8c1a55569ac4a8ff0931137f075797e9744135a3fec6c767af26d4592a9022271a4052b1f1f24f072bd5ceb1d19e02b0bbf31251a7bb8b723bd3da02f59ca1c0c91bc10896970a1e0f8e0fd5b755da58d70be8935f65661629a9f833b35810113ce80125f8ed7a031cfd5eb1475b597856b4ce7629a9429d840a3457bbd70c05c73c6ce84dd59f917a24d06ca8743225ab8c266e209fbadf57cdb587df96eedf54c04fb3f83ec0dddd1d8bf9051c6de4db3aa099f1779f3f30f61f432e5c8f6b7ac4ee6c6dd6fce667556df155ddb2002735fb7addd8acf74863ccfe04a3088317634eb47e56b18c79f31318ac5e448a9094b5c2c70b44dbbbff7c235cef4701d504723b032726d009c6d7f527e645a23fc1ab478e6e2699211694d575bd436d778e7ae53868c63dfb78f591d82b979585cec8bbcde61f375e783e97e8f787fb392fa4f0bc3e9010aa9ec3cf6471bb16355c3ed3fdf2ee5b95ade86371d60fb0699a851770a212d0e0eeb3f20df78ab180343fd79a9d04d8dc56d9fe3d820b27d98d12275a40d270f4181aa1a18305c8d3469a954b2e85405a394684cc234702a6cb01c04ccc941a4392cf64ba31bea700d339152fce6abb0dbe84823b8d3b18258a29414fd52c91ccbc198e101287b79d8bd0e5f4b05a14a07055e66ea37d781934840a204638bffda400113bb1344126fe1c2ac1e57bb861c3d35349b1fba0c11e12921262ae52fb2e0bee444f8197e2817b9356e10af67e794a7967055936bfcb51f4be6fc5602a8df97ddcd73afe83c02f00d4f1d8302bd2e826d4c50fd3ded9a74debd0e807b694e912a8a3a800f5b983e171898874b77ea305ede4abdf0db5ca84a8a60b184374c08066537b08d42a609b97039ff8905bccc4454188b051c5d1e574e1e8b74cc5cbded064a5374c55f17002ee9b17447fdc324a12e3f2f414b491cccf20c9f98820461f4c7d61355a30f1a6cbb2fe7403b90587298fcc9500519451e44225c750b8b32a649ea288eda389e596390a5386652dea5d0e096ee270bcab9da0c3c4ba464f44172732a748e339cbfc8f0c1ee3187225e2fb29e67ffbd84df8acf759c817d628e03a245e9ae0cf00939fa12964e8dc124659eea8798b097ba52480532fa11194e4623ff63f6cc1d17c7080bb2c1459fe8cc9b197b89ba91986b7bfa61e514bf5c60f981e63baffd9fce44af87ad73fba197e46790fc03f8a46aa6d7590f47e08daedd7f564685208ee87da84b04b11cdc0c020bb26b0abdad3c1340649130e26af9c7651533313301e8b517a84c807d86a4c0cff68566d34da4111d67bdc77ef5f49ec7d83af24a143bceaee020acce02499c725ebc341a86d91c7f63dd962ffcc09fc0b5673873ee55cea4bba69b5058e4b364fb3802832d5118774790dd12eed78330412dc877cc034309a7e292ebee955a431fd46faf7884cf3bd5ab03d02b8784af2a84bb907ba5f021fc332f638a2d4b6451ea0a2cdee6ff1621d698eadfc99d6f405003f300e321a9e80ed3b577a899fa7dd891b53cab8acaa9fd3ce02640eaa75646f134c190817675b9e4a688d0668528fa3b378749bb6b1a0bd16873e89dfc927dcf568fc3cf94ac49d289ba1fb9c58a74574c8fbac321db256bf919e9432d6a8ae9a0dae900901f436b13d0c2b753a5eb6fcef03301d7faa15ff32ec0730335b13394bff66ea83a7043f9e1df5962671ff2d2759db234a4a4fa47fd3a9359bb14ef5b5fc398836d42afcc5d5f252b1b061afc6a0815b3a7586d25eed5ee52c56d2389b8f2438649ee652b431d3cc4262c6e5ea1c0ccec21aa6123e88b4bc933098576bd78cf5bcbbc278ee682029e05e91aac2043ad3ec1d3d97f6ebc6bd5faffa5bc71328cd4351bcb9a0d8d1ea9253b97b08c8c53b20e29703568d5d7dfa2bfa6aeb3a93043e4123116a125311695a11c8538658210971d7763ed7870c1309893909e306df22fe968612531e58a91d19dfe2c658ade19c8e0900c779af8ec333d19552e42cdedbe13ff85d99b10baf8daf70951290db56bc7ea049e6b468b6546a7f3ae77c2e14770eed1ea345f0d139336af4eee686ac2e73bd4262cb802e0173fb872a77cd2c0f3421c497b0260be79ae29d04d9a9da9d7bc101ae3ec9dace1133332f05b7b82aaee577c06c0d2a8eaa8a96db8b899cb3240cc4dca4f8eab513ad5b3529063b5df3664dde0db1e0ae5c29e69f011076fc7edc4261d043f38f1c68645466c9a5e685a07393b7e2f8f74db86742ba5ac089b45a42df3f943842747674c1653aa2a8ef282f64e7ec2ce1fada994288a5f9b95da4bc083ea008308", ++ "chainId": 28001, ++ "nonce": 0, ++ "maxPriorityFeePerGas": "1000000000", ++ "maxFeePerGas": "2000000000", ++ "to": "0x0000000000000000000000000000000000000001", ++ "value": "1", ++ "data": "0x", ++ "accessList": [] ++ }, ++ "algName": "ML-DSA-87", ++ "sender": "0xaee69521a942a58325bd694f2cc3d64752465d4a", ++ "signingPreimage": "0x50f90a4e826d6180843b9aca00847735940083045f289400000000000000000000000000000000000000010180c003b90a20fd3c43f5fc7807747a73e1d8e78cd9e611ad6bbba9b264534594174b49082aeab827927b335526cc50c2f0368fc666efb79c34af8ee90427d1718736acaad47f5959b9b26457619c07c2dda553d55a7498e30a37c92909f6b37c246dca4e8fd24d4cccd811396d276e5d11f1e8d14bb9eb4d5ec49798202422edd57ea4d8c0e8c6388e9c93a0ff2e52f7ca2303e40300c50c926a7da22919be462e2dcbdb3ec235b14290a282ee76a04f4bf7c5e2ec5c8fc0aaac72ce4f8fcf0d42b9fc3c4049ab6dfdfa12623d89c0d96434a8e13cb6f79ad6f4a8102f9a0bfe582d967372d8928ff9d7d7393b22ec1386ee91dd196eb9c349a6a12f7504e66a096eec0a8ae98e62275f6b84fb40a81a390c6ef0102c8fd2c11d303745b8fc49e638e6f3004961724a977a18d81efc67e37d0d44558e28cf9015dfa08eff82004c4a53dfa40c2451fa03a987f70c855dbee37e36480863044165e6d46d4c43278091317beab6f301d61c8f54f94ee88fa358c9aed6e55b80b4196e79a3d7251ede25c4baef8129882a69789df3a8262941199ded7bd381de00c257199c18e9086d9ce3f72c6d95c0a4d109a3a15474805f0dc06649a16a7805e8d74bd1af7658f5f9ef726d560595ff9f072d4cfaaf61283087843b904b0f5670ca551079c759d4a2839a71505e007cb5d49b0437b87a080fab5e2efdedd1d01f0f1ab72f0e6129a2525fc6e9829b56ce9d9f287a5bd80be8b1556e4ca82eafc60ec22315448768a72e45d4d956c16ee48a0658e307f81ee0f695f1b1b747d1d533d8da720f088b64608b1edb2d4d27247b2cd35e3d94e1256a3c5c944bbafe07b55ca74df4ec0b869733ddb466717a16e98af4ec310d046c90046c024fb33fbb2ffbb105d41bdd0e0af27b1239aa4d92b6ef8d727e51372fefc3b5c8bc6eb585a0ae2dd9970009555c2c2ced5e5a1e97c659565b6139dc4f3bae788e21f9658affa8fdb6f4733f7fa83fefab91a030f8ec976b8263c17abcef46dc22456d5347eea7c30dd25bdc3caad7cb2010f06573d56b6e5d28e6e847d8df8f18d6a2655ea66fe3c8e7e4ef192b5a8aeb35f8469277657b77155142c7b1653d5eb9cf9c4d69bbc5746e0c1876e330f9016a2f5343ec60b2f35ea380169a2bdf102888e6aae7e797d0bce73834f418018e8e55492da8236a542a999d9eea55bd52ee2e59feb0329d716c78c0bfcd79dc27cd9639399d2fd99d2fc9c4900ad60bdb8221501038ec4baadd2ac4830b2bfe1e20804c818c8e089661e7d842bc4e566fa0bc2d7968a44f8df0d4e88e494fc7bebdb695d3a8edf6a2703817b0f8c1a55569ac4a8ff0931137f075797e9744135a3fec6c767af26d4592a9022271a4052b1f1f24f072bd5ceb1d19e02b0bbf31251a7bb8b723bd3da02f59ca1c0c91bc10896970a1e0f8e0fd5b755da58d70be8935f65661629a9f833b35810113ce80125f8ed7a031cfd5eb1475b597856b4ce7629a9429d840a3457bbd70c05c73c6ce84dd59f917a24d06ca8743225ab8c266e209fbadf57cdb587df96eedf54c04fb3f83ec0dddd1d8bf9051c6de4db3aa099f1779f3f30f61f432e5c8f6b7ac4ee6c6dd6fce667556df155ddb2002735fb7addd8acf74863ccfe04a3088317634eb47e56b18c79f31318ac5e448a9094b5c2c70b44dbbbff7c235cef4701d504723b032726d009c6d7f527e645a23fc1ab478e6e2699211694d575bd436d778e7ae53868c63dfb78f591d82b979585cec8bbcde61f375e783e97e8f787fb392fa4f0bc3e9010aa9ec3cf6471bb16355c3ed3fdf2ee5b95ade86371d60fb0699a851770a212d0e0eeb3f20df78ab180343fd79a9d04d8dc56d9fe3d820b27d98d12275a40d270f4181aa1a18305c8d3469a954b2e85405a394684cc234702a6cb01c04ccc941a4392cf64ba31bea700d339152fce6abb0dbe84823b8d3b18258a29414fd52c91ccbc198e101287b79d8bd0e5f4b05a14a07055e66ea37d781934840a204638bffda400113bb1344126fe1c2ac1e57bb861c3d35349b1fba0c11e12921262ae52fb2e0bee444f8197e2817b9356e10af67e794a7967055936bfcb51f4be6fc5602a8df97ddcd73afe83c02f00d4f1d8302bd2e826d4c50fd3ded9a74debd0e807b694e912a8a3a800f5b983e171898874b77ea305ede4abdf0db5ca84a8a60b184374c08066537b08d42a609b97039ff8905bccc4454188b051c5d1e574e1e8b74cc5cbded064a5374c55f17002ee9b17447fdc324a12e3f2f414b491cccf20c9f98820461f4c7d61355a30f1a6cbb2fe7403b90587298fcc9500519451e44225c750b8b32a649ea288eda389e596390a5386652dea5d0e096ee270bcab9da0c3c4ba464f44172732a748e339cbfc8f0c1ee3187225e2fb29e67ffbd84df8acf759c817d628e03a245e9ae0cf00939fa12964e8dc124659eea8798b097ba52480532fa11194e4623ff63f6cc1d17c7080bb2c1459fe8cc9b197b89ba91986b7bfa61e514bf5c60f981e63baffd9fce44af87ad73fba197e46790fc03f8a46aa6d7590f47e08daedd7f564685208ee87da84b04b11cdc0c020bb26b0abdad3c1340649130e26af9c7651533313301e8b517a84c807d86a4c0cff68566d34da4111d67bdc77ef5f49ec7d83af24a143bceaee020acce02499c725ebc341a86d91c7f63dd962ffcc09fc0b5673873ee55cea4bba69b5058e4b364fb3802832d5118774790dd12eed78330412dc877cc034309a7e292ebee955a431fd46faf7884cf3bd5ab03d02b8784af2a84bb907ba5f021fc332f638a2d4b6451ea0a2cdee6ff1621d698eadfc99d6f405003f300e321a9e80ed3b577a899fa7dd891b53cab8acaa9fd3ce02640eaa75646f134c190817675b9e4a688d0668528fa3b378749bb6b1a0bd16873e89dfc927dcf568fc3cf94ac49d289ba1fb9c58a74574c8fbac321db256bf919e9432d6a8ae9a0dae900901f436b13d0c2b753a5eb6fcef03301d7faa15ff32ec0730335b13394bff66ea83a7043f9e1df5962671ff2d2759db234a4a4fa47fd3a9359bb14ef5b5fc398836d42afcc5d5f252b1b061afc6a0815b3a7586d25eed5ee52c56d2389b8f2438649ee652b431d3cc4262c6e5ea1c0ccec21aa6123e88b4bc933098576bd78cf5bcbbc278ee682029e05e91aac2043ad3ec1d3d97f6ebc6bd5faffa5bc71328cd4351bcb9a0d8d1ea9253b97b08c8c53b20e29703568d5d7dfa2bfa6aeb3a93043e4123116a125311695a11c8538658210971d7763ed7870c1309893909e306df22fe968612531e58a91d19dfe2c658ade19c8e0900c779af8ec333d19552e42cdedbe13ff85d99b10baf8daf70951290db56bc7ea049e6b468b6546a7f3ae77c2e14770eed1ea345f0d139336af4eee686ac2e73bd4262cb802e0173fb872a77cd2c0f3421c497b0260be79ae29d04d9a9da9d7bc101ae3ec9dace1133332f05b7b82aaee577c06c0d2a8eaa8a96db8b899cb3240cc4dca4f8eab513ad5b3529063b5df3664dde0db1e0ae5c29e69f011076fc7edc4261d043f38f1c68645466c9a5e685a07393b7e2f8f74db86742ba5ac089b45a42df3f943842747674c1653aa2a8ef282f64e7ec2ce1fada994288a5f9b95da4bc083ea008308", ++ "signingHash": "0x3ef7803edb46bad878a1a585571475b75bba9b66e2407b73c6d5794ec507790d", ++ "signature": "0xc74a08868ea0fdad9fbb1b485b793bce6e4fa945668b2351232a7c8a084fb2d87a5c83e7dced6bde3743617a7c4babcbf9381694b64d7e0feeb6615440e927a63293c6b695900b1e7afad1ae612c324a88e6761c3b6fdc41d18f514be1ba3a1ca53fdbcf8c19f95bcd60223abe52d07fd7c4114dad9f347cf122f6241429da7e598959c22692910323a0a699595398f518c58deea7159920d89e3e65702bc2cabea7e9ca0bbb7002fb7be5243bba6a08a12238ead443ff4394510d25eb279885d9112c44529f294c5a2bf493a74ba8f4e38744e632680b3ce055a897d2a5468ef52e6e70dc5af55a9fb83739f6cfc6994871662b8263f2e07e21a6514624909369e0138755165dd2af5a791da63c0604b68ff160fad82a578a06e76f62bb7e951b81d8fde2dade6c3a314eec5913151c16a93a5abd5ceb59ad96197a349721ab123b42f799dbde36d7965e0e5dfcf43d8d1ae13f4a00afd8dde36137d3de87f4e2c0366c18f4b3a449ffd9afc7c742c50456f6e642f7d09e86d5aef224b20ce5d8a2bf164484df8fc87daf69e459fc78c6b974cdb8fb1dccbd5290f560e3114302886e2828ad73ce71b06186254206743076c6af0c3d1facc36695dc28db1d026686b141247e1c7a9323d544b0079023cd2adedc0479ceefc8e0c34782603d0c27d18768eb5485a85bc385ffdfd25f735688a3b9a0c6f532b2a7989c751804de0163582293a17f635fdde14e9067f12929b079f89a53dfc87b986f89f718320ede83cf4086d0d095b24b66d5e3f1bfdb08b79cd5b25657cf2b20faff1d199c16bb7f631b33970b34c12997637447e4127db1bf580b339807637a86e0248cbcf8e1d30ae6fe053e2f9b7b3d0f9e3f4813af908b633560779012f9e06af00e252bb0df9f0caf7d1e82f8deb2ede21d13f406dd9ea69445d50844c95c921d8d1cbc8afbc5ddcd4157e65cbfbbafe2670e9146a5643db049a8d696f0eae07c3d4099447ef78bdb90fc05d7cc97bf9b9dee8719464ec6b9e6de866f73914a80396dea3900a9f7b43a47e648e20a9504da92c47ac65368813e5453e82020f8861aba2534346e6137e71adb28a5ff080365d6ea26dfc30ef5500d0760749d6e4e0f93aaebed544832308b19f72e92ebb8eaec2492c6ea7972fd359a8b67a1f4053bfb1a3c6fb720b5925a7a827e59b3bd0651b8014c77b25d61db17eb169cc2fa2442c82f68fb42200c59baa468a9fc92f13273ff071124f8b31e2b4b2517ee622d29489997ecb5676fb52e3002371c01004c214757b1f510091c2377a08d058209d18feb9b524a41712b6df4b92f2b14c583d3196749f367b3bbc8ab5e9e2615c511f23ba5fb1bac2ee9436dd8d162bd96f1fb663d2b520ba7e8ddfc2a4d549b63714654cfbb37ee1dca5f723139187ada8f8d5b42ad09ff196b70720d2527b4466d44f33cce486ef2e86311c75ead31a17b16989dc63bacf0d465379d9138df7ea2ebc71f4cc3b6662e24895050e2553dbc160a567d6722964a904d43bf2684206cee3562256e98addf3038e968cf6c09c921e2c89c9c249f6ba7afd20dd27587b4143f704f2738315a91c2d40c467111b4f3bae4f77bae402145e742ba83a527aeffca13ee2290988df882feaf5a3c24b416d59e06cf4f8832a016ce55419b7bd2028545557528a0af23f17942433e13fad089fdb79449bd9d99b0324b1828791249b6789a09cdfb6ec40c0fbaf8b28ea4f72d3df5159bea7a7965b94beb1bb01d87d76ae7ce47fc333fc152038ab4e7ca64a802f96f2e92bcc53ad9cae0242c622c99b2daf005e554cd6ba127991d84841ffe3ba0674eb7930509045dd00ab91a7c6acd2f4a40e7ffc9959929851e40d417f488a29f7bb5af3847a8caa5daa0682aea870ac8b866ccfdc435971e3d862c2f651cc6eb8a33fca313a56ab8228ee9ac7dce14ea3c3fba4f58c70888ae558d8cd632a2723ba54a3d538f4e20936a2ee543e9b2141e06095c72d4ee19908febf7c248c03a78655420da644746e2d508c31ceb0b648800d95dceec72f7576502046e5dce54b5a097f625001bfc92d6e4633b293cdfa63b1fca5de9e26d03ba2f1cf3647dbdad5359ed76bf702f0621c057dea3617af678016aa13360ebb02c6cb2fa0d3f137a1a3c639c989e12383841ab7ca5a84f84c08af4b85416ce7ec3ed91636a79add207bedf9dfb2ff626387f20cb3315502b2b06f361cbf6a51fb507598af41c5a5b9cd95ba1894ca372c5e1851e62a0d3d78dab6f92bd43ba6fcd71230b6cfd843d89268872b75e3c5b471db530b7ab5451dbe63fa0bdaa7d74907cb376643e129febbe6ce10b5dffbf323dc973f097d2081e06aad65e674c5a41bb81b80556fd78691fe6b5bfad47fd1b64db577f27f22f17973cbff3a7c4aa43569bbe6843d773a2f933f472650e88e0ec6643f87dfd37e7fba915c7386a130b8d4f0ff4d0e99a633d1f863f3e8ce4e1ac5e31438d89c3b6d5ace3aa6e14e7073164ea01e853958bece389316023127f9916297a47c5d6ab5513bab874c1ae3bf2850cfab9018cbf50a7febe03ff3e8e4e55ae5632812b6a147edf051b461bfe56f01a6ab571e0553e37a57a3a9811f4fda2e189d79b74215d6fe199595d18c6543c747572f8cdc61a53ce09d9ca96b43a5af5b56b53148dd4fdfc1e22c9958caa783f2d112d8d5fd39e6955df75d35c59a1967fc0d8d3293e39a6cdf82d6846b39a74b1a538372c266b3f12d7ccf464ff7e32fe54342994f51d344302b3b2e11eed2187a741eec99ef0a79a19a854c03d8bd3fcd557b9d8bfb35f0976da327fc7e8d95fb43db8e66be5b373385fc1b68099566c7e88aefe92c7c521d084c15c8d403d87bbc975e8d687e2a51e5ce53191e3db5bdc50f475853cda7cc5a7365c41035aecb61444c2a48492a600b58300b2805be97b4e9e30526e9e62bb3248a17c3809219307ca85351495c943a865359c783cf615a809c739cce0786c6045d9c8071797001434cbae571d42cef415e28ed6adab09ed10e0713dc1412ed80b8d26d549964321559137483c81b19203080b36abcd68e841f4c78b5302da79fd6e555f390322ae67e54c55749491889d79eb2982fc4428086bd1060c4113137ddb4dd7c9b690b802e805999d4f5127f66f6b1d397dc3509504e2196513b5787407b45b928cfc3c437523daa4f876dfe061e351cb3bad99ea205ab276c4f2a5ccee1027104f09adaf55cd282d4e9e44ac2b65f7e423da1cebb0ac70691347e7a7bd0de8bc9fb2e7a08afc0075a0e85902316cd6ec9a5802c51a6d65bf3251ae712ae0f66de6cba1e51efbad53ad7e769f851799caf5fb81ef35b4da814915dcfa67553f1c67158154707d16804be27af2115559e4758b313b9da4a1131718b29a75970b3f0a5e2f2c11a0a1ed47f238c145dc4fe9e8e963d8395c67dfa94a232b8133a0d953b05455f79e0bdff0bc3de9c04d93d97084ab99229622cfe3958ac5fda6757de1696b575f6250b441e6368386751b1d2f6007d254d152fe76578fbbe164af49cda805b9325f7ea47f5e18f3de1ce38006ae1bf027d3e354d6f923d8bc6286aaf9b9a7b9e7a7fbd36a31db0d88166dc68e7eb567d8164bed1e279a8810fc21656621ff7cb42a21ee04e6086924bb62dddd16c3343db69250f7ed95aba953202640bef18dfb6c00f1ee03c670a5cc6c0f3cd7cb006590dddffa6f19d450860e289f85edb4fb0dbbd2f992eebba09cddc1b20a3c98d8739ad1875a514b09b1f23cf406c4c55cc4266e0b8b283fa445849316c30bd19254e9d035756d2773cab7f5fd56d6d3a094a717ccca43251201cfddf80ba1aa0dad34f5a3acf8476b784fd53a1aff1307a7838705112650bd25dfea21b991d801f018a5dae26edb9dd5a0ab940a94b7bbb90fd1f30f9d1dd2ea3aad6d0a2a3c42fd2641233c7d624314c3a54ce616d2ca5683d04f6fc0924f0b667be32f4de8820f9cfab963a287a1c708857ebabed9b5bcc35d539ef9bc170636cd528ba86a620195134cacbc2b8e742376124b67ed8155ab53979e281f97b184c60355b008f15f45c8b7a506b1c1f511e4aaf0bc32fee27b656b01ed6afaa4053ed674a60175b6c89a77d42963111d0940730db02ff67534fe6a296e0e01e2db6b3f0fe9648286a0f3420cd4393163662751deed6457abbb370f83ed92e9da86a0181df13b8ef33e5718ce524a700ba5f2b40f3f973a67c1c412dc251c19e6b7ceb279dc1dfc36a3567e3a98a9d96ef52b0d0734f57bd9edebedd925167f230b1c81ca938e94ceebe43d2573735873a436331a8b76c1169c6a869889cb74e5d33721bbb99ec87f160c025f570f1d11c2e3ae6a52580785d854790139510179b003418dbcee1f4fd4fa39fc134521ffd7f29cee650472638dd04d8331ee439eaa32c57b56c4fd9c9ba87516a8236dc99b8d6e4c2a48e53a6779d5e13bbd73cc7917f0cfb8e231218bf33bc3c213599eb76476558f0e38a3979cdaad6ac6b03bd0ef0aefec634fbe0e1e87bd78123a19b7352326b9def856ecfc02b06622f94e8cb2a1cb6cf10d020f789a7db775063030f15b463f08878219636f2bb94ed11a3ed9689d797a3253809c7f2e0c60672e8476a9e272075c7f808258901e611c4d535a11bfe01daccaec3a26a187ab45a9fd0bb9a7ecf2e9c4aa3177ddaca1449f19f78ebe26be2e25bca012a10649b56899339f5418925648f3253bd716336dcf65e6b4f16d2ae51fed1acbc8335c103f721f46023cc2043f568ddb033aa5f2157e52b89eed4571cb523fcc66ef5cddc90679633c2a46f45f1a86c975db4f6f37f9fcfe54809ccf75305bc9f242f26f30a0ad5f3cd71cbd31ac45ceed2dc8571142a09b7d3091a4838727b9a067927e17b7559076649b405dfd480627881489d52339d4a8943ca396e92b92066e45d525acf7785a944345d25d721d6a354b1a9f3e2afd379d1c341badd0dd913a3471a5b78a81c0b338a2984e115d003b1deec1b44fd19824279d742017248fb4434cf47ad07fc87df86e634b2d8f6e0aa572088678b9a9f767404f06c84ea8cbf65d46effbde7de1f090d3d4b9e7c7c3b2b1dfa9e591efe12ffe0fc82c9eeb7fcd9f25fe438b39c46614438d0be7e1dad11bce8b91396f867531e8c45d8c01119abc2b20be253779b5e651bd80536854e2aabccddc5f4fbfb3497111fe9a8f8a417ab4dd806745c82126524ec325c931ac1f31a3cea19e159e1dbfbfcff761cccabefad38062872fe8a45a14781ba12551befb31c21150a56ccacf9815d8f458b1f591c71ba7b0459e7c09a4eac5c9e5b2984314ecd2f7054ed055ba082204f7ed62b1d513cd0992bc10c28bc745e30b27f6ca28ac84dec6cf0ef73c971ce12b2ddcc0d6264d8f7dfbba0a20f8ff1d7387cd9d9872f4b345b409950d66ababa9852f7f5ee634dad6085666956b97e4c9142dbdce96bf42d1334c61d04ffed5ac8f12a5075ac87a7416fd52579a876c89e22008759a93ebc57279215e178662a064bca65670089ec688532468c07161f56bbafbfa5655768123e713c7aff52852978e9d6004ab3a434d9b5c73f0d90dd1fe742a169a84f0e96a189bfc5265d8c5e2d9e42f5503d04ce01f9a912bf8b9bd8c282a4d40bbeca55d14971d1d305a16eb85d02b4edd8d35baea56156ba12f4e344dc4cb34509fc780ee97d24173a75e49960ec32055710bf8119d005447c6c391365737a0cff68d9f44868b97b2357dc051ed7fcf590b32292624545935e87f25e56b3e4a2b9c39c4936b5be9030b4711f246623264e0370291b675e75e1ce9cb425e5fabcfb55e25f084ea8a1b84870135ad997f7b700876691939cf268204669790a10ddd3d38cd22252226a09fddd395482deb561ee834ba452750d5b8e0de63c77c85a7446dd5bb29df2620f98e07bdac4cb59af8b9dc64d427dab780257e252e8d6793fd1b99f7c3162b4233a418526ab227d40b9deadc943d0f108a38d7a6b075465a44c920c0146338504eadf826cae345e864452e94706c8dceac5cca87469f2183a9c918295c09eef65f747b7a3d7fb3d259c4f531ffe48a5c338717ceb45efb557a47a593fac85cfaa4c263c109d385c01a3193a6ad1be5923d6a53e6c369a2b5e704cd6ac3ac255954c3fca1079895c596596873c1309fdcc5f36fe039d06806dd16cb00bd028edd6b44fb1e9ce4a020362824bef6a59502cce4e1d3d3884d7d865823deb092916a215ee73bdae1d4baf7f294caac7f63a36c2a15a40517438b8cecb93a5da3e11c25ff228ef3e7281360a2ce3ac66e69a2f136a83fbdbc44dd1737e9a31bdce16df9e118345ec1d9a9c47bfa60502722c9813d31c9f1b0364e2e25d77a0816db52f7e86bade9aff644df50b1cea9a339e76e869fa1fa50101f8182a2a6aee2e90d242d4b4d6b999d1c358a8d9297b6e0e2ec141c2b3692d70b141a213d477e87c52a2c87a5c6d1f0fb091f35f50915878ac70000000000000000000000000000000009111b212a32363b", ++ "raw": "0x50f91c64826d6180843b9aca00847735940083045f289400000000000000000000000000000000000000010180c003b90a20fd3c43f5fc7807747a73e1d8e78cd9e611ad6bbba9b264534594174b49082aeab827927b335526cc50c2f0368fc666efb79c34af8ee90427d1718736acaad47f5959b9b26457619c07c2dda553d55a7498e30a37c92909f6b37c246dca4e8fd24d4cccd811396d276e5d11f1e8d14bb9eb4d5ec49798202422edd57ea4d8c0e8c6388e9c93a0ff2e52f7ca2303e40300c50c926a7da22919be462e2dcbdb3ec235b14290a282ee76a04f4bf7c5e2ec5c8fc0aaac72ce4f8fcf0d42b9fc3c4049ab6dfdfa12623d89c0d96434a8e13cb6f79ad6f4a8102f9a0bfe582d967372d8928ff9d7d7393b22ec1386ee91dd196eb9c349a6a12f7504e66a096eec0a8ae98e62275f6b84fb40a81a390c6ef0102c8fd2c11d303745b8fc49e638e6f3004961724a977a18d81efc67e37d0d44558e28cf9015dfa08eff82004c4a53dfa40c2451fa03a987f70c855dbee37e36480863044165e6d46d4c43278091317beab6f301d61c8f54f94ee88fa358c9aed6e55b80b4196e79a3d7251ede25c4baef8129882a69789df3a8262941199ded7bd381de00c257199c18e9086d9ce3f72c6d95c0a4d109a3a15474805f0dc06649a16a7805e8d74bd1af7658f5f9ef726d560595ff9f072d4cfaaf61283087843b904b0f5670ca551079c759d4a2839a71505e007cb5d49b0437b87a080fab5e2efdedd1d01f0f1ab72f0e6129a2525fc6e9829b56ce9d9f287a5bd80be8b1556e4ca82eafc60ec22315448768a72e45d4d956c16ee48a0658e307f81ee0f695f1b1b747d1d533d8da720f088b64608b1edb2d4d27247b2cd35e3d94e1256a3c5c944bbafe07b55ca74df4ec0b869733ddb466717a16e98af4ec310d046c90046c024fb33fbb2ffbb105d41bdd0e0af27b1239aa4d92b6ef8d727e51372fefc3b5c8bc6eb585a0ae2dd9970009555c2c2ced5e5a1e97c659565b6139dc4f3bae788e21f9658affa8fdb6f4733f7fa83fefab91a030f8ec976b8263c17abcef46dc22456d5347eea7c30dd25bdc3caad7cb2010f06573d56b6e5d28e6e847d8df8f18d6a2655ea66fe3c8e7e4ef192b5a8aeb35f8469277657b77155142c7b1653d5eb9cf9c4d69bbc5746e0c1876e330f9016a2f5343ec60b2f35ea380169a2bdf102888e6aae7e797d0bce73834f418018e8e55492da8236a542a999d9eea55bd52ee2e59feb0329d716c78c0bfcd79dc27cd9639399d2fd99d2fc9c4900ad60bdb8221501038ec4baadd2ac4830b2bfe1e20804c818c8e089661e7d842bc4e566fa0bc2d7968a44f8df0d4e88e494fc7bebdb695d3a8edf6a2703817b0f8c1a55569ac4a8ff0931137f075797e9744135a3fec6c767af26d4592a9022271a4052b1f1f24f072bd5ceb1d19e02b0bbf31251a7bb8b723bd3da02f59ca1c0c91bc10896970a1e0f8e0fd5b755da58d70be8935f65661629a9f833b35810113ce80125f8ed7a031cfd5eb1475b597856b4ce7629a9429d840a3457bbd70c05c73c6ce84dd59f917a24d06ca8743225ab8c266e209fbadf57cdb587df96eedf54c04fb3f83ec0dddd1d8bf9051c6de4db3aa099f1779f3f30f61f432e5c8f6b7ac4ee6c6dd6fce667556df155ddb2002735fb7addd8acf74863ccfe04a3088317634eb47e56b18c79f31318ac5e448a9094b5c2c70b44dbbbff7c235cef4701d504723b032726d009c6d7f527e645a23fc1ab478e6e2699211694d575bd436d778e7ae53868c63dfb78f591d82b979585cec8bbcde61f375e783e97e8f787fb392fa4f0bc3e9010aa9ec3cf6471bb16355c3ed3fdf2ee5b95ade86371d60fb0699a851770a212d0e0eeb3f20df78ab180343fd79a9d04d8dc56d9fe3d820b27d98d12275a40d270f4181aa1a18305c8d3469a954b2e85405a394684cc234702a6cb01c04ccc941a4392cf64ba31bea700d339152fce6abb0dbe84823b8d3b18258a29414fd52c91ccbc198e101287b79d8bd0e5f4b05a14a07055e66ea37d781934840a204638bffda400113bb1344126fe1c2ac1e57bb861c3d35349b1fba0c11e12921262ae52fb2e0bee444f8197e2817b9356e10af67e794a7967055936bfcb51f4be6fc5602a8df97ddcd73afe83c02f00d4f1d8302bd2e826d4c50fd3ded9a74debd0e807b694e912a8a3a800f5b983e171898874b77ea305ede4abdf0db5ca84a8a60b184374c08066537b08d42a609b97039ff8905bccc4454188b051c5d1e574e1e8b74cc5cbded064a5374c55f17002ee9b17447fdc324a12e3f2f414b491cccf20c9f98820461f4c7d61355a30f1a6cbb2fe7403b90587298fcc9500519451e44225c750b8b32a649ea288eda389e596390a5386652dea5d0e096ee270bcab9da0c3c4ba464f44172732a748e339cbfc8f0c1ee3187225e2fb29e67ffbd84df8acf759c817d628e03a245e9ae0cf00939fa12964e8dc124659eea8798b097ba52480532fa11194e4623ff63f6cc1d17c7080bb2c1459fe8cc9b197b89ba91986b7bfa61e514bf5c60f981e63baffd9fce44af87ad73fba197e46790fc03f8a46aa6d7590f47e08daedd7f564685208ee87da84b04b11cdc0c020bb26b0abdad3c1340649130e26af9c7651533313301e8b517a84c807d86a4c0cff68566d34da4111d67bdc77ef5f49ec7d83af24a143bceaee020acce02499c725ebc341a86d91c7f63dd962ffcc09fc0b5673873ee55cea4bba69b5058e4b364fb3802832d5118774790dd12eed78330412dc877cc034309a7e292ebee955a431fd46faf7884cf3bd5ab03d02b8784af2a84bb907ba5f021fc332f638a2d4b6451ea0a2cdee6ff1621d698eadfc99d6f405003f300e321a9e80ed3b577a899fa7dd891b53cab8acaa9fd3ce02640eaa75646f134c190817675b9e4a688d0668528fa3b378749bb6b1a0bd16873e89dfc927dcf568fc3cf94ac49d289ba1fb9c58a74574c8fbac321db256bf919e9432d6a8ae9a0dae900901f436b13d0c2b753a5eb6fcef03301d7faa15ff32ec0730335b13394bff66ea83a7043f9e1df5962671ff2d2759db234a4a4fa47fd3a9359bb14ef5b5fc398836d42afcc5d5f252b1b061afc6a0815b3a7586d25eed5ee52c56d2389b8f2438649ee652b431d3cc4262c6e5ea1c0ccec21aa6123e88b4bc933098576bd78cf5bcbbc278ee682029e05e91aac2043ad3ec1d3d97f6ebc6bd5faffa5bc71328cd4351bcb9a0d8d1ea9253b97b08c8c53b20e29703568d5d7dfa2bfa6aeb3a93043e4123116a125311695a11c8538658210971d7763ed7870c1309893909e306df22fe968612531e58a91d19dfe2c658ade19c8e0900c779af8ec333d19552e42cdedbe13ff85d99b10baf8daf70951290db56bc7ea049e6b468b6546a7f3ae77c2e14770eed1ea345f0d139336af4eee686ac2e73bd4262cb802e0173fb872a77cd2c0f3421c497b0260be79ae29d04d9a9da9d7bc101ae3ec9dace1133332f05b7b82aaee577c06c0d2a8eaa8a96db8b899cb3240cc4dca4f8eab513ad5b3529063b5df3664dde0db1e0ae5c29e69f011076fc7edc4261d043f38f1c68645466c9a5e685a07393b7e2f8f74db86742ba5ac089b45a42df3f943842747674c1653aa2a8ef282f64e7ec2ce1fada994288a5f9b95da4bc083ea008308b91213c74a08868ea0fdad9fbb1b485b793bce6e4fa945668b2351232a7c8a084fb2d87a5c83e7dced6bde3743617a7c4babcbf9381694b64d7e0feeb6615440e927a63293c6b695900b1e7afad1ae612c324a88e6761c3b6fdc41d18f514be1ba3a1ca53fdbcf8c19f95bcd60223abe52d07fd7c4114dad9f347cf122f6241429da7e598959c22692910323a0a699595398f518c58deea7159920d89e3e65702bc2cabea7e9ca0bbb7002fb7be5243bba6a08a12238ead443ff4394510d25eb279885d9112c44529f294c5a2bf493a74ba8f4e38744e632680b3ce055a897d2a5468ef52e6e70dc5af55a9fb83739f6cfc6994871662b8263f2e07e21a6514624909369e0138755165dd2af5a791da63c0604b68ff160fad82a578a06e76f62bb7e951b81d8fde2dade6c3a314eec5913151c16a93a5abd5ceb59ad96197a349721ab123b42f799dbde36d7965e0e5dfcf43d8d1ae13f4a00afd8dde36137d3de87f4e2c0366c18f4b3a449ffd9afc7c742c50456f6e642f7d09e86d5aef224b20ce5d8a2bf164484df8fc87daf69e459fc78c6b974cdb8fb1dccbd5290f560e3114302886e2828ad73ce71b06186254206743076c6af0c3d1facc36695dc28db1d026686b141247e1c7a9323d544b0079023cd2adedc0479ceefc8e0c34782603d0c27d18768eb5485a85bc385ffdfd25f735688a3b9a0c6f532b2a7989c751804de0163582293a17f635fdde14e9067f12929b079f89a53dfc87b986f89f718320ede83cf4086d0d095b24b66d5e3f1bfdb08b79cd5b25657cf2b20faff1d199c16bb7f631b33970b34c12997637447e4127db1bf580b339807637a86e0248cbcf8e1d30ae6fe053e2f9b7b3d0f9e3f4813af908b633560779012f9e06af00e252bb0df9f0caf7d1e82f8deb2ede21d13f406dd9ea69445d50844c95c921d8d1cbc8afbc5ddcd4157e65cbfbbafe2670e9146a5643db049a8d696f0eae07c3d4099447ef78bdb90fc05d7cc97bf9b9dee8719464ec6b9e6de866f73914a80396dea3900a9f7b43a47e648e20a9504da92c47ac65368813e5453e82020f8861aba2534346e6137e71adb28a5ff080365d6ea26dfc30ef5500d0760749d6e4e0f93aaebed544832308b19f72e92ebb8eaec2492c6ea7972fd359a8b67a1f4053bfb1a3c6fb720b5925a7a827e59b3bd0651b8014c77b25d61db17eb169cc2fa2442c82f68fb42200c59baa468a9fc92f13273ff071124f8b31e2b4b2517ee622d29489997ecb5676fb52e3002371c01004c214757b1f510091c2377a08d058209d18feb9b524a41712b6df4b92f2b14c583d3196749f367b3bbc8ab5e9e2615c511f23ba5fb1bac2ee9436dd8d162bd96f1fb663d2b520ba7e8ddfc2a4d549b63714654cfbb37ee1dca5f723139187ada8f8d5b42ad09ff196b70720d2527b4466d44f33cce486ef2e86311c75ead31a17b16989dc63bacf0d465379d9138df7ea2ebc71f4cc3b6662e24895050e2553dbc160a567d6722964a904d43bf2684206cee3562256e98addf3038e968cf6c09c921e2c89c9c249f6ba7afd20dd27587b4143f704f2738315a91c2d40c467111b4f3bae4f77bae402145e742ba83a527aeffca13ee2290988df882feaf5a3c24b416d59e06cf4f8832a016ce55419b7bd2028545557528a0af23f17942433e13fad089fdb79449bd9d99b0324b1828791249b6789a09cdfb6ec40c0fbaf8b28ea4f72d3df5159bea7a7965b94beb1bb01d87d76ae7ce47fc333fc152038ab4e7ca64a802f96f2e92bcc53ad9cae0242c622c99b2daf005e554cd6ba127991d84841ffe3ba0674eb7930509045dd00ab91a7c6acd2f4a40e7ffc9959929851e40d417f488a29f7bb5af3847a8caa5daa0682aea870ac8b866ccfdc435971e3d862c2f651cc6eb8a33fca313a56ab8228ee9ac7dce14ea3c3fba4f58c70888ae558d8cd632a2723ba54a3d538f4e20936a2ee543e9b2141e06095c72d4ee19908febf7c248c03a78655420da644746e2d508c31ceb0b648800d95dceec72f7576502046e5dce54b5a097f625001bfc92d6e4633b293cdfa63b1fca5de9e26d03ba2f1cf3647dbdad5359ed76bf702f0621c057dea3617af678016aa13360ebb02c6cb2fa0d3f137a1a3c639c989e12383841ab7ca5a84f84c08af4b85416ce7ec3ed91636a79add207bedf9dfb2ff626387f20cb3315502b2b06f361cbf6a51fb507598af41c5a5b9cd95ba1894ca372c5e1851e62a0d3d78dab6f92bd43ba6fcd71230b6cfd843d89268872b75e3c5b471db530b7ab5451dbe63fa0bdaa7d74907cb376643e129febbe6ce10b5dffbf323dc973f097d2081e06aad65e674c5a41bb81b80556fd78691fe6b5bfad47fd1b64db577f27f22f17973cbff3a7c4aa43569bbe6843d773a2f933f472650e88e0ec6643f87dfd37e7fba915c7386a130b8d4f0ff4d0e99a633d1f863f3e8ce4e1ac5e31438d89c3b6d5ace3aa6e14e7073164ea01e853958bece389316023127f9916297a47c5d6ab5513bab874c1ae3bf2850cfab9018cbf50a7febe03ff3e8e4e55ae5632812b6a147edf051b461bfe56f01a6ab571e0553e37a57a3a9811f4fda2e189d79b74215d6fe199595d18c6543c747572f8cdc61a53ce09d9ca96b43a5af5b56b53148dd4fdfc1e22c9958caa783f2d112d8d5fd39e6955df75d35c59a1967fc0d8d3293e39a6cdf82d6846b39a74b1a538372c266b3f12d7ccf464ff7e32fe54342994f51d344302b3b2e11eed2187a741eec99ef0a79a19a854c03d8bd3fcd557b9d8bfb35f0976da327fc7e8d95fb43db8e66be5b373385fc1b68099566c7e88aefe92c7c521d084c15c8d403d87bbc975e8d687e2a51e5ce53191e3db5bdc50f475853cda7cc5a7365c41035aecb61444c2a48492a600b58300b2805be97b4e9e30526e9e62bb3248a17c3809219307ca85351495c943a865359c783cf615a809c739cce0786c6045d9c8071797001434cbae571d42cef415e28ed6adab09ed10e0713dc1412ed80b8d26d549964321559137483c81b19203080b36abcd68e841f4c78b5302da79fd6e555f390322ae67e54c55749491889d79eb2982fc4428086bd1060c4113137ddb4dd7c9b690b802e805999d4f5127f66f6b1d397dc3509504e2196513b5787407b45b928cfc3c437523daa4f876dfe061e351cb3bad99ea205ab276c4f2a5ccee1027104f09adaf55cd282d4e9e44ac2b65f7e423da1cebb0ac70691347e7a7bd0de8bc9fb2e7a08afc0075a0e85902316cd6ec9a5802c51a6d65bf3251ae712ae0f66de6cba1e51efbad53ad7e769f851799caf5fb81ef35b4da814915dcfa67553f1c67158154707d16804be27af2115559e4758b313b9da4a1131718b29a75970b3f0a5e2f2c11a0a1ed47f238c145dc4fe9e8e963d8395c67dfa94a232b8133a0d953b05455f79e0bdff0bc3de9c04d93d97084ab99229622cfe3958ac5fda6757de1696b575f6250b441e6368386751b1d2f6007d254d152fe76578fbbe164af49cda805b9325f7ea47f5e18f3de1ce38006ae1bf027d3e354d6f923d8bc6286aaf9b9a7b9e7a7fbd36a31db0d88166dc68e7eb567d8164bed1e279a8810fc21656621ff7cb42a21ee04e6086924bb62dddd16c3343db69250f7ed95aba953202640bef18dfb6c00f1ee03c670a5cc6c0f3cd7cb006590dddffa6f19d450860e289f85edb4fb0dbbd2f992eebba09cddc1b20a3c98d8739ad1875a514b09b1f23cf406c4c55cc4266e0b8b283fa445849316c30bd19254e9d035756d2773cab7f5fd56d6d3a094a717ccca43251201cfddf80ba1aa0dad34f5a3acf8476b784fd53a1aff1307a7838705112650bd25dfea21b991d801f018a5dae26edb9dd5a0ab940a94b7bbb90fd1f30f9d1dd2ea3aad6d0a2a3c42fd2641233c7d624314c3a54ce616d2ca5683d04f6fc0924f0b667be32f4de8820f9cfab963a287a1c708857ebabed9b5bcc35d539ef9bc170636cd528ba86a620195134cacbc2b8e742376124b67ed8155ab53979e281f97b184c60355b008f15f45c8b7a506b1c1f511e4aaf0bc32fee27b656b01ed6afaa4053ed674a60175b6c89a77d42963111d0940730db02ff67534fe6a296e0e01e2db6b3f0fe9648286a0f3420cd4393163662751deed6457abbb370f83ed92e9da86a0181df13b8ef33e5718ce524a700ba5f2b40f3f973a67c1c412dc251c19e6b7ceb279dc1dfc36a3567e3a98a9d96ef52b0d0734f57bd9edebedd925167f230b1c81ca938e94ceebe43d2573735873a436331a8b76c1169c6a869889cb74e5d33721bbb99ec87f160c025f570f1d11c2e3ae6a52580785d854790139510179b003418dbcee1f4fd4fa39fc134521ffd7f29cee650472638dd04d8331ee439eaa32c57b56c4fd9c9ba87516a8236dc99b8d6e4c2a48e53a6779d5e13bbd73cc7917f0cfb8e231218bf33bc3c213599eb76476558f0e38a3979cdaad6ac6b03bd0ef0aefec634fbe0e1e87bd78123a19b7352326b9def856ecfc02b06622f94e8cb2a1cb6cf10d020f789a7db775063030f15b463f08878219636f2bb94ed11a3ed9689d797a3253809c7f2e0c60672e8476a9e272075c7f808258901e611c4d535a11bfe01daccaec3a26a187ab45a9fd0bb9a7ecf2e9c4aa3177ddaca1449f19f78ebe26be2e25bca012a10649b56899339f5418925648f3253bd716336dcf65e6b4f16d2ae51fed1acbc8335c103f721f46023cc2043f568ddb033aa5f2157e52b89eed4571cb523fcc66ef5cddc90679633c2a46f45f1a86c975db4f6f37f9fcfe54809ccf75305bc9f242f26f30a0ad5f3cd71cbd31ac45ceed2dc8571142a09b7d3091a4838727b9a067927e17b7559076649b405dfd480627881489d52339d4a8943ca396e92b92066e45d525acf7785a944345d25d721d6a354b1a9f3e2afd379d1c341badd0dd913a3471a5b78a81c0b338a2984e115d003b1deec1b44fd19824279d742017248fb4434cf47ad07fc87df86e634b2d8f6e0aa572088678b9a9f767404f06c84ea8cbf65d46effbde7de1f090d3d4b9e7c7c3b2b1dfa9e591efe12ffe0fc82c9eeb7fcd9f25fe438b39c46614438d0be7e1dad11bce8b91396f867531e8c45d8c01119abc2b20be253779b5e651bd80536854e2aabccddc5f4fbfb3497111fe9a8f8a417ab4dd806745c82126524ec325c931ac1f31a3cea19e159e1dbfbfcff761cccabefad38062872fe8a45a14781ba12551befb31c21150a56ccacf9815d8f458b1f591c71ba7b0459e7c09a4eac5c9e5b2984314ecd2f7054ed055ba082204f7ed62b1d513cd0992bc10c28bc745e30b27f6ca28ac84dec6cf0ef73c971ce12b2ddcc0d6264d8f7dfbba0a20f8ff1d7387cd9d9872f4b345b409950d66ababa9852f7f5ee634dad6085666956b97e4c9142dbdce96bf42d1334c61d04ffed5ac8f12a5075ac87a7416fd52579a876c89e22008759a93ebc57279215e178662a064bca65670089ec688532468c07161f56bbafbfa5655768123e713c7aff52852978e9d6004ab3a434d9b5c73f0d90dd1fe742a169a84f0e96a189bfc5265d8c5e2d9e42f5503d04ce01f9a912bf8b9bd8c282a4d40bbeca55d14971d1d305a16eb85d02b4edd8d35baea56156ba12f4e344dc4cb34509fc780ee97d24173a75e49960ec32055710bf8119d005447c6c391365737a0cff68d9f44868b97b2357dc051ed7fcf590b32292624545935e87f25e56b3e4a2b9c39c4936b5be9030b4711f246623264e0370291b675e75e1ce9cb425e5fabcfb55e25f084ea8a1b84870135ad997f7b700876691939cf268204669790a10ddd3d38cd22252226a09fddd395482deb561ee834ba452750d5b8e0de63c77c85a7446dd5bb29df2620f98e07bdac4cb59af8b9dc64d427dab780257e252e8d6793fd1b99f7c3162b4233a418526ab227d40b9deadc943d0f108a38d7a6b075465a44c920c0146338504eadf826cae345e864452e94706c8dceac5cca87469f2183a9c918295c09eef65f747b7a3d7fb3d259c4f531ffe48a5c338717ceb45efb557a47a593fac85cfaa4c263c109d385c01a3193a6ad1be5923d6a53e6c369a2b5e704cd6ac3ac255954c3fca1079895c596596873c1309fdcc5f36fe039d06806dd16cb00bd028edd6b44fb1e9ce4a020362824bef6a59502cce4e1d3d3884d7d865823deb092916a215ee73bdae1d4baf7f294caac7f63a36c2a15a40517438b8cecb93a5da3e11c25ff228ef3e7281360a2ce3ac66e69a2f136a83fbdbc44dd1737e9a31bdce16df9e118345ec1d9a9c47bfa60502722c9813d31c9f1b0364e2e25d77a0816db52f7e86bade9aff644df50b1cea9a339e76e869fa1fa50101f8182a2a6aee2e90d242d4b4d6b999d1c358a8d9297b6e0e2ec141c2b3692d70b141a213d477e87c52a2c87a5c6d1f0fb091f35f50915878ac70000000000000000000000000000000009111b212a32363b", ++ "hash": "0xe37a3c437aacfb3f7abe9bd9da80fb8a50398a56e6b60e6fd29f08abd049d9d7", ++ "intrinsicGasEstimate": "236504" ++} +diff --git a/ethereum/core/src/test/resources/aere/pqtx/wrong-chain-ml-dsa-1.json b/ethereum/core/src/test/resources/aere/pqtx/wrong-chain-ml-dsa-1.json +new file mode 100755 +index 000000000..2ad914461 +--- /dev/null ++++ b/ethereum/core/src/test/resources/aere/pqtx/wrong-chain-ml-dsa-1.json +@@ -0,0 +1,6 @@ ++{ ++ "id": "wrong-chain-ml-dsa-1", ++ "expect": "WRONG_CHAIN_ID (when validated on 28001)", ++ "alg": 1, ++ "raw": "0x50f90ec5820af080843b9aca0084773594008302d5709400000000000000000000000000000000000000010180c001b90520372c0f4874450fb92ace74d41bc0326c7f739589f9b86a97afa87ef65d5939e608c81a44909e41f41931349f744b7aedff8d5270b91c4a2f61192e2bb6e3e6d12231deab235359e78ccada386becb6a6a1105afc0999243ab8b363f7cd2c19b2b741c9969dcf20252f20d130dc05d9244f58d6de9b4fb4b6f0125c43b60ebe3c7272b6ae7578f1abb4bb5e2dcb31eebf4aa7089aa2bf40a0ba5eb73aaa90e45472168738cb0e866547e079075d0341c4728ed794cd449d0cdeceaf64d09173a2677e3f2621b56014df58ee8de2bbcf8cb2bf369e57a28e40b24df6d0b15d3417b7b7fce3c49230b3bb7c7080c4f1344380db37299d8c0c29a7c9e95bfc4f3ec70b767895be6ad798449f94c92f320696f8f9ec844fc9451f5ab30874e6c78673b932e24a918d5bc03f4fa647c8679631ad8b30ba4f9a1a790a535104f5079f781fa4c4aad807777327e08bd6e14b957e527650d39e2de7d30da7b2b27457d3539c2f99903054bd1a6d6301b2f83249db80dedc911059b047b5552407ed32b79b57ea4968198decf978fed532277fb189841d0c17f261f1a7c4618c33b258a71cb6b2b921c48735c1fbc375a80fe2d702e9f70db34547147f6d65153f1c508ff90aaf57dfde59ce6e56c5c52049b10fbc2640cd937bf5027d755444cd5e84aad66be4ccf04f710f0b970cbe7fc5e4e7a44f5d8fc2af94ec6df87862dabe0d85dd17e43728800d5dc8ff9e9e1fd349b9d6431f264836a447646a1ee2d73e6a4d1803d71b98510d94d53b5d07764400f413b7837f463e273980296ad5e78a62c83fb91354c25c2e28bfa19480ca41b0ca2d8beed456029eabc0ba69cf3504cf1207ab21bed0e3a60795f5b066019132212a9bd20e98b43bb32484454b7693bc63764d0ba8768da4336ad16a979b1d3e8b06e89afc4caafaddead83ad861a4596814062468d8cb2e131e71cf38c4e335bb3d1d9629251fd50a4273999a20e11d9a881fa2465c2ef32301fa8805ccb38b4656e3c815b54ef42f292d05847c0e5149b0a2ffd1719fcad10730b763406e9e453d2e4a556c397a984a7d8622466522e79f715f980279cb2d1d74d5fd15f8beb8df9c0a1fb93f0d5494ab701cd60cf2b613b484a191d873449c4815f44eebb62658ef90f7033a5e48d40c8f4ec9a25d4f1b73a150e5b03a4feec9142362e77d74d364bb8a40f9873efa2d9f458d84277fdb0063642d9ebc794356368a0b4e53f6ab0d9acf4418fa42d05c11db12cb67d6c4cbbfde4d85358abaf8042510db53c1540fcb40045b334bc54a17608e82433d1e9e53c99769b86b69b8cf1914584cdef479061718a2cc5bec5c31272e0463139721c0c3523a6ea31832b5c15aa59819304c217fbc51a2636bfff331e6dc1037e89be15c17d6708762976329353612f312b828e525c09b81afafa5ae5c25d82b70b6e55f892429a93e5eb4048ade675583e828da988f9d253123a06c0ed047adf4d93d20cc6c865f5a761fec27811305165cd4bda05f563215d0612944b88ade9d64477ae8fb441978720e70b0d77e2edafd3a2c571cd28a42cca2f289d157cec813c24e7f56051b4f28c153adfbb2c1ff90ea1fbc03cb89603387169072442f5d3081140e2256ebadcd775d2f1da92c15a9ff1ce6e980b3b1e91a7e379081ed46a4259fada21536af318eb2043ea16cece60b73acd415d239389cad9212e15bad1b1ae38f93c756635a9b87417faa844e4f43bae1f1ae68641a04f0ff1d55f9045cf7e3ed59fde5c07219b9113329356d4300885ee19c9955c5757db33c6ae75b49d035003171d02ccbd7321d6a14db0bd011d4877f04070dbda3b67bf22d083eb90974dbb3fe4443f9160a432bb4e436b97eb1dddeb445ac5e99b3ecb7573e7d8306ca2c9ca81e4c0b9cb4bc8a19222f0f45910a0982bd3c78abccf395a5d87c9ee78cfd7d902808c21483a0e99137edc77073a35ad680dfe59c284e489272a85481bbbdd14158024750fb2762a931e06a16079056458fe2bb97ed55419da0798f94ec35767c7511ce7ccc0e626152f8b566bd71377dace697520cfa5aff622276c0e5cb0a970cc3795c889258daa614042dbe34dcf258b8aa52423035182399130f3b140164baf8caef8e9767f31db5beac2872907bce2550f448ebd74184f8bd28fc2ef9aa9e2e6625895fca23bc190983377c16b98ffb338e78ca6d64bc95fa90258a375240d11fc8cbbd8ea8f18ea28567ac02184342e61d6c6dcb3fe04cdb8ce967cec1c9e205114304839c81d5af6895401ba809bb81cac66062bb5159cdbb36b644927d99bd2ad337ef7fde1850b6b4776b7f484f4aa0b9a2a3bc69b4ae541bb6adc3cd5d8f9de83a5543941a97b2e0ff61700f834e90a74c2de3a0e02066685c94e3e08964b9f876772aa3e04be45979dc7e1df55e8c81d45bd07379d3f672509fd5faec33122e17f08c158948dc23a1bce964f6f665e7cb41e8994ddd2f01e8d5552227925c85985517e703dd10c79f4d77721bc28872048f7feda6197ccbfb0560be1e013953c76b9262a178642b0be5628880950fdb8bea2ed6b301b2751a8fe1876b8ab4d5f34dec3bc95b3d01dd1c53897c8cca238b7b7ce99175e3bb8547daef19d7399db5029106cb37b67d70dd131d6cbf97ccadff5fd2bdf83b0210d94ec7b1ee8c1efdc5646d704fc79544026b62c176259f3bb3de1d0a0be8ccd9d95370083a78fe4a0269484907e88051f16ed1e9da53180f28980e0951b897e3ab2694412ef3fab73ecf4ccaa14b236d4aac95b1d943d563c0194b6fdbccef0c92bded1d856684b98e76ee8832c64921f999f1908c5d8ebb9d5895ac48f976e60d636b99f327532932b44170e94db91b268e5e1024e2e15ed126800e7cef347cc58743fd57a9b7511e4e95d422a8e6b1022cd6ab7816a6afe1fcdedba4915ebf5a459e8b26d3c848276ee3b0a8202a3140e968a2686506d34960d96e86b309864754fcbfd128b816346fdcc04b8d14faff7cf5ea31c7422ebea74a7a524548bc57cb38bcc8d8689c90a469d8f0105a6f0cf2e7c24933a7993988389f5066bb7385e1ed75992c75c3df24c0a08c3f2162f5bab574b13dae648904d6754f92e1354190e706f1bc45ac71473c465fa73d784c09fe7fcab408e1d808dc5c318a0a6db8219121958f35046abcfed9d874f60dd18376fcd844c333ae7dc13353449082c79f46e9d7a2eaf71a7c54867b04dccef0e3593ce939e12e395e5b69a9a133e4a2f3140f17f89894b70bb7f21082621742249f544464f1995693f37ade71ead1de00701beade7e60d857c7c00c5431a4129c42803d00cce4f3765a9dff1ca90fbd27ee23f3a75baa049fe6205a24aa0a621ac5126649644c4b0cd3cb78d5db9460dff55761e5ffbae250c6365e632d953291efb1f891a614d52c8f882a7c867725c427eeeea2229c424bfc00ee7f4b186c8c68daff492d317733795f6674a44e3bc276e013f28c75fd176c53d8f6fe39675562bf7e0957a631587c0e76112da49f057786a976110ec3fa93e589117f1346180af06f2954b49ac60b4369e49b53b8ac068aac4e107294791eec204ac6ad869342cd55e90257893f92e3ae3c598be9cce120d674a8c128e46951a6b8817033aeebf46c55a1c9aa23d48c03b24ce63786fd7946c4be1f1dde7e6beb733f8f3a8ac8363ed03e1434669e7aa048ae77176b393c9ee6dc5febb7b8ca0b343e78601ca958f87600561c3bc4d32cbc0a31c0c026ab7b5f7659c15ea8d71709b9c5d3d377702097345ef51efc9c9384cddadae853e0d889cb835cbac3ac59370fa6395f9ca44a4a6250fb55181c83d14c1e4b5ec25ba3211abe634fac46163998739787b76a9f253eb83d0e9a9955859748cc42503b95624516bc41bdc12fce27d495c7326cce0e25d831261ebb157954913fefefc24021d90dfb342749507a899441028a24c5a678b636b26c8389737dd40c10f37c0d07ec8655247f149cc9dbb3567b2c81507c07f17944cf617477d692b6024d2000416fd75de3463eab6752130eef9d3400362c82c44fdc950ea47ac75ae25bc133b8c48f8bb9bdc6d0c7c109aadd7965b87a122544a14e72e219d58d237e7795cd3132e7802b8c370e8c14c170ec3026b5572e393e62cedb243a376d302e6573d75397c72b507a0e39a4b9c87c8fd6f49330e35248724217cc9782bcb408f5ec6f1510d334e31978c6ef5baac79444fb7ed56bd6eaf034d0dc32a66e3827886aa3c10327cb9bb7c9fab27bcd9a97c4d4181d06b7fe82204a369a079c6bbe454de03b87daee2b76390c701a26372cead63e99513d354dedea2a0b5f1788302ace68679fb52d6f11a17ea658616d4f625c1d3da587949e048acf37c4980125ea1e8666f85e37ef13af70c2a4219d9cc12fc0855713ec9c139f1bf41f34f0b787a08afc33814ff0e7530c22946d0b33ecba6d7be579e0a5644f6a4ee5a4088bbec8ab100faba7edbee3238a8fe55f4a19b45babb17d10cad5919a01839e67c256b66c84571a460f2ef287c367562be9e02bcf5e2a2bf1036b4a61e0393d58acc5554691847f8d972f48cedd46902b7c4711841bcd9d6186cfa462df62a78cb924429994a56f8edc2466fe9de98bb87c75d771cf41528174d7e0858b00b3398e59a7cd32a64ab4958b03724dd05f00915d08ae5356d605494a2728bd06fdbabbcb6b1bedbb83239a91a5ea9132f1aff725414898b5a848279c0f789900ad0c57b9a8c8aa3f10a19095b77e14f08389423411c59651ccf228fc0c0e94d0a057a894acdf7280c05fd56c7171659686383ebc6b19a93c47139f3ed455c0e642a49bea837a451cb0c6ab2073ce13f6b72eb71cb138ffaf668d41b8a6e94f1a9c216a33bf0532f9e9b4ed22944599f90ccc7162a0132113359dacb2793aa80c1077dfda1799414bf3dd0d2ebf84663c0f5762429c9ac94755562a7c62d90eb6b99233531d3bb7115b346c4644e94e78ea0a11ec4b1d18ee61891aa47bcf3988bae06fc01428dc32d5012ff32d1d2c17e1a7e23bfba427ac10a5462d6fecd35161296da85d00e3a59d5e6a68a38dd0e3f3f03b157321ab52d551bdadf30223f81df4d9a477bbf25c8ba80bf835f3e1012783270bbabc02b4f29ddb269784ae6d1323416f8a9799a1a9afc8e2e3e7f5fe0d1d2e4b4f5a858d9c9fa9b6bdbfd0d405091112383b44a3acb5bdcbccdef2f6475867838bb2c6e5eef4f9fa00000000000000000000000000000000000000001020303c" ++} +diff --git a/ethereum/core/src/test/resources/aere/pqtx/wrong-chain-ml-dsa-2.json b/ethereum/core/src/test/resources/aere/pqtx/wrong-chain-ml-dsa-2.json +new file mode 100755 +index 000000000..cf1ebcd49 +--- /dev/null ++++ b/ethereum/core/src/test/resources/aere/pqtx/wrong-chain-ml-dsa-2.json +@@ -0,0 +1,6 @@ ++{ ++ "id": "wrong-chain-ml-dsa-2", ++ "expect": "WRONG_CHAIN_ID (when validated on 28001)", ++ "alg": 2, ++ "raw": "0x50f914be820af080843b9aca008477359400830383209400000000000000000000000000000000000000010180c002b907a05424000ae9a496294dd6b99046cfdbb235ae71031b379ce274c13b0c96411740cb61021a5390fbe8fb56b642b4e5e5abb7aeab143e0037f8db283b9f2febd650265ed6b05f1b867c124aef245c1811422f92414447fd0b20844e0f1b1cb4eec052400aad52b9d773aae0015c0c390bfdde3d367bd8ef9c3c73bca3164b9d418ecbb2d25303bbd12739a858c5fac4cbfa0203b9f3c319e9bc89d17a7a244b57d6613b5caabdb528f077eff9727015b9c266d777c0ba11920118c7ec4dd59b380c40e361a5b23ce04be4c61e12a1c21e091fa0d890687cbf588944db3e34253c75cdf797378de79162e0b4ca309f550365a289f652dd6d559478912cff70a5417589f73f14640970694d7b5a6bd8f000a65c632f0326ed3d128fda96dbea68cb812634dd75e9f63813a5645336681378b6a63220c066550aba43a5b937131d41f08752d13949a115a74bf96fa1925ff4338e10df0301abe40aaa49e14a1a1d58eef005a0f1d0299267002cebeda20cadcea7419cd0c365c53aede1aa36efaf764d3d36169747cdc8ee9a09e0a0ac6bcaeaeded0cd303c965d705d1b91a8b35f408ad8a033b02ff57610bddbfa60e2114e53f20f8fcad15dffac6a494c3f4f631590f2be3422af14cba7c900c9c38796952e20c3069bea2ab54bf0edf9315903aa5cf68549f878eafcc77561b36f8b78d950871ed4914417316f868a2bc54c72d3c9929f0138a2f0cbccb7c4141e94bb8a7ae11f660861cbbe498697d7327709271460a75bd4e024de3d4d6fe5af066944c96ba87459fa948a95eeb61abbaf643190ccaa9cd18591eb6f249a6eb92db89fc95616d21b41d7d15424b80a750938a12cd57d50ef95aaf222025dc4d81df320d9bc32c608b1c5a1aac8bd14c0f7a4c4380cdd65c793a6287050ff4d609102fd91e47e7365fb55513aeca273618528c199f7bb151e9c11a0bf202147734d1be9d5d0e9318096fa567317ed9fa6a8efe201b915b9aa05ca0d19e15c9a80449cc130b398ebbac3d938f55eea8f74e024b0e33dfecdbea02b920ddc5b277146685916194bfa1f8ac7784c7ed76923d4ff9ed97d3ceac283f9b26977df0133c5fa4fb5fcec577bc152e926529f06ff50c204c083c0b4d6b8d474cf5cf7cfcf7acf76921225443eb0abf5e4c76d0a3f524243ee4e372e80ff0f76dd9d135f240114a2ebe6d69609b6c60d275f251d7dceadc600d18e0cf8bc8fa8392e61fa32b8f7481c4883e19d820b51af0f87d3f7faae44e3b908fc759c6de59361487d8e424ffbd662d57a68801110e9cb099214143253949cc479eaae0e321fecab90c63b06819ce3cf56fc2fc1419408075c658024a28950aa47d549bcdb8b5bc9aeab463466c7834ca7d73058c206a95a023f0c8ac95badd383e63041e1f0ef69f51634f5ce2f48d80f8f2e92f4a1fb5322930141fc50e1e20bd235a8b8d768d473b46711257d028e710f82f0b6bd923da05ca64ae805f708577e125b3fca2feb5656f61ded034eaae481a195628801c82db85345f9ffdc72c860cffe2264ba4be2a08fdb37a4f5f80276baba4ea637847335c89dbd35504389f68b2d61ea66d4eac5fd0057d9e757d929214a7d83daf2fc2b3f840e083792d20d772584b252ba955ae8ae8c36af648c59ae0b25d3bca19413fbd749077b6969db5847409d13e4a67387e59116fe3836a733f3253d47f9919f49618fac8edf5fd711a4239447655d6c40f479f86ef3c9d4dc1a6f9683a3f1bc5c9730025e76ea1588d43494f59823f42c207e34412f73c7e2a126501b8fb810f12995c213063d909a5a9c2653d3511ceb809efa69c638f4c977d46a2a305cc831ecc0feebf5d9356ede6c780da78fe2ad683b3312282d5142612c0d1f85108de04de5c3d5414b62826d5b75f8887dc2d932c40f306c08e5d4f69ef4d344fe4317552df9890d0961e3d857bd9b2c5b44536d8606f62f66d14ddd43b0e5d3379cf66d664616df12b1c62385a57f6a562c34e120b79d64ca39d8b23e1e07b2c03910d711f09df5b54cb0e133dd751bea7832fdb38de2f1dafae69fbdf82b6887ff0a92940299020a835b958bb01b25b8699a0846614cb256a6621ef92e4a0947df9141ed4f91eca527aef25862158a8fa712dd60dda947f6a16e077810e29542a56f9244c34a73454b9ad0d8e8c40bd503efe12c4e5bf7d0ff8e6ab642a039f4e8b575d33c0c3de77a1abfa60b0252d7a549dd791d9630c8b848988157608448d77938bfb27beaea8f9b4a259263797c5b2fbdc7f469b2769f7dcb7b9478cdf5f109322eb2f0a83163469b72d8121fe8e01df227563cbd941a1f63961e2a010c6322a812a014279583a0066492a17d1a15c07024f6b8b1561041b3db608ec1c7475ce4d086e60523e85d7a955841558a35535d6e52cb166669a69049646b94d2fdb901ffbb1446a2d7f58696bce000ccbbb6942b0ad6480d006602c228218d3a2f0bc6a5d5c73acda9fc4b0b496a7d527050f8d77b71641f030368e9494bfe8e9dbf6dd4040426da1b0423739b40450273f4a8d551637e5fe3ca8d54fc476e6278e75cebf4a4ac3ac8ef3807041c4b274568797c8caaffae7df08c6c158d92411db981ea02cc212064c3ad6a9cd1cc02087acce5ceb8d7cddaec429d199c3d90670288be6e88e6826d1f6c417df5f35139c71a62c0267adfce7cb3aa49e827165b28c8cc157a691cb52ef103d8d7ca0ebfc628904d0b2d365dd3401b620d6d93cef670e2fb90ced0f4116f56457e97077c6728c16d27e9876b13275f1e5fdaacdf89ed7d20f7d53cc974910a84cdc1aa3c1289de7ce0a80dd983de4463d77f3390e4a9c64baa94cd0f8e31cb21844698ec396abb704d0886c9ef5239451d5a9c328564fdcf5203a0f6cca322d20e25a21a6d96246cd64ac993e175af68aa3ed24b210cb1822e8526257427552a8a896fc3ad77ac0a87bb1d574405d7ced0467b62c49470365adf9e16c492eb671ccfc637e4b9dd0a32f9de2a51a8970d431d8ad92e0cc265d8c6d165469da63fca1f189d70c51e911f624c3786b42e3b3a8c372842cbae99d6afdd2281251c28adeefde311b8bfd0c45e67ce6a6ec16193fd9bbeaff975331eb112c326b303748bd0d28535b73bb1a179b4ef4eba08f42c2dda78366631dfcd78944bdc9058169461baf34386bfd505b119ab6b2c9b84b9b130bb7723e601ce99257925bf06fa80f5ac9f0bd3d00044f71451e72354c0e20408548cbf68de3efdcf93b4383820d93c5fcbae49ecf9fa027660bd9f91613be07d7eb3cf2153588094db67c578447d1302b402283a05e03265d33c0f0a7bd7fffc77c2092a67a4a0983d0a6539bed110abca2a292c7c8f5eec9ad3cfc085708394705bd63d386eea1aa4b22daae2ed1685f17161b7df3ede14455504195692c683908273d73f0503ed4cc7c1c61288a17193923780fb50f9b020e08977c6d25ef33373ba13ebda30c38fc1a2d87d79b846f2ec4511e2fe77eb60fdc46811f055a646bd93de0b342177919f009be73913e21a9171584a1b5051f258fcf079fd474bcdb731b003a37403826cdee0e8ed0dbc9ca9b56cdc7e6d47c653561b47f3b3542a2c8c28777c13c72f4290e832452decb15cc5dd8c06283750af8c67694b40a8902e8f631d7b9447c84ff2122bf310f86cae68147e2357f8af55bbeff3d80b74f55c3e097681e593d0aa6a11ef2c3f55832f21f21bcefda9ebd539e4a11960ee8af8c609b7cdbda9580b278094058a30c5047f7a90b5b87aafb222afb7b80570cec0e3dac70d56592cce38ff1c4ae0b588f6cccd46ca44c77381a529aa712c8e4a806b1938ac1f1363912762be173e7d3f9e1c2dc2ca36ce29038463fa8697c268e68bb614e40ab18084c27746eb8b6b4a71ff53d509b877ac2532807d57694c7cbb1bc095092728374272e29dfb865393b9fedfb9ff0e67fec861da566318a65937254e65889cfe4c506e72e05db17c6094c7ee6ba48af6882582f5928d885daa934ceeedb494fcb8012f10253368df32f5951d8c6f319dd7800ac3fa437234265efb3fae1559aac30295de2d0c3e191551adea1d5da29bdb0fb81d8e1f6ff0e4579c31c8d5d958b06b2c9be37de43fe83317e079c18805836d72eda19cf86a97912187b39c0350bc790e0b760361c3b1558bae5fe841932dea5b84ac413661da37af425631c5f68ff4040d3c3f7b4bae3c74b0764d776d39cbde0b3c5d75f142ba111d4f604227900c3fa913b6d78d5fcd20df306b8b8f6dcf2321b1f90f55f81cec1b2599bbad1adabec95a2c4b0cfe0d429e056878af68871308cd2e21f94f6d6fef8b29fc9273aa97e6150d20c916e68710baa9b3de541b1022135151b61ce965d87d850eeccd4d74e201312d9ddf6952cb11f756f96558947f31adbf036179029c62dbc27815ddfee70b6652f257702a449381e1a7b66b5f550bf83cf23baac996f0b66fb405f5ed1400b5937d89e897dd40d70973bf716c97c81c1f9e36f83273e63cf489b26772a6d582fe3675137b934d1251cbb3d5bd4ab78ba886103ebe9864367a3048c3c0995e2bd2b76c97bb2e5c1d9e9c51244c202d33cd6a706f6958eff96bb47996a36f19e95350a4b2cd5a5d9a3e4c5240b5c6daa2bea47d43b4a10c767767ef81cab30be74d4891c6b64f0528d82a1e3053ea34231a2d10efa129d8d42ac32f9d8d0cdb559043cfcbc38968c8b031e40da7bc4add0f003c7e50216ad414ca3badb380de9c45713eb46965f8ccf77eae0beeb62529f21431597682605f4e07210a9ab9790b4614917c7b8f4db754cd57a031d03365317d6126de0a89ebe6ef2f877a64a9ec824ca8e6d0017c5249fcad3df09bdf09ced66cd6f840f151ec8a4db239bba62385779742ade58fbe56c4c79c0e8b271f77880953b8a39be239e729bc5fcc831993ae3fe8eb687527956a451dc7031428ebc64c9c426f77827adadd46b9384028a12bbbc4ec7cf7280f2d3fb2dffed142f13aa17f7d42405049d80dbb6ac9762ef56b947baff7012cf08c416935629de0ca1bd8a6f637d7416b780e898413284b16da24bf585eb288c85cac3520927bbcd07a1dfb8373b12ac7ab31f894074a15db8b033659c8e6160e743dc42228594e1db9f8b69f7d8334e75ec6288ab0da7cff63435ca2405f3d1e94b235dea4d61865f5e5ef73b3e86e28e624cab6fee2dd09d01f62d72d18a9fed6f77e660ac1cb3049a6cf0856d5373cda997bff223b10f59d76d6707fa7afc8c2dffe7a5a967f1340a18abfe9b64e3fc0822651a6c52c28b342842b830630c65c5e8cc82e8d06cba10b394207e72a4711d991e5851b3b5ca8e479c445b8c951d759b731b88ed7646f742a276cb4aedb8916c298a22c46103924f0b785febbc0d67acc5a869ba1fa1afda456fdd1a7bed9b29bcb5982fa0ee7eb2da5682f62e16b3a9754ac227df6b703e1b929d3b1142c6c616329d8a564e4a525a0e84194b365d1bd011c5bcac9812195ae3f60f6ec946f46cc2ad0ec7adade8ec2e5f70f1d6e65b20de2714f118ddd25dfa24c1b9c821af013cd54d363c0881a0295cf7129feead0336e2ff7df676665a392f57faf8bf1581715f3634049b2431c077709b3edba503de7a238eafc0c616f29ca4ed0b526ce819a6372f8e25e3f902e1fd7f7387b341948bd79bf3a1f81559138cdd455a098e49a68e64869b15806d363e7a5cd10c8e04ef7c7c28a2281e29daa999bf2aafcfdadf5d55b97c0dbf1b8e77a5351b26069b2eece2515fc08799a6f30896360b3eed3ca9056d5f2c1d8f761458fcc6d9e82091f6f0b3e954f73b9dc682c1b2e2be3d86c6da5b586ecb6d5ec9a57210e1c69467be07c9ebd1e363359274b27fdee6896043272af11e89ca047c388aa35d594d004c580fa87a46744f4f8be181ff374a362e753c8a48a9cf1060d176c08f01cef66e49b2ffc9b2280041ca8bd9cfcd10bc8e0058798ad72a45ed4566a9eed14aa4d03b6f691090515522b9466ee017f38cfdea83c92476f6640f7506862004d1a29c78575b8960cc50aac4092764ea5694dc21d84d65902f5c89384b687b9021ef3e66ed6d45a2f13c198ca722845f1d24b8729a766d2d1383573888b3405a30952c17e90cc6d4a9174bee6f1b0cbc8b449a36dea4593005938735ab45992733414e1bef4ec12368e575f5edd51a7b414fe67ad2c4fbdedb93bd350242fd4e285684e75ab4710982193459f779f7bd5ac12804098b53205bdfef7ae96f7615a52e35227ff29b8a1bd3ca54bfa1eaeaf2e84324119864ed4483fd9ca40fbc00fd5f72d7ca00ff9abfcbade3a8085392e70425691f8158c1ef9e173c10666843f3e14ba2fd5c78bd516e4a6503a11e06f39f6a5bbd657765317b1163f2ed9cbc687ec95515ec56ae75b64cc743c02442d02915e47b8c706ac70b0b5901fb61a5aa8bdc9057eaa42d43b37a5381266ecddd7510dd2a18138805758c2d41e896934cab73c61370ac959447e43bf5b629b904e7bba5ae8f959f923aa7844a898d150df35672635f304f8c55177e9185de1ef8ba82516d56bd27c7e7ba2b139a570f44f4085b275fa05d8f8e65b87a6e30570fa6846d227584ab63ae6293c0d0b8299fb53979a6e796eae7fe8f3439aa3a51889a8a5b1a4514ce597365a27cf5bec9e91558393ebfeeccc6462b7c25f6c03e251546d9fb603b1c25ab756044e042e2f325af8fc2cbb79fcf6f414bad1217cf96f21bb0d48c95c35abee171486d8bd0fa0b485b32f0e73dcf6f05a5475f95f3359634c4de53f44d1c53e4ec3264a82f635258c5b4d8bc912ba3b5503c53b1b239d44d3799a62dd3913871b0ed6fc87f186db1a40b2fe81ed197ab0df57c63de250f72ad592da936177e9f154b90625f76e86cbc2cd2f310d427330559b8b26f685a0a81d652562a3e3c70dd17e31b10b1c55208a551daec05aad5c5dfb32eb37a9dbfb4e43afdeb2d2dd27528c8dac2d043b0548ea7c2b0ca74ebf7dc4da83a8205d227e813d21808b11dec1ed78cc9fb489a0149eccc352e5d22f69b0c65993ec5de6f0ad0c20c936d35c26eb0f4cc61a0f64a376bd91ef980392fe99ef9e1845f1ccffd52b13679d164c7e917816daaee828a733ce4baeb1763ad59e9309ad1f6569dc7882b947032ce67ff0bb1f80362779455ce5741708931b8972ad67943161f6d2a9e4b376b07e3f5cdaff63b769b463be0c2e0a848211071e38709ce089d0413bf7bf9464fcccd03f16ae31c3a607716ff8f52eedabfc4f645c2ed293dee63f3434fb12b5024bb5cc16b0c03ebb2e8c2af1ef088e863cb75ecdcfbbb61ccd394954357e3baf7f7f2a137416073d0dbec002a60a5cadb5d678cbfe91d84bcd31b2144454c5677e64f9ff400000000000000000000000000000000000000000000070d12161e21" ++} +diff --git a/ethereum/core/src/test/resources/aere/pqtx/wrong-chain-ml-dsa-3.json b/ethereum/core/src/test/resources/aere/pqtx/wrong-chain-ml-dsa-3.json +new file mode 100755 +index 000000000..85f6fec1b +--- /dev/null ++++ b/ethereum/core/src/test/resources/aere/pqtx/wrong-chain-ml-dsa-3.json +@@ -0,0 +1,6 @@ ++{ ++ "id": "wrong-chain-ml-dsa-3", ++ "expect": "WRONG_CHAIN_ID (when validated on 28001)", ++ "alg": 3, ++ "raw": "0x50f91c64820af080843b9aca00847735940083045f289400000000000000000000000000000000000000010180c003b90a20fd3c43f5fc7807747a73e1d8e78cd9e611ad6bbba9b264534594174b49082aeab827927b335526cc50c2f0368fc666efb79c34af8ee90427d1718736acaad47f5959b9b26457619c07c2dda553d55a7498e30a37c92909f6b37c246dca4e8fd24d4cccd811396d276e5d11f1e8d14bb9eb4d5ec49798202422edd57ea4d8c0e8c6388e9c93a0ff2e52f7ca2303e40300c50c926a7da22919be462e2dcbdb3ec235b14290a282ee76a04f4bf7c5e2ec5c8fc0aaac72ce4f8fcf0d42b9fc3c4049ab6dfdfa12623d89c0d96434a8e13cb6f79ad6f4a8102f9a0bfe582d967372d8928ff9d7d7393b22ec1386ee91dd196eb9c349a6a12f7504e66a096eec0a8ae98e62275f6b84fb40a81a390c6ef0102c8fd2c11d303745b8fc49e638e6f3004961724a977a18d81efc67e37d0d44558e28cf9015dfa08eff82004c4a53dfa40c2451fa03a987f70c855dbee37e36480863044165e6d46d4c43278091317beab6f301d61c8f54f94ee88fa358c9aed6e55b80b4196e79a3d7251ede25c4baef8129882a69789df3a8262941199ded7bd381de00c257199c18e9086d9ce3f72c6d95c0a4d109a3a15474805f0dc06649a16a7805e8d74bd1af7658f5f9ef726d560595ff9f072d4cfaaf61283087843b904b0f5670ca551079c759d4a2839a71505e007cb5d49b0437b87a080fab5e2efdedd1d01f0f1ab72f0e6129a2525fc6e9829b56ce9d9f287a5bd80be8b1556e4ca82eafc60ec22315448768a72e45d4d956c16ee48a0658e307f81ee0f695f1b1b747d1d533d8da720f088b64608b1edb2d4d27247b2cd35e3d94e1256a3c5c944bbafe07b55ca74df4ec0b869733ddb466717a16e98af4ec310d046c90046c024fb33fbb2ffbb105d41bdd0e0af27b1239aa4d92b6ef8d727e51372fefc3b5c8bc6eb585a0ae2dd9970009555c2c2ced5e5a1e97c659565b6139dc4f3bae788e21f9658affa8fdb6f4733f7fa83fefab91a030f8ec976b8263c17abcef46dc22456d5347eea7c30dd25bdc3caad7cb2010f06573d56b6e5d28e6e847d8df8f18d6a2655ea66fe3c8e7e4ef192b5a8aeb35f8469277657b77155142c7b1653d5eb9cf9c4d69bbc5746e0c1876e330f9016a2f5343ec60b2f35ea380169a2bdf102888e6aae7e797d0bce73834f418018e8e55492da8236a542a999d9eea55bd52ee2e59feb0329d716c78c0bfcd79dc27cd9639399d2fd99d2fc9c4900ad60bdb8221501038ec4baadd2ac4830b2bfe1e20804c818c8e089661e7d842bc4e566fa0bc2d7968a44f8df0d4e88e494fc7bebdb695d3a8edf6a2703817b0f8c1a55569ac4a8ff0931137f075797e9744135a3fec6c767af26d4592a9022271a4052b1f1f24f072bd5ceb1d19e02b0bbf31251a7bb8b723bd3da02f59ca1c0c91bc10896970a1e0f8e0fd5b755da58d70be8935f65661629a9f833b35810113ce80125f8ed7a031cfd5eb1475b597856b4ce7629a9429d840a3457bbd70c05c73c6ce84dd59f917a24d06ca8743225ab8c266e209fbadf57cdb587df96eedf54c04fb3f83ec0dddd1d8bf9051c6de4db3aa099f1779f3f30f61f432e5c8f6b7ac4ee6c6dd6fce667556df155ddb2002735fb7addd8acf74863ccfe04a3088317634eb47e56b18c79f31318ac5e448a9094b5c2c70b44dbbbff7c235cef4701d504723b032726d009c6d7f527e645a23fc1ab478e6e2699211694d575bd436d778e7ae53868c63dfb78f591d82b979585cec8bbcde61f375e783e97e8f787fb392fa4f0bc3e9010aa9ec3cf6471bb16355c3ed3fdf2ee5b95ade86371d60fb0699a851770a212d0e0eeb3f20df78ab180343fd79a9d04d8dc56d9fe3d820b27d98d12275a40d270f4181aa1a18305c8d3469a954b2e85405a394684cc234702a6cb01c04ccc941a4392cf64ba31bea700d339152fce6abb0dbe84823b8d3b18258a29414fd52c91ccbc198e101287b79d8bd0e5f4b05a14a07055e66ea37d781934840a204638bffda400113bb1344126fe1c2ac1e57bb861c3d35349b1fba0c11e12921262ae52fb2e0bee444f8197e2817b9356e10af67e794a7967055936bfcb51f4be6fc5602a8df97ddcd73afe83c02f00d4f1d8302bd2e826d4c50fd3ded9a74debd0e807b694e912a8a3a800f5b983e171898874b77ea305ede4abdf0db5ca84a8a60b184374c08066537b08d42a609b97039ff8905bccc4454188b051c5d1e574e1e8b74cc5cbded064a5374c55f17002ee9b17447fdc324a12e3f2f414b491cccf20c9f98820461f4c7d61355a30f1a6cbb2fe7403b90587298fcc9500519451e44225c750b8b32a649ea288eda389e596390a5386652dea5d0e096ee270bcab9da0c3c4ba464f44172732a748e339cbfc8f0c1ee3187225e2fb29e67ffbd84df8acf759c817d628e03a245e9ae0cf00939fa12964e8dc124659eea8798b097ba52480532fa11194e4623ff63f6cc1d17c7080bb2c1459fe8cc9b197b89ba91986b7bfa61e514bf5c60f981e63baffd9fce44af87ad73fba197e46790fc03f8a46aa6d7590f47e08daedd7f564685208ee87da84b04b11cdc0c020bb26b0abdad3c1340649130e26af9c7651533313301e8b517a84c807d86a4c0cff68566d34da4111d67bdc77ef5f49ec7d83af24a143bceaee020acce02499c725ebc341a86d91c7f63dd962ffcc09fc0b5673873ee55cea4bba69b5058e4b364fb3802832d5118774790dd12eed78330412dc877cc034309a7e292ebee955a431fd46faf7884cf3bd5ab03d02b8784af2a84bb907ba5f021fc332f638a2d4b6451ea0a2cdee6ff1621d698eadfc99d6f405003f300e321a9e80ed3b577a899fa7dd891b53cab8acaa9fd3ce02640eaa75646f134c190817675b9e4a688d0668528fa3b378749bb6b1a0bd16873e89dfc927dcf568fc3cf94ac49d289ba1fb9c58a74574c8fbac321db256bf919e9432d6a8ae9a0dae900901f436b13d0c2b753a5eb6fcef03301d7faa15ff32ec0730335b13394bff66ea83a7043f9e1df5962671ff2d2759db234a4a4fa47fd3a9359bb14ef5b5fc398836d42afcc5d5f252b1b061afc6a0815b3a7586d25eed5ee52c56d2389b8f2438649ee652b431d3cc4262c6e5ea1c0ccec21aa6123e88b4bc933098576bd78cf5bcbbc278ee682029e05e91aac2043ad3ec1d3d97f6ebc6bd5faffa5bc71328cd4351bcb9a0d8d1ea9253b97b08c8c53b20e29703568d5d7dfa2bfa6aeb3a93043e4123116a125311695a11c8538658210971d7763ed7870c1309893909e306df22fe968612531e58a91d19dfe2c658ade19c8e0900c779af8ec333d19552e42cdedbe13ff85d99b10baf8daf70951290db56bc7ea049e6b468b6546a7f3ae77c2e14770eed1ea345f0d139336af4eee686ac2e73bd4262cb802e0173fb872a77cd2c0f3421c497b0260be79ae29d04d9a9da9d7bc101ae3ec9dace1133332f05b7b82aaee577c06c0d2a8eaa8a96db8b899cb3240cc4dca4f8eab513ad5b3529063b5df3664dde0db1e0ae5c29e69f011076fc7edc4261d043f38f1c68645466c9a5e685a07393b7e2f8f74db86742ba5ac089b45a42df3f943842747674c1653aa2a8ef282f64e7ec2ce1fada994288a5f9b95da4bc083ea008308b91213821ea6fa73a5016d6bbd3227853ecb797d9d49be7e4f6fa69ba1f84c4f509491e1a07ad0af5fad203717c10931dbc67c1c1a9d90591f8951f595dc96357d91e202ebae7955f0e45245936c750840ffd8df89e0f2789e427c79ac48d58982a6e087bf4235920efc666854459d6e4979a308594b7284181a9cdc011b48e1520454c48443228ba54f286952331533db7bcfb04df5e4cde37ff417dc4444adf86a4c4cb15be04b0fc9cf292ff313ff1ab5d1ecbffd14fe67526d4b6bc29c838ccbb2a442d08df1f8686da94e80867f9cef8b96b62656778cbad55c43f24b0915e6ace48353025c135a5c33bce233363ded2753716e61f213b04f2c429a797460e1e4166fed83575632b70540218afe616cd89fb77a981f60952c58a2246bbecf6606c005fe50caa3c3669c5b019e3ba52773aecf0fdf06d570d4ebe10241fd5694127365088a8045523fa639f6df29c6ed9ecb839c27582b133867f2f4b9395cae52c2606176d8ef04d32ce7bd563fbd3f2f3b9f7fc0468bbb8c770a563c278e22acb2d8a59e5e37538f07b98fbcadee17f23e625e93e76188bd1ea77152cf39cf05e568b95a3f5188920c0e8ab46e4ad9d9e6533de563334cf5ed7cd4e62b43a8e9455d4d232903dda4774c7dd9bcd39b01c2744ad47b361469c54aa589a35c798365bf8e6748465c7fffa0b688007f5f814953f87c03c6b3bf682ba6e70adc817596410d1e581e8c0f237e889bb4d4a09be8514707573b6299414845ef8ba7c9fade369afd8b1d7078031ad82bea47e516a2e5827a67591e85e628930bc81f20a5733270b8285922eb6fdd1f23cbc47f34563578db8433eeb007c428e5815e15bf6232d1801a39deabbb77a1a2bef02d16cc88b975e1835c105bdc813f7faaf6eecf601a4a67e30053c7fc92aae3cfc52900e471c3e8b70da92395c6f46cb6930f1ca80fcbffe73d5c02208b62d5f5368d0c7fb25ee9229284e10ac2278556f7026ba0d3bd5fcd1e04120b728a2f308366757fe15d176f72df5e566a31aaa809a97745a20352d4d698d945da8476defb6052414fe086e326b558b64447e3c9dac0b1f4ce5a06e678426d48f3436597cba43cf8af80b94c3813832bacbcd5d7c167fdaa1ad13afdc0ced8c3da011c695a33724bcdd9902eb545ec9aaf7038cf39600b9dad3091adc79a8b0e6b2fb457260f7ff789dbf78431ac839802a8f435e0b29b3c236a4cf8c7c138896b6724ae5b3690503ca21977bc6c5e29506702292a120e59ef8269682389ecbd41fb5aae0f6f04d0bd1942a527bb5152eb57843c6f64944996972eee48cce5d8a69ced00b240345dea932b5e32e4056f959b77051f10da85b06e6b6b6e4e191463e5105f55f1772d8117448f4b5511a2423475130ce26bdec44150b872116667791aea4c083f3b0abe44f19f841019911236179d280653dab71efc7fcd82bbeb56e5ede3bd7958a94b9a1ff082d82586ea876f6f3f059dec352b6c69ea86043137e547b62937e7a806a445528fa15b71e8cc987c587d3382ac00bd307b2c2ae5ff99f30bd1d1deb08976f9214788735d772b2ef04d827280e6dd836de156d1ac3a4eb530cb4c701ca69d51de13a6681de8d57dda2787a14bb5ebd934993596f6a98fcbd50339f0222da1df81247c5a0d620f6cb4f5ef311bdc66546bd3a5e2a5af0efa3c6158d7a6bb493ea89d8eaca4c07ec56189283118b7cadf32abbe1f65293973a1a55bae933f09feaaa3c6058083bec065f15fc795dc8e824f1ce941fa09f4d3512f4edd6fcc4c5b2f10d059087638d2bfcdd3f9e24383fd7466a1982226f8696abc5df1e51648fc165c46a0012c49e33a0de4ed6ba6215889cc923c8746407d38d856eee5c6f81ddd1b12d0511c79c0c6d0e88d09fc5f2f16e3f09b4c1754e26aa554840e707836cf3cedce57e463e0aa1a7be552d07594f3dd77cb0e76d9412faa917abdd50b3ce779d8c81019ec9d947f5717cee5a4e98c3e30e60fd4f34c66b29be5dd96d0ec235f6340464948976334d47a0e49198e77980acb97bcf77243f5c9eafdb8e742120065b8eb4348027a7dd251e9244ff3a2776d1783a449bcfb4dc4b3370362bb68ae3a61f842def75fcdb248634441092e25530db3b7bc2ce203231387d34c8195dbf293a2e680a2dff9b74e30f2f00ce4e84a309f1255ab55af3b2ce5b7a50712cd1d13ebcbb48dfa08b5edda1621ad30eb3e72562a2d3baeeb741514ddba8234d365e67ee35adc7a7902df9ac3f3f885da7a9fc4b6580d97f2dfbfd110b2fe313dd2b023ae1e98488742cad89bf33303ea235ce1bfb87577a04dfd3c1e1f952422bec2c5ef6a90aef6c39777d2c1fc283261b734fe19ff85777a63ddf6da7b6434e2ea8a2f6d0db84fff632a1d8e596fdb92b8d93a7a9a9d2a113032f96b1ba8ccc804c797f9ea6f65453c0ac308d10113abdab820817805547c0f1c392eacdf950a1bb88a85679461d2e2dff041037822062476311fd328173a9718934c3f2eeb8e6cff5316d44d5a870bb90407936c8188b9f8d4a26e92124a220f64dba55fd4ca34c39fc575899f8bba24d846fdd53ea0c0be00a9a5079406fbee0e83cdf8f360238e2e2c36b1c61b525c42a60d86bb79240bffc2fe6e0f5d46b09bc745ca7919a872461409298c61deb8648ed88774eb701ef565a0e1fa69bbefb87157441546e21371b98925e06d51124161b7ae678b84ce504360206a84f388557b99ee7774fdd0f411f300198c9abf5dc20a4f97d675dfa194e77266b7d8d47b101a8cac292d8b9945d1ad42d1878e8f5f35b0ec80281f02d7bfeb65226ad77704d7f6bc30fbcc6a4be54c8d162fe9529af1370f44ceb0da10ea36d389b6936fc9957f04a19ea796d2c5face0c8ee7bb23ff2db8a0b77a1cff4b169ece8d636663a3bcfd2d35bf44c251b8689338b76914d17f968ceff5e8de1719631729429371e5faf9cbb2067e891c658d0cb98276b16851b432b885d94d8efaa43e40ce4d09a97c1b7dca5d94d595629c44b41e75d2cda9bbcfef02adee54b5367a687428f005a1324eb8411f4468b0565ffc6b17953fc42a015e9acd9ae1e078a54ddd3b7355109c0eb56ab28263a02ec012993ebefa4060e1dd6347414681e634843baf9ee268fdbe7d00f22d05356231488042d08658f350b47c525d468bf63517758bad63c83821375d1c9f1165d7a3c829ac39a41200aabfbf03e8bde55c379e0f8ea8f81f59784212ec997612fa1aaed3f3ab82035cb761365982d4acb7e4713b9850511c3ad88b0844910e0968333ea3dba07068ad671208d1f4704e8d4d5b55b6b3ea7a4a70d2cffea0e8927bb215c1c628fbdaa6ea2d62d9fbcf274b56d7ffd5639ff2626b37ebba7f7add88dd978a5743d673e4ec7c82836de153d84ead5d378d290b2fffb2e5095496e7ca773d2db364896700ca09f0148df30d7c9ad70448503849831a428f602182eef6353db3c68f3100501e2575e31f658ad8fbe6f6f21f6d7dd369ecd1aab3f944d25ccb12c5fb2cecc39d32587b479f787482e38d60da3b15757816c4d0de0ac1dbfa4f6feab2e15be5b343227dbcc11fda877e9231d0d4cce4fdb0e9b73d0946fb60241613d51b658a2fd828d0931b1d2b7cdd50ee00c6aa454b0483747647b33d3eb9a4ce5f46755ea957202286a0e414940c570f7b8da652f3c7a0ebb28dcdfbc0a922f9f19bde6101563f898877ae298a51264cf5c692d97c62efab162d6265ea36aa59ffc86ba42d9f17979df28eb2f77045dbbd99d53e3d4553a9b8d4c7cec4d74f4f3d6a24129f7d48ef74dabd2804acaceb715fae1462446a06da637d196f19d02830a3f7ba8f0de5df1d080dfca719d6992317284a5d250c644e9dfd86930fd7224f199c4f52b7dfffc9767c003d066c43e980516a1bb2fcc92a3d5de9787b5f34e3bfee0ae38b97f84bc9f521b2f4e5c34787a8ea0a4c4daae98753a15af3481c41caa45b78cf98452cb7ec13ece08355d16be12f2c5cfc6aab958cb6273117bcff01a34940ef94ee8afa76360afac39aaeed3f54e7de726c3dc224de85cd92f57f3b105db6ad3e5f194fa33646984de36299958effd36bb1f9510b79f975bfdada10b334ab6a7f0f8337f084a7bc3db1e7af1b8d6b6a2e7061a80efb4ff5d7198175eba1c12e6acbf5092f0de03789a7de9b0cb0b385ed38f8db4c65628f674768ae91d256faa8152595d1270ca3d54249a3b1df84097f81d618297bb042750aca4f20d5a056689a5e8cebca0c3e36cc6333a16b9c7052517b5793552c1f625f8ec14a860cfdda306fafad0b77c47b47136f61ed8b9baf08ae974012c43bc00501f8044109125d5ebb450063fc6a954ab1bdee806882f8e04404ba28b6136c94f8f0a9fed264f5943e90d0c0edec69c668813d34d54bb6e43d90a9509d9dd7761431cf02109cefe77ff28cbd8915c116023f38e8457638dc11e9fef20ff2a6fb5623760981c8d267a18a64e19b1b58bf038a54dc90ea235346bd44ec79f37a20c6171ddbaab5276ca4dac6ebd79e5683727ed773ed29993b07de6905a295fb90d01f2f275d6a4cbcb1b6856593451083b988d1a0fb1bfd316e94e9f006a8011809413d695316d7477ee7dc327451aa7abfd1b91984ee887708153ffe1a4294aa561692db5d097dc9df1136c2bf29758760a422afb7e562d8be5769dd192c46aa95e3c8f9b6ec7d618b365137908987a75be43e511bb99f94936f1ab5fd6f5c93dcbaf8d0c017f6a24740b9cd49a1cb38a4e34397ce65265ed7276706d875a98adc60a7267a30e7fb39ee43fc930e697bc23972aecbe8c4c9059b8a43a26d4214d5162de2b80e8317b2877d2396164bd7240b411517ca0f57eed764e3319aee03e3e13570e75292ae6fb8c09a70f270325add11b3c7b0095ec721a35adf071b8db007d7b9a2408b62c1b17f0388250bfbee0dd7b9aef7eb8ce50df5bb18c441154a9da18773c57731a4345354ae6e03d4e863504e2adaffd618aa7aa44789269f83af69fb32a9cfc7fbdd6b316d03e2a9ce31509e05a07cb6c6ee6e127dd3b1c13d89e50f7407fc2e9ab99c1f6c7129589a1c77242392e1e08724fe2f6de96e87b4c4506a963e84eb3461bc9e0cab6094b7dd291e8e421823a6bc48d0d1a242fb8b0ca0a7b1f4124e4c262d5294b465eeaba288047cf6f24b26bf17dfdb70b09082384bad7c357a9ba33e7b97f290b540906b944240a88efbf89bc909a917f533d2bd3881847458dd8c58032949b43e21323af420a7d90c23977162d68627fa8f8f28659cbf497876b9dba0f2916b89f447d76167acf7ae5fa9769847feeef93deaa892e1e3ba4fbd3be85c9fad2ac7f403614c2a67d06ce3417529a8a866a22c5c10aa5bf07e195ad79556cc366ed3ab2d2f713d60e7e62a1af637aa9bec3ed96ff952fcd7162da28cbcc5999a38b926cee893a7704e325f0724ac383bb2d6e48e3c35d0bae557963a675674d3bcb058531b8b12d1c692b61078dd9bf9b2eeec94ce172b434941eafffb982dcc73e22812cabace33b2516fa45ec3eb82d2ce5b0204340f7f153d4ec65b2f42ce29e3dc5bb38531b666e4fed6485d2c3fecd536719bf758eade6c96086b258947dc694be765b3b9d8286852b18c25f15eca74ab745ac106bb91e2275b69b129cbd14b1c07348d0d157c90bbbfc11af6b4960fbc666cb3c9e4a3c408e6f595a67179e4ca2349c270ffa3d1581d8b9ebf6e9684067d358b6de326c0e6b890bc05e3175662f4c6f44e09ed3305ed04cb16d0635ee07377a106c24e42abeae5efa977446f88b7bb5def5aca2dbbf51e519521d018b5c668622e2b55e66b9b8fdfcde3b8491852907848ca86732810216ea19a9964842c6a3811078e6d1e73ac0d876d14272bd649dc6502816d2e20f7bbbc3026cbef63c6ae6ce07af82a832a06e634c59ca060b9b385a23a8c8bb2651a98f94ba138bf84e5f5a2e60c51962597df442438d9ff5447595d4919ccb4eb362fef3ba49fd94ab48a47f88e2ff009aa09f83ceb512524ea34ccbaf392492aa19ba75a480c16ea43beb1bfe8e1077f2735543aa1131daaf325e1895da07dd555f5a03d9387ee8d0ecdaa134950b7cb1a006587d3121060f97d3ca36e72dc9934a49f6fda29d7dd7fa6755379a0db48f97cd8bdf6ff252b08534b7d377a55bb91e616263467fd381ab0a12947c590cda2d43febbb45ba5fdd9a90d306c20e1b661e62c76232003ba0cde7da5e40e40b6b10cc9b9cca21e4f13daa7a05572e808b2205c37e0628566aef675caf08fa26ac299e866bb58c9fbc420acb2f02bfa6802da6fe81a065de34185dc38a6ec1ddd4b6aa188faa7f5c515614dd1c8bf6870eadf7d2595ddd56185b073be9f18f21285999c6c64a928572d7859e76520132b42398596d7f808392a2da2640546b6ea4c21d24520d59748ea4b2bdc10887a4cc15161acdd462639ab2cde2040e333543525f6c82dce50000000000000000000000000000000000000000000000080f121a1e232934" ++} +diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransaction.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransaction.java +index 4c7a60bdc..077f2be57 100644 +--- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransaction.java ++++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransaction.java +@@ -1,33 +1,39 @@ + /* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 ++ * ++ * Modifications Copyright contributors to the Aere Network. ++ * ++ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the ++ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was ++ * found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch. + */ + package org.hyperledger.besu.ethereum.eth.transactions; + + import static org.hyperledger.besu.ethereum.core.kzg.CKZG4844Helper.CELL_PROOFS_PER_BLOB; + import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.ACCESS_LIST_ENTRY_SHALLOW_SIZE; + import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.ACCESS_LIST_STORAGE_KEY_SIZE; + import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.BLOBS_WITH_COMMITMENTS_SIZE; + import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.BLOB_PROOF_BUNDLE_SIZE_V0; + import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.BLOB_PROOF_BUNDLE_SIZE_V1; + import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.CODE_DELEGATION_ENTRY_SIZE; + import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.EIP1559_AND_EIP4844_SHALLOW_SIZE; + import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.FRONTIER_AND_ACCESS_LIST_SHALLOW_SIZE; + import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.KZG_PROOF_CONTAINER_SHALLOW_SIZE; + import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.KZG_PROOF_SIZE; + import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.OPTIONAL_ACCESS_LIST_SHALLOW_SIZE; + import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.OPTIONAL_CHAIN_ID_SIZE; + import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.OPTIONAL_CODE_DELEGATION_LIST_SHALLOW_SIZE; + import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.OPTIONAL_SHALLOW_SIZE; + import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.OPTIONAL_TO_SIZE; + import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.PAYLOAD_SHALLOW_SIZE; +@@ -143,77 +149,91 @@ public abstract class PendingTransaction + + public void decrementScore() { + // use temp var to avoid non-atomic update of volatile var + final byte newScore = (byte) (score - 1); + + // check to avoid underflow + if (newScore < score) { + score = newScore; + } + } + + public abstract PendingTransaction detachedCopy(); + + private int computeMemorySize() { + return switch (transaction.getType()) { + case FRONTIER -> computeFrontierMemorySize(); + case ACCESS_LIST -> computeAccessListMemorySize(); + case EIP1559 -> computeEIP1559MemorySize(); + case BLOB -> computeBlobMemorySize(); + case DELEGATE_CODE -> computeDelegateCodeMemorySize(); ++ case AERE_PQ -> computeAerePqMemorySize(); + } + + PENDING_TRANSACTION_SHALLOW_SIZE; + } + + private int computeFrontierMemorySize() { + return FRONTIER_AND_ACCESS_LIST_SHALLOW_SIZE + + computePayloadMemorySize() + + computeToMemorySize() + + computeChainIdMemorySize(); + } + + private int computeAccessListMemorySize() { + return FRONTIER_AND_ACCESS_LIST_SHALLOW_SIZE + + computePayloadMemorySize() + + computeToMemorySize() + + computeChainIdMemorySize() + + computeAccessListEntriesMemorySize(); + } + + private int computeEIP1559MemorySize() { + return EIP1559_AND_EIP4844_SHALLOW_SIZE + + computePayloadMemorySize() + + computeToMemorySize() + + computeChainIdMemorySize() + + computeAccessListEntriesMemorySize(); + } + + private int computeBlobMemorySize() { + return computeEIP1559MemorySize() + + OPTIONAL_SHALLOW_SIZE // for the versionedHashes field + + computeBlobWithCommitmentsMemorySize(); + } + + private int computeDelegateCodeMemorySize() { + return computeEIP1559MemorySize() + computeCodeDelegationListMemorySize(); + } + ++ // AIP-20: an EIP-1559 shape plus the post-quantum authorization (record + two Bytes wrappers + the ++ // raw key and signature bytes, up to 7,219 of them for ML-DSA-87). ++ private static final int AERE_PQ_AUTHORIZATION_SHALLOW_SIZE = 64; ++ ++ private int computeAerePqMemorySize() { ++ return computeEIP1559MemorySize() ++ + OPTIONAL_SHALLOW_SIZE ++ + transaction ++ .getPqAuthorization() ++ .map(a -> AERE_PQ_AUTHORIZATION_SHALLOW_SIZE + a.authorizationByteCount()) ++ .orElse(0); ++ } ++ + private int computeBlobWithCommitmentsMemorySize() { + final int blobCount = transaction.getBlobCount(); + + return switch (transaction.getBlobsWithCommitments().get().getBlobType()) { + case KZG_PROOF -> + OPTIONAL_SHALLOW_SIZE + + BLOBS_WITH_COMMITMENTS_SIZE + + calculateListShallowSize(blobCount) // list of KZGProofBundle + + BLOB_PROOF_BUNDLE_SIZE_V0 * blobCount; + case KZG_CELL_PROOFS -> + OPTIONAL_SHALLOW_SIZE + + BLOBS_WITH_COMMITMENTS_SIZE + + calculateListShallowSize(blobCount) // list of KZGProofBundle + + (BLOB_PROOF_BUNDLE_SIZE_V1 + + KZG_PROOF_CONTAINER_SHALLOW_SIZE + + KZG_PROOF_SIZE * CELL_PROOFS_PER_BLOB) + * blobCount; + }; + } + +diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java +index fe525c431..1ad11b019 100644 +--- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java ++++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java +@@ -1,33 +1,39 @@ + /* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 ++ * ++ * Modifications Copyright contributors to the Aere Network. ++ * ++ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the ++ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was ++ * found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch. + */ + package org.hyperledger.besu.ethereum.eth.transactions; + + import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MAX_SCORE; + import static org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolStructuredLogUtils.logInvalid; + import static org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolStructuredLogUtils.logStart; + import static org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolStructuredLogUtils.logStop; + import static org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason.CHAIN_HEAD_NOT_AVAILABLE; + import static org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason.CHAIN_HEAD_WORLD_STATE_NOT_AVAILABLE; + import static org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason.INTERNAL_ERROR; + import static org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason.TRANSACTION_ALREADY_KNOWN; + import static org.hyperledger.besu.ethereum.trie.pathbased.common.provider.WorldStateQueryParams.withBlockHeaderAndNoUpdateNodeHead; + + import org.hyperledger.besu.datatypes.Address; + import org.hyperledger.besu.datatypes.Hash; + import org.hyperledger.besu.datatypes.TransactionType; + import org.hyperledger.besu.datatypes.VersionedHash; + import org.hyperledger.besu.datatypes.Wei; + import org.hyperledger.besu.ethereum.ProtocolContext; + import org.hyperledger.besu.ethereum.chain.BlockAddedEvent; +@@ -432,40 +438,49 @@ public class TransactionPool implements BlockAddedObserver { + // Strict replay protection is enabled but the tx is not replay-protected + return ValidationResultAndAccount.invalid( + TransactionInvalidReason.REPLAY_PROTECTED_SIGNATURE_REQUIRED); + } + if (transaction.getGasLimit() > chainHeadBlockHeader.getGasLimit()) { + return ValidationResultAndAccount.invalid( + TransactionInvalidReason.EXCEEDS_BLOCK_GAS_LIMIT, + String.format( + "Transaction gas limit of %s exceeds block gas limit of %s", + transaction.getGasLimit(), chainHeadBlockHeader.getGasLimit())); + } + if (transaction.getType().equals(TransactionType.EIP1559) && !feeMarket.implementsBaseFee()) { + return ValidationResultAndAccount.invalid( + TransactionInvalidReason.INVALID_TRANSACTION_FORMAT, + "EIP-1559 transaction are not allowed yet"); + } else if (transaction.getType().equals(TransactionType.BLOB) + && transaction.getBlobsWithCommitments().isEmpty()) { + return ValidationResultAndAccount.invalid( + TransactionInvalidReason.INVALID_BLOBS, "Blob transaction must have at least one blob"); + } ++ // AIP-20: not admitted to the pool before the activation height (the next block is judged). ++ if (transaction.getType().supportsPqAuthorization() ++ && !org.hyperledger.besu.ethereum.mainnet.AerePqTransactionSupport.isActiveAt( ++ chainHeadBlockHeader.getNumber() + 1)) { ++ return ValidationResultAndAccount.invalid( ++ TransactionInvalidReason.INVALID_TRANSACTION_FORMAT, ++ org.hyperledger.besu.ethereum.mainnet.AerePqTransactionSupport.inactiveMessage( ++ chainHeadBlockHeader.getNumber() + 1)); ++ } + + // Call the transaction validator plugin + final Optional maybePluginInvalid = + configuration + .getTransactionPoolValidatorService() + .createTransactionValidator() + .validateTransaction(transaction, isLocal, hasPriority); + if (maybePluginInvalid.isPresent()) { + return ValidationResultAndAccount.invalid( + TransactionInvalidReason.PLUGIN_TX_POOL_VALIDATOR, maybePluginInvalid.get()); + } + + try (final var worldState = + protocolContext + .getWorldStateArchive() + .getWorldState(withBlockHeaderAndNoUpdateNodeHead(chainHeadBlockHeader)) + .orElseThrow()) { + if (worldState instanceof BonsaiWorldState bonsaiWorldState) { + bonsaiWorldState.disableCacheMerkleTrieLoader(); + } +diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseTransactionPoolTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseTransactionPoolTest.java +index 86592933a..f3b958463 100644 +--- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseTransactionPoolTest.java ++++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseTransactionPoolTest.java +@@ -1,33 +1,39 @@ + /* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 ++ * ++ * Modifications Copyright contributors to the Aere Network. ++ * ++ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the ++ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was ++ * found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch. + */ + package org.hyperledger.besu.ethereum.eth.transactions.layered; + + import static org.assertj.core.api.Assertions.assertThat; + import static org.hyperledger.besu.ethereum.core.TransactionTestFixture.createSignedCodeDelegation; + import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MAX_SCORE; + import static org.mockito.ArgumentMatchers.any; + import static org.mockito.Mockito.mock; + import static org.mockito.Mockito.when; + + import org.hyperledger.besu.crypto.KeyPair; + import org.hyperledger.besu.crypto.SignatureAlgorithm; + import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; + import org.hyperledger.besu.datatypes.Address; + import org.hyperledger.besu.datatypes.BlobType; + import org.hyperledger.besu.datatypes.CodeDelegation; + import org.hyperledger.besu.datatypes.TransactionType; + import org.hyperledger.besu.datatypes.Wei; + import org.hyperledger.besu.ethereum.core.BlobTestFixture; + import org.hyperledger.besu.ethereum.core.Transaction; +@@ -173,40 +179,43 @@ public class BaseTransactionPoolTest extends TrustedSetupClassLoaderExtension { + + final TransactionType txType = TransactionType.values()[randomizeTxType.nextInt(4)]; + + return switch (txType) { + case FRONTIER, ACCESS_LIST, EIP1559 -> + createTransaction(txType, nonce, maxGasPrice, payloadSize, null, keys); + case BLOB -> + createTransaction( + txType, + nonce, + maxGasPrice, + maxGasPrice.divide(10), + payloadSize, + 1, + BlobType.KZG_PROOF, + null, + keys); + case DELEGATE_CODE -> + createTransaction( + txType, nonce, maxGasPrice, payloadSize, List.of(CODE_DELEGATION_SENDER_1), keys); ++ case AERE_PQ -> ++ throw new IllegalArgumentException( ++ "AIP-20 transactions need ML-DSA keys and are not generated by this test"); + }; + } + + protected Transaction createTransaction( + final TransactionType type, + final long nonce, + final Wei maxGasPrice, + final int payloadSize, + final List codeDelegations, + final KeyPair keys) { + return createTransaction( + type, + nonce, + maxGasPrice, + maxGasPrice.divide(10), + payloadSize, + 0, + BlobType.KZG_PROOF, + codeDelegations, + keys); +diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayersTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayersTest.java +index 1b48609ac..ccbf03c38 100644 +--- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayersTest.java ++++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayersTest.java +@@ -1,33 +1,39 @@ + /* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 ++ * ++ * Modifications Copyright contributors to the Aere Network. ++ * ++ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the ++ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was ++ * found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch. + */ + package org.hyperledger.besu.ethereum.eth.transactions.layered; + + import static org.assertj.core.api.Assertions.assertThat; + import static org.assertj.core.api.Assertions.fail; + import static org.awaitility.Awaitility.await; + import static org.hyperledger.besu.datatypes.TransactionType.ACCESS_LIST; + import static org.hyperledger.besu.datatypes.TransactionType.BLOB; + import static org.hyperledger.besu.datatypes.TransactionType.DELEGATE_CODE; + import static org.hyperledger.besu.datatypes.TransactionType.EIP1559; + import static org.hyperledger.besu.datatypes.TransactionType.FRONTIER; + import static org.hyperledger.besu.ethereum.core.TransactionTestFixture.createSignedCodeDelegation; + import static org.hyperledger.besu.ethereum.eth.transactions.layered.LayeredRemovalReason.PoolRemovalReason.INVALIDATED; + import static org.hyperledger.besu.ethereum.eth.transactions.layered.LayersTest.AuthorityAndNonce.NO_DELEGATIONS; + import static org.hyperledger.besu.ethereum.eth.transactions.layered.LayersTest.AuthorityAndNonce.delegation; + import static org.hyperledger.besu.ethereum.eth.transactions.layered.LayersTest.AuthorityAndNonce.toCodeDelegations; + import static org.hyperledger.besu.ethereum.eth.transactions.layered.LayersTest.Sender.S1; + import static org.hyperledger.besu.ethereum.eth.transactions.layered.LayersTest.Sender.S2; + import static org.hyperledger.besu.ethereum.eth.transactions.layered.LayersTest.Sender.S3; + import static org.hyperledger.besu.ethereum.eth.transactions.layered.LayersTest.Sender.S4; +@@ -1789,40 +1795,43 @@ public class LayersTest extends BaseTransactionPoolTest { + return this; + } + + private PendingTransaction create( + final Sender sender, + final TransactionType type, + final AuthorityAndNonce[] authorityAndNonces, + final long nonce) { + if (liveTxsBySender.get(sender).containsKey(nonce)) { + fail( + "Transaction for sender " + sender.name() + " with nonce " + nonce + " already exists"); + } + final var newPendingTx = + switch (type) { + case FRONTIER -> createFrontierPendingTransaction(sender, nonce); + case ACCESS_LIST -> createAccessListPendingTransaction(sender, nonce); + case EIP1559 -> createEIP1559PendingTransaction(sender, nonce); + case BLOB -> createBlobPendingTransaction(sender, nonce); + case DELEGATE_CODE -> + createEIP7702PendingTransaction(sender, nonce, authorityAndNonces); ++ case AERE_PQ -> ++ throw new IllegalArgumentException( ++ "AIP-20 transactions need ML-DSA keys and are not generated by this test"); + }; + liveTxsBySender.get(sender).put(nonce, newPendingTx); + return newPendingTx; + } + + private PendingTransaction replace(final Sender sender, final PendingTransaction pendingTx) { + final var replaceTx = + createRemotePendingTransaction( + createTransactionReplacement(pendingTx.getTransaction(), sender.key), + sender.hasPriority); + liveTxsBySender.get(sender).replace(pendingTx.getNonce(), replaceTx); + return replaceTx; + } + + private Optional getMaybe(final Sender sender, final long nonce) { + return Optional.ofNullable(liveTxsBySender.get(sender).get(nonce)); + } + + private PendingTransaction get(final Sender sender, final long nonce) { + return getMaybe(sender, nonce).get(); +diff --git a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/FrontierGasCalculator.java b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/FrontierGasCalculator.java +index 52771a088..4dd3f52d5 100644 +--- a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/FrontierGasCalculator.java ++++ b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/FrontierGasCalculator.java +@@ -1,33 +1,39 @@ + /* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 ++ * ++ * Modifications Copyright contributors to the Aere Network. ++ * ++ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the ++ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was ++ * found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch. + */ + package org.hyperledger.besu.evm.gascalculator; + + import static org.hyperledger.besu.evm.internal.Words.clampedAdd; + import static org.hyperledger.besu.evm.internal.Words.clampedMultiply; + import static org.hyperledger.besu.evm.internal.Words.clampedToInt; + import static org.hyperledger.besu.evm.internal.Words.numWords; + + import org.hyperledger.besu.datatypes.Address; + import org.hyperledger.besu.datatypes.Transaction; + import org.hyperledger.besu.datatypes.Wei; + import org.hyperledger.besu.evm.account.Account; + import org.hyperledger.besu.evm.frame.MessageFrame; + import org.hyperledger.besu.evm.operation.ExpOperation; + + import java.util.function.Supplier; + + import org.apache.tuweni.bytes.Bytes; + import org.apache.tuweni.units.bigints.UInt256; + +@@ -139,40 +145,53 @@ public class FrontierGasCalculator implements GasCalculator { + return clampedAdd(getMinimumTransactionCost(), dynamicIntrinsicGasCost); + } + + /** + * Calculates the dynamic part of the intrinsic gas cost + * + * @param transaction the transaction + * @param baselineGas how much gas is used by access lists and code delegations + * @return the dynamic part of the intrinsic gas cost + */ + protected long dynamicIntrinsicGasCost(final Transaction transaction, final long baselineGas) { + final int payloadSize = transaction.getPayload().size(); + + long cost = + clampedAdd(callDataCost(payloadSize, transaction.getPayloadZeroBytes()), baselineGas); + + if (cost == Long.MIN_VALUE || cost == Long.MAX_VALUE) { + return cost; + } + ++ // AIP-20: the post-quantum authorization bytes are charged like non-zero calldata (16 per byte) ++ // and the verification itself at the algorithm's flat cost. Zero for every other type. ++ final long pqCost = ++ transaction ++ .getPqAuthorization() ++ .map(a -> clampedAdd(16L * a.authorizationByteCount(), a.algorithm().verifyGas())) ++ .orElse(0L); ++ cost = clampedAdd(cost, pqCost); ++ ++ if (cost == Long.MIN_VALUE || cost == Long.MAX_VALUE) { ++ return cost; ++ } ++ + if (transaction.isContractCreation()) { + cost = clampedAdd(cost, contractCreationCost(payloadSize)); + + if (cost == Long.MIN_VALUE || cost == Long.MAX_VALUE) { + return cost; + } + } + + return cost; + } + + /** + * Calculates the cost of the call data + * + * @param payloadSize the total size of the payload + * @param zeroBytes the number of zero bytes in the payload + * @return the cost of the call data + */ + protected long callDataCost(final long payloadSize, final long zeroBytes) { + return clampedAdd( diff --git a/patches/0007-aere-pq-finality.patch b/patches/0007-aere-pq-finality.patch new file mode 100644 index 0000000..bf0fba9 --- /dev/null +++ b/patches/0007-aere-pq-finality.patch @@ -0,0 +1,2065 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aere Network +Date: Tue, 22 Sep 2026 22:21:16 +0000 +Subject: [PATCH] Aere Network: AIP-21, verifiable post-quantum finality of every block + +AERE patch 0007: AIP-21, verifiable post-quantum finality of every block + +Why this patch exists: since 2026-09-22 every validator of chain 2800 keeps the +Falcon-512 seals it hears on QBFT Commit messages in a window on disk and answers +the JSON-RPC method aere_getPqFinality for any block in that window: which distinct +validators sealed that block hash with a post-quantum signature, re-verified now +against the registry epoch of that height, and whether they reach the commit quorum. +A node built without this patch follows the chain exactly as before (nothing here +changes block validity, so there is no activation height), but it cannot answer the +method and it does not keep the window. The window survives a restart (segments of +64 heights under /aere-pq-seal-window, each frame bound to the canonical block +hash and re-verified on restore); aere.pq.finality.persist=false disables the +journal. + +What it changes, as one delta over the tree produced by patches 0001 to 0006: the +seal cache of the BFT layer (PqSealCache: keyed by the on-chain block hash, with a +retention window instead of a fixed depth), the QBFT controller builder (wiring of +the window store), the QBFT JSON-RPC method set and RpcMethod (the new method), plus +the new files PqSealWindowStore, AereGetPqFinality and their tests. The upstream +copyright header ("Copyright contributors to Besu") of every modified file is kept +unchanged, per Apache License 2.0 section 4(c); the two upstream files this patch +modifies that had no notice yet (RpcMethod, QbftJsonRpcMethods) receive, in this diff, +the in-file modification notice that section 4(b) requires, QbftBesuControllerBuilder +already carries it from patch 0003, and PqSealCache is an Aere Network file introduced +by patch 0003 (section 4(b) does not apply to it); the changed files are also listed +here and the holder is named in NOTICE, per section 4(d). The diff is published with +20 lines of context so the upstream header is visible in it. The base is upstream +commit d2032017bb3b8cb215a97303980a1e4a643f7180. + +How it was measured, 2026-09-22: see patch 0006; the same build reproduces the fleet +distribution wt3f12ef0d class by class on consensus and on these files. +Specification: aips/AIP-21.md; the two-client conformance record is in +aips/aip21-conformitate/. + +Apache License 2.0, section 4(b): the files this patch modifies that existed before it +(upstream files of Hyperledger Besu, or Aere Network files introduced by patch 0003, all +with their copyright notices kept unchanged) are, and this notice is the statement that +contributors to the Aere Network changed them: + app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java + consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealCache.java + consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/jsonrpc/QbftJsonRpcMethods.java + ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java + +The in-file "Modifications Copyright" blocks for the upstream files among them are not in +this delta on purpose: the delta is exact to the fleet distribution wt3f12ef0d, and the +notices will enter the production sources with the next fleet distribution. +--- +diff --git a/app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java b/app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java +index 281add4c9..252f65583 100644 +--- a/app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java ++++ b/app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java +@@ -470,40 +470,64 @@ public class QbftBesuControllerBuilder extends BesuControllerBuilder { + PqSealCache.instance() + .restoreFromDisk( + aereCapSigilii.getNumber(), + aereCapSigilii.getHash(), + PqSignerRegistry.falconSealSupport()); + LOG.info( + "AERE PERSISTENTA-SIGILII: {} VERIFIED seal(s) restored for chain head {} ({}). The " + + "threshold demanded at the next height is K={}. Zero means this node cannot " + + "propose until it hears a Commit, which after a fleet restart with K>0 never " + + "happens again on its own.", + aereSigiliiRestaurate, + aereCapSigilii.getNumber(), + aereCapSigilii.getHash(), + aereAnchorConfig.minSealsAt(aereCapSigilii.getNumber() + 1L)); + } catch (final Exception e) { + LOG.warn( + "AERE PERSISTENTA-SIGILII: restoring the chain head seals could not be done ({}); the " + + "node starts with an empty cache, exactly as before this repair.", + e.toString()); + } ++ // AIP-21, SECOND HALF (2026-09-18): the finality WINDOW survives the restart too. Same ++ // security argument as above, one step further: a journal frame counts only if its hash is ++ // the canonical hash THIS chain holds at that height, and each seal only if it verifies now ++ // against the registry epoch bound there. A forged journal buys an empty window. ++ try { ++ final BlockHeader aereCapFereastra = blockchain.getChainHeadHeader(); ++ final int aereFereastraRestaurata = ++ PqSealCache.instance() ++ .restoreWindowFromDisk( ++ aereCapFereastra.getNumber(), ++ aereInaltime -> ++ blockchain.getBlockHeader(aereInaltime).map(BlockHeader::getHash), ++ PqSignerRegistry.falconSealSupport()); ++ LOG.info( ++ "AERE FEREASTRA-SIGILII (AIP-21): {} VERIFIED seal(s) restored into the finality window " ++ + "below chain head {}; aere_getPqFinality answers for those blocks again.", ++ aereFereastraRestaurata, ++ aereCapFereastra.getNumber()); ++ } catch (final Exception e) { ++ LOG.warn( ++ "AERE FEREASTRA-SIGILII (AIP-21): the finality window could not be restored ({}); the " ++ + "node answers 'unavailable' for blocks heard before this restart, as before.", ++ e.toString()); ++ } + } + + final BftExecutors bftExecutors = + BftExecutors.create(metricsSystem, BftExecutors.ConsensusType.QBFT); + final QbftBlockCodec blockEncoder = new QbftBlockCodecAdaptor(qbftExtraDataCodec); + + final Address localAddress = Util.publicKeyToAddress(nodeKey.getPublicKey()); + final BftProtocolSchedule bftProtocolSchedule = (BftProtocolSchedule) protocolSchedule; + QbftProtocolSchedule qbftProtocolSchedule = + new QbftProtocolScheduleAdaptor(bftProtocolSchedule, protocolContext); + final QbftBlockCreatorFactory blockCreatorFactory = + new QbftBlockCreatorFactory( + transactionPool, + protocolContext, + bftProtocolSchedule, + qbftForksSchedule, + miningConfiguration, + localAddress, + qbftExtraDataCodec, + ethProtocolManager.ethContext().getScheduler()); +diff --git a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealCache.java b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealCache.java +index dac0d63dd..716c67e0f 100755 +--- a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealCache.java ++++ b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealCache.java +@@ -52,104 +52,146 @@ import org.slf4j.LoggerFactory; + * amount of waiting healed it. The seals over M(head) exist nowhere else: they travel only on the + * Commit messages of the head block, and those are never replayed. + * + *

AND IT IS NOT DEFECT A8 IN ANOTHER COAT. A8 was a registry of public keys read from a file and + * BELIEVED. Every seal read back here is re-verified, cryptographically, against the anchored + * registry over M rebuilt from the header this process just loaded - see {@link PqSealStore}. A + * forged file cannot inject a seal without forging a Falcon-512 signature; the worst it achieves is + * the empty cache an absent file already gives. Persistence is OFF unless a caller enables it, and + * the only caller that does is the QBFT controller builder, only when the anchor is actually armed. + * + *

WHAT IT DOES NOT DO. The in-memory path verifies nothing. Whether a seal is valid, whether its + * index maps to an eligible validator, and whether there are enough of them, are decided at + * SELECTION time by the producer, against the message M of the specific parent. A seal that was + * signed under the pre-fork message simply fails that check and is dropped, which is what makes the + * height switch safe without any coordination beyond the shared activation height. + */ + public final class PqSealCache { + + private static final Logger LOG = LoggerFactory.getLogger(PqSealCache.class); + +- /** How many distinct block hashes are retained. Several rounds at one height each add one. */ +- private static final int MAX_ENTRIES = 1024; ++ /** ++ * AIP-21 (AERE, 2026-09-18): how far below the highest seen height entries are kept, in blocks. The ++ * consensus default stays 256 (what the producer needs is the parent); a node that serves ++ * aere_getPqFinality may keep more through the system property, at ~7 kB per block for ten seals. ++ * Read once, never below 256, never a consensus value: two nodes with different windows import ++ * the same chain. ++ */ ++ public static final String PROPERTY_FINALITY_WINDOW = "aere.pq.finality.window"; ++ ++ private static final long DEFAULT_HEIGHT_WINDOW = 256L; + + /** How far below the highest seen height entries are kept, in blocks. */ +- private static final long HEIGHT_WINDOW = 256L; ++ private static final long HEIGHT_WINDOW = readWindow(); ++ ++ /** How many distinct block hashes are retained. Several rounds at one height each add one. */ ++ private static final int MAX_ENTRIES = (int) Math.max(1024L, Math.min(HEIGHT_WINDOW * 4L, 1 << 20)); ++ ++ private static long readWindow() { ++ final Long v = Long.getLong(PROPERTY_FINALITY_WINDOW); ++ if (v == null) { ++ return DEFAULT_HEIGHT_WINDOW; ++ } ++ return Math.max(DEFAULT_HEIGHT_WINDOW, Math.min(v, 1L << 18)); ++ } ++ ++ /** ++ * The retention window in blocks, as this node runs it. ++ * ++ * @return the window ++ */ ++ public long retentionBlocks() { ++ return HEIGHT_WINDOW; ++ } + + private static final PqSealCache INSTANCE = new PqSealCache(); + + private final LinkedHashMap byHash = new LinkedHashMap<>(); + private long highestSeen = -1L; + + /** Guards the file, never the map, so a slow disk cannot hold up the consensus thread's map. */ + private final Object writeLock = new Object(); + + private volatile Path persistenceFile; + private volatile long persistenceChainId; + + private long persistedBlock = -1L; + private Hash persistedHash; + private int persistedCount = -1; + ++ // AIP-21, SECOND HALF (2026-09-18): the finality window survives a restart. Heard heights are ++ // appended to a segmented journal (PqSealWindowStore) when the NEXT height begins, and read back, ++ // re-verified seal by seal against the canonical chain, at startup. Null = journal off. ++ private volatile Path windowDirectory; ++ private volatile long windowPrunedSegment = -1L; ++ private long windowJournaledThrough = -1L; ++ ++ /** Heights restored at startup at most, newest first, whatever the configured window. */ ++ private static final long MAX_RESTORED_HEIGHTS = 4096L; ++ + private PqSealCache() {} + + /** + * The process-wide cache. + * + * @return the singleton + */ + public static PqSealCache instance() { + return INSTANCE; + } + + /** + * Turn on writing the chain head's seals to the node's data directory. + * + *

The path is DERIVED from the data directory Besu was started with, never configured + * separately: a second configurable path is a second thing that can be pointed at the wrong node's + * state, and this file is per-node by construction. + * + * @param dataDirectory the node's data directory, or null to leave persistence off + * @param chainId the chain id used in the M pre-image, which must be the SAME value the producer + * uses or every restored seal would fail verification + */ + public void enablePersistence(final Path dataDirectory, final long chainId) { + if (dataDirectory == null) { + LOG.warn( + "AERE PERSISTENTA-SIGILII: no data directory was supplied, so heard seals stay in memory " + + "only. A simultaneous restart of the whole fleet with K>0 stops the chain."); + return; + } + this.persistenceChainId = chainId; + this.persistenceFile = PqSealStore.fileIn(dataDirectory); ++ this.windowPrunedSegment = -1L; ++ this.windowDirectory = ++ PqSealWindowStore.enabled() ? PqSealWindowStore.directoryIn(dataDirectory) : null; + LOG.info( + "AERE PERSISTENTA-SIGILII: heard seals for the chain head are kept in {} (chainId={}). " + + "Every seal read back is re-verified against the anchored registry before it is used.", + this.persistenceFile, + chainId); + } + + /** + * Turn persistence off again. For tests, and for an operator who wants the previous behaviour. + */ + public void disablePersistence() { + this.persistenceFile = null; ++ this.windowDirectory = null; + synchronized (writeLock) { + persistedBlock = -1L; + persistedHash = null; + persistedCount = -1; + } + } + + /** + * The file heard seals are written to, or null when persistence is off. + * + * @return the seal file, or null + */ + public Path persistenceFile() { + return persistenceFile; + } + + /** + * Refill the cache from disk for a specific block, normally this node's chain head at startup. + * + *

Call this AFTER the Falcon registry has been activated, or every seal will be rejected for +@@ -209,65 +251,71 @@ public final class PqSealCache { + } + if (blockNumber > highestSeen) { + highestSeen = blockNumber; + } + prune(); + } + return verified.size(); + } + + /** + * Remember the seals heard for a block. Idempotent per validator index: the first seal seen for + * an index wins, so a later duplicate cannot displace it. + * + * @param blockNumber the height of the block the seals attest + * @param onchainBlockHash the ON-CHAIN hash of that block + * @param seals the seals heard, possibly empty + */ + public void record( + final long blockNumber, final Hash onchainBlockHash, final Collection seals) { + final List toPersist; ++ List windowToJournal = List.of(); + synchronized (this) { + if (onchainBlockHash == null || seals == null || seals.isEmpty()) { + return; + } + final Entry entry = + byHash.computeIfAbsent(onchainBlockHash, unused -> new Entry(blockNumber)); + for (final FalconSeal seal : seals) { + if (seal != null && seal.getValidatorIndex() >= 0 && seal.getSignature() != null) { + entry.seals.putIfAbsent(seal.getValidatorIndex(), seal); + } + } + if (blockNumber > highestSeen) { ++ // AIP-21 second half: a NEW height begins, so what was heard for the heights below it is as ++ // complete as this node will ever hold it. It goes to the window journal, outside this lock. ++ windowToJournal = windowSnapshotBelow(blockNumber); + highestSeen = blockNumber; + } + prune(); + if (persistenceFile == null || blockNumber < highestSeen) { + // Only the HIGHEST block is worth a file: the producer of the next block asks for the head's + // seals and nothing else, and writing older heights would just churn the disk. + return; + } + toPersist = PqAnchor.sortedByIndex(new ArrayList<>(entry.seals.values())); + } + // Deliberately OUTSIDE the map lock: a file write must never be able to stall a caller that only + // wants to read the cache. + persist(blockNumber, onchainBlockHash, toPersist); ++ // the head file first (liveness depends on it), the window journal after (best effort) ++ persistWindow(windowToJournal); + } + + private void persist( + final long blockNumber, final Hash onchainBlockHash, final List seals) { + final Path file = persistenceFile; + if (file == null) { + return; + } + synchronized (writeLock) { + if (blockNumber < persistedBlock) { + return; + } + if (blockNumber == persistedBlock + && onchainBlockHash.equals(persistedHash) + && seals.size() <= persistedCount) { + return; + } + try { + // MAGAZIA TINE VARFUL (D-330, 2026-09-03): before the main file moves to a HIGHER height, what it + // held becomes the previous slot. The main file may hold a proposal that never imports; the +@@ -379,47 +427,199 @@ public final class PqSealCache { + } + + /** + * How many seals are retained for a block hash. + * + * @param onchainBlockHash the ON-CHAIN hash + * @return the seal count, 0 when nothing is retained + */ + public synchronized int sealCount(final Hash onchainBlockHash) { + final Entry entry = byHash.get(onchainBlockHash); + return entry == null ? 0 : entry.seals.size(); + } + + /** + * Drop everything held in memory. For tests, and for a deliberate operator reset. The file, if + * one exists, is deliberately left alone: it is rewritten by the next commit this node hears. + */ + public synchronized void clear() { + byHash.clear(); + highestSeen = -1L; ++ windowJournaledThrough = -1L; // what clear() forgets, the journal mark forgets too + synchronized (writeLock) { + persistedBlock = -1L; + persistedHash = null; + persistedCount = -1; + } + } + ++ /** ++ * AIP-21, second half. Everything heard BELOW a height that has just begun and not yet handed to ++ * the window journal. Caller holds the map lock. ++ * ++ * @param newHeight the height that has just begun ++ * @return the heard blocks to journal, possibly empty ++ */ ++ private List windowSnapshotBelow(final long newHeight) { ++ if (windowDirectory == null) { ++ return List.of(); ++ } ++ final List out = new ArrayList<>(); ++ for (final Map.Entry e : byHash.entrySet()) { ++ final long n = e.getValue().blockNumber; ++ if (n > windowJournaledThrough && n < newHeight && !e.getValue().seals.isEmpty()) { ++ out.add(PqSealWindowStore.heard(n, e.getKey().getBytes(), e.getValue().seals.values())); ++ } ++ } ++ windowJournaledThrough = Math.max(windowJournaledThrough, newHeight - 1L); ++ return out; ++ } ++ ++ private void persistWindow(final List heard) { ++ final Path directory = windowDirectory; ++ if (directory == null || heard.isEmpty()) { ++ return; ++ } ++ synchronized (writeLock) { ++ try { ++ long highest = -1L; ++ for (final PqSealWindowStore.Heard h : heard) { ++ PqSealWindowStore.append(directory, persistenceChainId, h); ++ highest = Math.max(highest, h.blockNumber()); ++ } ++ final long segment = highest / PqSealWindowStore.SEGMENT_BLOCKS; ++ if (segment != windowPrunedSegment) { ++ PqSealWindowStore.pruneSegments(directory, highest, HEIGHT_WINDOW); ++ windowPrunedSegment = segment; ++ } ++ } catch (final Exception e) { ++ // A disk fault must not be able to stop this node from taking part in consensus. It costs the ++ // answer aere_getPqFinality gives for these blocks after a restart, and nothing else. ++ LOG.warn( ++ "AERE FEREASTRA-SIGILII: could not append {} heard block(s) to the window journal {} ({}). " ++ + "This node keeps working; after a restart it answers 'unavailable' for them.", ++ heard.size(), ++ directory, ++ e.toString()); ++ } ++ } ++ } ++ ++ /** ++ * AIP-21, second half. Bring the finality window back after a restart. Nothing in the journal is ++ * believed: a frame counts only if its hash is the CANONICAL hash this node's own chain holds at ++ * that height, and each seal only if it verifies, against the registry epoch bound at that height, ++ * over the commit message rebuilt from that canonical hash. Never throws. ++ * ++ * @param headNumber this node's chain head ++ * @param canonicalHashAt the canonical on-chain hash at a height, empty when the chain holds none ++ * @param registry the anchored registry seals are verified against ++ * @return how many verified seals entered the window ++ */ ++ public int restoreWindowFromDisk( ++ final long headNumber, ++ final java.util.function.LongFunction> canonicalHashAt, ++ final PqSignerRegistry registry) { ++ final Path directory = windowDirectory; ++ if (directory == null || canonicalHashAt == null || registry == null) { ++ return 0; ++ } ++ int restored = 0; ++ try { ++ final long from = Math.max(0L, headNumber - Math.min(HEIGHT_WINDOW, MAX_RESTORED_HEIGHTS)); ++ final List frames = ++ PqSealWindowStore.readAll(directory, persistenceChainId, from, headNumber); ++ long highestInJournal = -1L; ++ int notCanonical = 0; ++ int rejected = 0; ++ final LinkedHashMap verified = new LinkedHashMap<>(); ++ for (final PqSealWindowStore.Heard frame : frames) { ++ final long n = frame.blockNumber(); ++ highestInJournal = Math.max(highestInJournal, n); ++ final java.util.Optional canonical = canonicalHashAt.apply(n); ++ if (canonical == null ++ || canonical.isEmpty() ++ || !canonical.get().getBytes().equals(frame.blockHash())) { ++ notCanonical++; ++ continue; ++ } ++ final org.apache.tuweni.bytes.Bytes32 message = ++ PqAnchor.commitMessage(persistenceChainId, n, frame.blockHash()); ++ for (final FalconSeal seal : frame.seals()) { ++ if (seal == null ++ || seal.getValidatorIndex() < 0 ++ || seal.getSignature() == null ++ || !registry.verifyAtHistoric( ++ n, seal.getValidatorIndex(), message, seal.getSignature())) { ++ rejected++; ++ continue; ++ } ++ final Entry entry = verified.computeIfAbsent(canonical.get(), unused -> new Entry(n)); ++ if (entry.seals.putIfAbsent(seal.getValidatorIndex(), seal) == null) { ++ restored++; ++ } ++ } ++ } ++ synchronized (this) { ++ // Oldest first, so the size cap still evicts the oldest: the restored heights go in BEFORE ++ // what this node already holds (its head, restored a moment ago from the head file). ++ final LinkedHashMap held = new LinkedHashMap<>(byHash); ++ byHash.clear(); ++ byHash.putAll(verified); ++ for (final Map.Entry e : held.entrySet()) { ++ final Entry target = byHash.get(e.getKey()); ++ if (target == null) { ++ byHash.put(e.getKey(), e.getValue()); ++ } else { ++ e.getValue().seals.forEach(target.seals::putIfAbsent); ++ e.getValue().extras.forEach(target.extras::putIfAbsent); ++ } ++ } ++ for (final Entry entry : verified.values()) { ++ if (entry.blockNumber > highestSeen) { ++ highestSeen = entry.blockNumber; ++ } ++ } ++ windowJournaledThrough = Math.max(windowJournaledThrough, highestInJournal); ++ prune(); ++ } ++ LOG.info( ++ "AERE FEREASTRA-SIGILII: {} verified seal(s) over {} block(s) restored into the finality " ++ + "window from {} ({} frame(s) read, {} not canonical on this chain, {} seal(s) FAILED " ++ + "verification and were dropped).", ++ restored, ++ verified.size(), ++ directory, ++ frames.size(), ++ notCanonical, ++ rejected); ++ } catch (final RuntimeException e) { ++ LOG.warn( ++ "AERE FEREASTRA-SIGILII: restoring the finality window from {} failed unexpectedly ({}); " ++ + "the window starts empty, exactly as before this journal existed.", ++ directory, ++ e.toString()); ++ return 0; ++ } ++ return restored; ++ } ++ + private void prune() { + final Iterator> it = byHash.entrySet().iterator(); + while (it.hasNext()) { + final Map.Entry e = it.next(); + if (highestSeen - e.getValue().blockNumber > HEIGHT_WINDOW) { + it.remove(); + } + } + while (byHash.size() > MAX_ENTRIES) { + final Iterator oldest = byHash.keySet().iterator(); + oldest.next(); + oldest.remove(); + } + } + + private static final class Entry { + private final long blockNumber; + private final Map seals = new LinkedHashMap<>(); + /** AERE ANCHOR V2: heard extra (non-Falcon) scheme seals, keyed (index << 8 | wire). */ + private final Map extras = new LinkedHashMap<>(); +diff --git a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealWindowStore.java b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealWindowStore.java +new file mode 100755 +index 000000000..b224d8fb2 +--- /dev/null ++++ b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealWindowStore.java +@@ -0,0 +1,387 @@ ++/* ++ * Copyright contributors to Besu / AERE Network. ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with ++ * the License. You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on ++ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the ++ * specific language governing permissions and limitations under the License. ++ * ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++package org.hyperledger.besu.consensus.common.bft; ++ ++import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput; ++import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; ++import org.hyperledger.besu.ethereum.rlp.RLPInput; ++ ++import java.io.IOException; ++import java.nio.ByteBuffer; ++import java.nio.channels.FileChannel; ++import java.nio.charset.StandardCharsets; ++import java.nio.file.DirectoryStream; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.nio.file.StandardOpenOption; ++import java.util.ArrayList; ++import java.util.Collection; ++import java.util.Comparator; ++import java.util.List; ++import java.util.regex.Matcher; ++import java.util.regex.Pattern; ++import java.util.zip.CRC32; ++ ++import org.apache.tuweni.bytes.Bytes; ++import org.slf4j.Logger; ++import org.slf4j.LoggerFactory; ++ ++/** ++ * AIP-21, SECOND HALF (2026-09-18): the finality window on disk. ++ * ++ *

The first half gave every validator a window of heard Falcon seals ({@link PqSealCache}) and a ++ * method that reads it back ({@code aere_getPqFinality}). The window lived in memory only, so a node ++ * that restarted answered {@code unavailable} for every block it had heard before the restart. This ++ * class is the journal that carries the window across a restart. ++ * ++ *

SHAPE. One directory under the node's data directory, {@value #DIRECTORY_NAME}, holding ++ * SEGMENT files of {@value #SEGMENT_BLOCKS} heights each ({@code w-.rlp}). A segment is ++ * a sequence of FRAMES: {@code length(4, big endian) || payload || crc32(payload)(4)}, the payload ++ * being the RLP list {@code [DOMAIN, chainId, blockNumber, blockHash, certificate]} with the ++ * certificate encoded exactly as {@link PqSealStore} and the header encode it. Frames are only ever ++ * APPENDED, one per heard block, when the next height begins; whole segments below the window are ++ * deleted. The footprint is therefore bounded by the window, whatever the age of the node. ++ * ++ *

WHY THIS IS NOT A8 IN ANOTHER COAT, the same argument as {@link PqSealStore}: a seal is ++ * SELF-VERIFYING. Nothing read from this directory is believed. {@link PqSealCache} re-verifies ++ * every seal against the registry epoch bound at its height, over the message rebuilt from the ++ * CANONICAL hash this process reads from its own chain at that height; a frame for a hash that is ++ * not canonical is dropped before a single signature is looked at. A forged journal can obtain the ++ * empty window an absent journal already gives, and nothing else. ++ * ++ *

WHAT A FAULT COSTS. Every method here is written not to throw on bad input: a torn tail (the ++ * node died mid-append), a flipped byte, a directory where a file should be, a journal from another ++ * chain. A bad frame costs that frame and what FOLLOWS it in the same segment (a frame boundary ++ * cannot be trusted after a bad length), never the node. Writes are best effort and unsynced: the ++ * head file of {@link PqSealStore} is the one liveness depends on, and it keeps its fsync. ++ */ ++public final class PqSealWindowStore { ++ ++ private static final Logger LOG = LoggerFactory.getLogger(PqSealWindowStore.class); ++ ++ /** Directory name under the node's data directory. */ ++ public static final String DIRECTORY_NAME = "aere-pq-seal-window"; ++ ++ /** Domain separator of a frame payload. */ ++ public static final String DOMAIN = "AERE-PQ-SEALWINDOW-1"; ++ ++ private static final Bytes DOMAIN_BYTES = Bytes.wrap(DOMAIN.getBytes(StandardCharsets.US_ASCII)); ++ ++ /** Heights per segment file. */ ++ public static final long SEGMENT_BLOCKS = 64L; ++ ++ /** A segment larger than this is ignored whole. */ ++ public static final long MAX_SEGMENT_BYTES = 64L * 1024L * 1024L; ++ ++ /** A frame payload larger than this ends the reading of its segment. */ ++ public static final int MAX_FRAME_BYTES = 4 * 1024 * 1024; ++ ++ /** More seals than this in one frame is not a certificate. */ ++ public static final int MAX_SEALS = 1024; ++ ++ /** System property: {@code false} turns the journal off (the window is then memory only). */ ++ public static final String PROPERTY_PERSIST = "aere.pq.finality.persist"; ++ ++ private static final Pattern SEGMENT_NAME = Pattern.compile("^w-(\\d{1,18})\\.rlp$"); ++ ++ private PqSealWindowStore() {} ++ ++ /** ++ * One heard block as the journal holds it. Unverified: the caller verifies. ++ * ++ * @param blockNumber the height ++ * @param blockHash the round-independent on-chain hash the seals were heard for ++ * @param seals the heard seals, index-sorted ++ */ ++ public record Heard(long blockNumber, Bytes blockHash, List seals) {} ++ ++ /** ++ * Whether the journal is enabled for this process. ++ * ++ * @return false only when {@value #PROPERTY_PERSIST} is {@code false} ++ */ ++ public static boolean enabled() { ++ return !"false".equalsIgnoreCase(System.getProperty(PROPERTY_PERSIST)); ++ } ++ ++ /** ++ * The journal directory of a node. ++ * ++ * @param dataDirectory the node's data directory ++ * @return the directory the segments live in ++ */ ++ public static Path directoryIn(final Path dataDirectory) { ++ return dataDirectory.resolve(DIRECTORY_NAME); ++ } ++ ++ /** ++ * The segment file a height belongs to. ++ * ++ * @param directory the journal directory ++ * @param blockNumber the height ++ * @return the segment path ++ */ ++ public static Path segmentFor(final Path directory, final long blockNumber) { ++ return directory.resolve("w-" + (blockNumber / SEGMENT_BLOCKS) + ".rlp"); ++ } ++ ++ /** ++ * Encode one frame payload. ++ * ++ * @param chainId the chain id ++ * @param heard the heard block ++ * @return the RLP payload ++ */ ++ public static byte[] encode(final long chainId, final Heard heard) { ++ final BytesValueRLPOutput out = new BytesValueRLPOutput(); ++ out.startList(); ++ out.writeBytes(DOMAIN_BYTES); ++ out.writeLongScalar(chainId); ++ out.writeLongScalar(heard.blockNumber()); ++ out.writeBytes(heard.blockHash()); ++ PqAnchor.writeCertificate(out, heard.seals()); ++ out.endList(); ++ return out.encoded().toArrayUnsafe(); ++ } ++ ++ /** ++ * Append one heard block to its segment. Best effort, unsynced. ++ * ++ * @param directory the journal directory ++ * @param chainId the chain id ++ * @param heard the heard block ++ * @throws IOException when the file system refuses; the caller logs and carries on ++ */ ++ public static void append(final Path directory, final long chainId, final Heard heard) ++ throws IOException { ++ Files.createDirectories(directory); ++ final byte[] payload = encode(chainId, heard); ++ final CRC32 crc = new CRC32(); ++ crc.update(payload, 0, payload.length); ++ final ByteBuffer frame = ByteBuffer.allocate(4 + payload.length + 4); ++ frame.putInt(payload.length); ++ frame.put(payload); ++ frame.putInt((int) crc.getValue()); ++ frame.flip(); ++ try (FileChannel channel = ++ FileChannel.open( ++ segmentFor(directory, heard.blockNumber()), ++ StandardOpenOption.CREATE, ++ StandardOpenOption.WRITE, ++ StandardOpenOption.APPEND)) { ++ while (frame.hasRemaining()) { ++ channel.write(frame); ++ } ++ } ++ } ++ ++ /** ++ * Delete every segment that lies WHOLLY below the window. ++ * ++ * @param directory the journal directory ++ * @param highestBlock the highest height heard ++ * @param window the retention window in blocks ++ * @return how many segment files were deleted ++ */ ++ public static int pruneSegments(final Path directory, final long highestBlock, final long window) { ++ if (directory == null || !Files.isDirectory(directory)) { ++ return 0; ++ } ++ final long oldestKept = Math.max(0L, highestBlock - window); ++ int deleted = 0; ++ try (DirectoryStream stream = Files.newDirectoryStream(directory)) { ++ for (final Path file : stream) { ++ final Matcher m = SEGMENT_NAME.matcher(file.getFileName().toString()); ++ if (!m.matches()) { ++ continue; ++ } ++ final long lastHeightOfSegment = (Long.parseLong(m.group(1)) + 1L) * SEGMENT_BLOCKS - 1L; ++ if (lastHeightOfSegment < oldestKept) { ++ try { ++ Files.deleteIfExists(file); ++ deleted++; ++ } catch (final IOException e) { ++ LOG.debug("AERE FEREASTRA-SIGILII: could not delete {} ({}).", file, e.toString()); ++ } ++ } ++ } ++ } catch (final IOException | RuntimeException e) { ++ LOG.debug("AERE FEREASTRA-SIGILII: could not list {} ({}).", directory, e.toString()); ++ } ++ return deleted; ++ } ++ ++ /** ++ * Read every frame for a height range, oldest first. Never throws; never verifies. ++ * ++ * @param directory the journal directory ++ * @param chainId the chain id this node runs ++ * @param fromBlock the lowest height wanted, inclusive ++ * @param toBlock the highest height wanted, inclusive ++ * @return the frames that decoded, for this chain, within the range ++ */ ++ public static List readAll( ++ final Path directory, final long chainId, final long fromBlock, final long toBlock) { ++ final List out = new ArrayList<>(); ++ if (directory == null || toBlock < fromBlock || !Files.isDirectory(directory)) { ++ return out; ++ } ++ final List segments = new ArrayList<>(); ++ try (DirectoryStream stream = Files.newDirectoryStream(directory)) { ++ for (final Path file : stream) { ++ final Matcher m = SEGMENT_NAME.matcher(file.getFileName().toString()); ++ if (!m.matches()) { ++ continue; ++ } ++ final long first = Long.parseLong(m.group(1)) * SEGMENT_BLOCKS; ++ final long last = first + SEGMENT_BLOCKS - 1L; ++ if (last >= fromBlock && first <= toBlock) { ++ segments.add(file); ++ } ++ } ++ } catch (final IOException | RuntimeException e) { ++ LOG.warn( ++ "AERE FEREASTRA-SIGILII: the window journal {} could not be listed ({}); IGNORED, the " ++ + "window starts empty exactly as it did before this journal existed.", ++ directory, ++ e.toString()); ++ return out; ++ } ++ for (final Path segment : segments) { ++ readSegment(segment, chainId, fromBlock, toBlock, out); ++ } ++ out.sort(Comparator.comparingLong(Heard::blockNumber)); ++ return out; ++ } ++ ++ private static void readSegment( ++ final Path segment, ++ final long chainId, ++ final long fromBlock, ++ final long toBlock, ++ final List out) { ++ final byte[] raw; ++ try { ++ if (!Files.isRegularFile(segment)) { ++ return; ++ } ++ final long size = Files.size(segment); ++ if (size <= 0 || size > MAX_SEGMENT_BYTES) { ++ LOG.warn( ++ "AERE FEREASTRA-SIGILII: segment {} has an implausible size ({} bytes); IGNORED.", ++ segment, ++ size); ++ return; ++ } ++ raw = Files.readAllBytes(segment); ++ } catch (final IOException | RuntimeException e) { ++ LOG.warn( ++ "AERE FEREASTRA-SIGILII: segment {} could not be read ({}); IGNORED.", ++ segment, ++ e.toString()); ++ return; ++ } ++ final ByteBuffer buffer = ByteBuffer.wrap(raw); ++ int frames = 0; ++ while (buffer.remaining() >= 8) { ++ final int length = buffer.getInt(); ++ if (length <= 0 || length > MAX_FRAME_BYTES || buffer.remaining() < length + 4) { ++ LOG.info( ++ "AERE FEREASTRA-SIGILII: segment {} ends in a torn or implausible frame after {} good " ++ + "frame(s); what follows it is dropped.", ++ segment, ++ frames); ++ return; ++ } ++ final byte[] payload = new byte[length]; ++ buffer.get(payload); ++ final int storedCrc = buffer.getInt(); ++ final CRC32 crc = new CRC32(); ++ crc.update(payload, 0, payload.length); ++ if ((int) crc.getValue() != storedCrc) { ++ LOG.warn( ++ "AERE FEREASTRA-SIGILII: segment {} holds a frame whose checksum does not match after " ++ + "{} good frame(s); it and what follows it are dropped.", ++ segment, ++ frames); ++ return; ++ } ++ frames++; ++ final Heard heard = decode(payload, chainId); ++ if (heard != null && heard.blockNumber() >= fromBlock && heard.blockNumber() <= toBlock) { ++ out.add(heard); ++ } ++ } ++ } ++ ++ private static Heard decode(final byte[] payload, final long chainId) { ++ try { ++ final RLPInput in = new BytesValueRLPInput(Bytes.wrap(payload), false); ++ in.enterList(); ++ if (!DOMAIN_BYTES.equals(in.readBytes())) { ++ return null; ++ } ++ if (in.readLongScalar() != chainId) { ++ return null; ++ } ++ final long blockNumber = in.readLongScalar(); ++ final Bytes blockHash = in.readBytes(); ++ if (blockNumber < 0 || blockHash.size() != 32) { ++ return null; ++ } ++ final List seals = new ArrayList<>(); ++ in.enterList(); ++ while (!in.isEndOfCurrentList()) { ++ if (seals.size() >= MAX_SEALS) { ++ return null; ++ } ++ in.enterList(); ++ final int index = in.readIntScalar(); ++ final Bytes signature = in.readBytes(); ++ in.leaveList(); ++ seals.add(new FalconSeal(index, signature)); ++ } ++ in.leaveList(); ++ in.leaveList(); ++ return new Heard(blockNumber, blockHash, seals); ++ } catch (final RuntimeException e) { ++ return null; ++ } ++ } ++ ++ /** ++ * Count the frames of a journal, for tests and for the operator's curiosity. Never throws. ++ * ++ * @param directory the journal directory ++ * @param chainId the chain id ++ * @return how many frames decode for this chain, at any height ++ */ ++ public static int frameCount(final Path directory, final long chainId) { ++ return readAll(directory, chainId, 0L, Long.MAX_VALUE).size(); ++ } ++ ++ /** ++ * The seals of one frame collection, for callers that hold a plain collection. ++ * ++ * @param blockNumber the height ++ * @param blockHash the on-chain hash ++ * @param seals the heard seals ++ * @return the frame value, seals index-sorted ++ */ ++ public static Heard heard( ++ final long blockNumber, final Bytes blockHash, final Collection seals) { ++ return new Heard(blockNumber, blockHash, PqAnchor.sortedByIndex(new ArrayList<>(seals))); ++ } ++} +diff --git a/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSealWindowPersistenceTest.java b/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSealWindowPersistenceTest.java +new file mode 100755 +index 000000000..5c661d32d +--- /dev/null ++++ b/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSealWindowPersistenceTest.java +@@ -0,0 +1,478 @@ ++/* ++ * Copyright contributors to Besu / AERE Network. ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with ++ * the License. You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on ++ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the ++ * specific language governing permissions and limitations under the License. ++ * ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++package org.hyperledger.besu.consensus.common.bft; ++ ++import static org.assertj.core.api.Assertions.assertThat; ++import static org.assertj.core.api.Assertions.assertThatCode; ++ ++import org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer; ++import org.hyperledger.besu.crypto.SecureRandomProvider; ++import org.hyperledger.besu.datatypes.Hash; ++import org.hyperledger.besu.ethereum.core.BlockHeader; ++import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; ++ ++import java.lang.reflect.Field; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.util.ArrayList; ++import java.util.HashMap; ++import java.util.List; ++import java.util.Map; ++import java.util.Optional; ++import java.util.OptionalInt; ++import java.util.function.LongFunction; ++import java.util.stream.Stream; ++ ++import org.apache.tuweni.bytes.Bytes; ++import org.apache.tuweni.bytes.Bytes32; ++import org.bouncycastle.crypto.digests.KeccakDigest; ++import org.bouncycastle.pqc.crypto.falcon.FalconPrivateKeyParameters; ++import org.bouncycastle.pqc.crypto.falcon.FalconSigner; ++import org.junit.jupiter.api.AfterEach; ++import org.junit.jupiter.api.BeforeEach; ++import org.junit.jupiter.api.Test; ++import org.junit.jupiter.api.io.TempDir; ++ ++/** ++ * AIP-21, SECOND HALF (2026-09-18): the finality window survives a restart. ++ * ++ *

The rule of the house: no proof without its negative pair. Real Falcon-512 keys, the real ++ * genesis-anchored registry, the real commit message, a real directory. What each test pins down: ++ * ++ *

    ++ *
  1. {@link #theWindowSurvivesARestartAndOnlyCanonicalBlocksComeBack()} - the point of the whole ++ * thing, with its negative: a journal frame for a hash that is NOT canonical at its height ++ * never comes back, and neither does a height this node's chain does not hold. ++ *
  2. {@link #aForgedSealInTheJournalNeverEntersTheWindow()} - THE load-bearing assertion, the one ++ * the build-time negative control plants against: three forgeries beside two genuine seals. ++ *
  3. {@link #aTornTailOrAFlippedByteCostsOnlyWhatFollowsIt()} - a node that died mid-append, a ++ * flipped byte, garbage, a directory where a file should be: never an exception, and the good ++ * frames BEFORE the damage still come back. ++ *
  4. {@link #aJournalFromAnotherChainOrOutsideTheWindowIsIgnored()}. ++ *
  5. {@link #oldSegmentsAreDeletedSoTheFootprintStaysBounded()}. ++ *
  6. {@link #theJournalIsOffWhenPersistenceIsOffOrThePropertySaysSo()}. ++ *
  7. {@link #aRestoredWindowIsNotAppendedASecondTime()} - the journal does not grow by what it ++ * already holds every time the node restarts. ++ *
  8. {@link #whatOneAppendCostsAgainstTheBlockInterval()} - the price on the consensus thread, ++ * as a number. ++ *
++ * ++ *

NOT MEASURED here: that a live validator answers {@code post-quantum} for a block heard before ++ * its restart. That is measured on testnet 28001 and recorded in the AIP. ++ */ ++public class PqSealWindowPersistenceTest { ++ ++ private static final long H = 1_000L; ++ private static final long ATTACH = 900L; ++ private static final int N = 7; ++ private static final long CHAIN_ID = 2_800L; ++ private static final long BLOCK_INTERVAL_MS = 523L; ++ private static final long HEAD = 5_000L; ++ ++ @TempDir private Path tmp; ++ ++ private Path dataDirectory; ++ private final List privateKeys = new ArrayList<>(); ++ private final Map chain = new HashMap<>(); ++ ++ @BeforeEach ++ public void setUp() throws Exception { ++ dataDirectory = Files.createDirectories(tmp.resolve("besu-data")); ++ final KeccakDigest kd = new KeccakDigest(256); ++ final StringBuilder manifest = ++ new StringBuilder("{\"config\":{\"aereFalconRegistry\":{") ++ .append(PqV2Fixture.manifestHeader(N, CHAIN_ID, H)); ++ for (int i = 0; i < N; i++) { ++ privateKeys.add(PqV2Fixture.privateKey(i)); ++ final byte[] anchoredRow = PqV2Fixture.anchorPreimageRow(i); ++ kd.update(anchoredRow, 0, anchoredRow.length); ++ manifest.append(',').append(PqV2Fixture.manifestEntry(i, N, CHAIN_ID, H)); ++ } ++ final byte[] anchoredHash = new byte[32]; ++ kd.doFinal(anchoredHash, 0); ++ manifest ++ .append("}},\"alloc\":{\"0000000000000000000000000000000000000fa1\":{\"storage\":{\"0x") ++ .append("0".repeat(64)) ++ .append("\":\"0x") ++ .append(Bytes.wrap(anchoredHash).toUnprefixedHexString()) ++ .append("\"}}}}"); ++ final Path genesisPath = tmp.resolve("genesis-registry.json"); ++ Files.writeString(genesisPath, manifest.toString()); ++ System.setProperty("aere.falcon.genesis", genesisPath.toAbsolutePath().toString()); ++ System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH)); ++ System.clearProperty(PqSealWindowStore.PROPERTY_PERSIST); ++ ++ resetFalconSingleton(); ++ bindTheRegistryScheduleTheWayALiveNodeDoes(genesisPath); ++ PqSealCache.instance().disablePersistence(); ++ PqSealCache.instance().clear(); ++ PqAnchorProducer.useConfigForTesting( ++ new PqAnchorConfig(CHAIN_ID, H, Map.of(H, 0, H + 10L, 3), OptionalInt.empty(), false)); ++ chain.clear(); ++ } ++ ++ /** ++ * The HISTORY door ({@code verifyAtHistoric}) refuses to resolve a key above the arming height ++ * unless a registry schedule is loaded (D2, 2026-08-06): it will not fall back to the head ++ * registry. A live node holds that schedule from its genesis ({@code config.pqRegistryHash}); the ++ * window restore judges heights BELOW the head, so it goes through that door, and so this fixture ++ * binds the schedule exactly the way a node does. Measured 2026-09-18: without this every restore ++ * here returned zero, with the refusal in the log. ++ */ ++ private void bindTheRegistryScheduleTheWayALiveNodeDoes(final Path genesisPath) throws Exception { ++ final PqRegistryHash.Registry held = PqRegistryHash.loadAuto(genesisPath); ++ String hash = PqRegistryHash.hashFor(held, CHAIN_ID); ++ if (!hash.startsWith("0x")) { ++ hash = "0x" + hash; ++ } ++ final Path schedule = tmp.resolve("genesis-schedule.json"); ++ Files.writeString( ++ schedule, ++ "{\"config\":{\"pqRegistryHash\":[{\"block\":" + H + ",\"hash\":\"" + hash + "\"}]}}"); ++ FalconSealSupport.instance() ++ .verifyRegistryBindingOrAbort( ++ 0L, CHAIN_ID, PqRegistryHash.loadScheduleFromGenesis(schedule)); ++ } ++ ++ @AfterEach ++ public void tearDown() throws Exception { ++ System.clearProperty("aere.falcon.genesis"); ++ System.clearProperty("aere.falcon.attachBlock"); ++ System.clearProperty(PqSealWindowStore.PROPERTY_PERSIST); ++ PqSealCache.instance().disablePersistence(); ++ PqSealCache.instance().clear(); ++ PqAnchorProducer.useConfigForTesting(null); ++ resetFalconSingleton(); ++ } ++ ++ @Test ++ public void theWindowSurvivesARestartAndOnlyCanonicalBlocksComeBack() { ++ assertThat(FalconSealSupport.instance().genesisAnchored()).isTrue(); ++ PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID); ++ ++ // Ten heights heard live, three genuine seals each. A height reaches the journal when the NEXT ++ // one begins, so after hearing HEAD the journal holds HEAD-9 .. HEAD-1 and the head file HEAD. ++ for (long n = HEAD - 9L; n <= HEAD; n++) { ++ for (int i = 0; i < 3; i++) { ++ PqSealCache.instance().record(n, header(n).getHash(), List.of(genuineSeal(i, header(n)))); ++ } ++ } ++ // and one losing proposal at HEAD-4: heard, journaled, NOT canonical ++ final BlockHeader loser = ++ new BlockHeaderTestFixture().number(HEAD - 4L).extraData(Bytes.of(7, 7, 7)).buildHeader(); ++ assertThat(loser.getHash()).isNotEqualTo(header(HEAD - 4L).getHash()); ++ ++ final Path journal = PqSealWindowStore.directoryIn(dataDirectory); ++ assertThat(journal).isDirectory(); ++ assertThat(PqSealWindowStore.frameCount(journal, CHAIN_ID)).isEqualTo(9); ++ appendUnchecked(journal, loser, List.of(genuineSeal(0, loser))); ++ ++ // THE RESTART: the in-memory window is gone ++ PqSealCache.instance().clear(); ++ assertThat(PqSealCache.instance().sealCount(header(HEAD - 5L).getHash())).isZero(); ++ ++ // this node's chain holds every height except HEAD-7 (the negative: a height it cannot vouch for) ++ final LongFunction> canonical = ++ n -> n == HEAD - 7L ? Optional.empty() : Optional.of(header(n).getHash()); ++ final int restored = ++ PqSealCache.instance() ++ .restoreWindowFromDisk(HEAD, canonical, PqSignerRegistry.falconSealSupport()); ++ ++ assertThat(restored) ++ .describedAs("nine journaled heights, minus the one the chain does not hold, three seals each") ++ .isEqualTo(8 * 3); ++ for (long n = HEAD - 9L; n < HEAD; n++) { ++ assertThat(PqSealCache.instance().sealCount(header(n).getHash())) ++ .describedAs("height %d", n) ++ .isEqualTo(n == HEAD - 7L ? 0 : 3); ++ } ++ assertThat(PqSealCache.instance().sealCount(loser.getHash())) ++ .describedAs("a frame for a hash that is not canonical at its height never comes back") ++ .isZero(); ++ assertThat(PqSealCache.instance().sealsFor(HEAD - 2L, header(HEAD - 2L).getHash())) ++ .extracting(FalconSeal::getValidatorIndex) ++ .containsExactly(0, 1, 2); ++ } ++ ++ @Test ++ public void aForgedSealInTheJournalNeverEntersTheWindow() throws Exception { ++ final BlockHeader block = header(HEAD - 3L); ++ final FalconSeal genuine0 = genuineSeal(0, block); ++ final FalconSeal genuine1 = genuineSeal(1, block); ++ final Bytes32 anotherBlocksMessage = ++ PqAnchor.commitMessage(CHAIN_ID, block.getNumber() - 1L, Bytes32.leftPad(Bytes.of(9))); ++ final byte[] randomBytes = new byte[genuine0.getSignature().size()]; ++ SecureRandomProvider.createSecureRandom().nextBytes(randomBytes); ++ final List all = ++ List.of( ++ genuine0, ++ genuine1, ++ new FalconSeal(2, Bytes.wrap(falconSign(privateKeys.get(2), anotherBlocksMessage))), ++ new FalconSeal(3, Bytes.wrap(randomBytes)), ++ new FalconSeal(4, genuine0.getSignature())); ++ final Path journal = PqSealWindowStore.directoryIn(dataDirectory); ++ PqSealWindowStore.append( ++ journal, CHAIN_ID, PqSealWindowStore.heard(block.getNumber(), block.getHash().getBytes(), all)); ++ ++ PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID); ++ final int restored = ++ PqSealCache.instance() ++ .restoreWindowFromDisk( ++ HEAD, n -> Optional.of(header(n).getHash()), PqSignerRegistry.falconSealSupport()); ++ ++ assertThat(restored) ++ .describedAs( ++ "THE LOAD-BEARING ASSERTION. Five seals were in the frame and only the two genuine ones " ++ + "may come out: a signature over another block's message, random bytes and a " ++ + "genuine seal re-labelled under another index all die at the verifier. Take the " ++ + "verifier out of restoreWindowFromDisk and this line goes red, which is what the " ++ + "build-time negative control proves.") ++ .isEqualTo(2); ++ assertThat(PqSealCache.instance().sealsFor(block.getNumber(), block.getHash())) ++ .extracting(FalconSeal::getValidatorIndex) ++ .containsExactly(0, 1); ++ } ++ ++ @Test ++ public void aTornTailOrAFlippedByteCostsOnlyWhatFollowsIt() throws Exception { ++ final Path journal = PqSealWindowStore.directoryIn(dataDirectory); ++ // five heights in ONE segment (64 heights per segment; HEAD-5 .. HEAD-1 share 4992/64 = 78) ++ for (long n = HEAD - 5L; n < HEAD; n++) { ++ PqSealWindowStore.append( ++ journal, ++ CHAIN_ID, ++ PqSealWindowStore.heard(n, header(n).getHash().getBytes(), List.of(genuineSeal(0, header(n))))); ++ } ++ final Path segment = PqSealWindowStore.segmentFor(journal, HEAD - 1L); ++ assertThat(PqSealWindowStore.segmentFor(journal, HEAD - 5L)).isEqualTo(segment); ++ final byte[] whole = Files.readAllBytes(segment); ++ PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID); ++ ++ // positive control first: the undamaged journal gives all five ++ assertThat(restoreAll()).isEqualTo(5); ++ ++ // (a) torn tail: the node died in the middle of the fifth append ++ Files.write(segment, java.util.Arrays.copyOf(whole, whole.length - 40)); ++ PqSealCache.instance().clear(); ++ assertThat(restoreAll()).describedAs("the four whole frames before the torn one").isEqualTo(4); ++ ++ // (b) one byte flipped inside the THIRD frame: the checksum catches it, frames one and two live ++ final int frame = whole.length / 5; ++ final byte[] flipped = whole.clone(); ++ flipped[2 * frame + frame / 2] ^= 0x20; ++ Files.write(segment, flipped); ++ PqSealCache.instance().clear(); ++ assertThat(restoreAll()).describedAs("the two frames before the damaged one").isEqualTo(2); ++ ++ // (c) garbage, empty, a directory where the segment should be: nothing, and no exception ++ Files.write(segment, new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9}); ++ PqSealCache.instance().clear(); ++ assertThat(restoreAll()).isZero(); ++ Files.write(segment, new byte[0]); ++ assertThat(restoreAll()).isZero(); ++ Files.delete(segment); ++ Files.createDirectory(segment); ++ assertThatCode(this::restoreAll).doesNotThrowAnyException(); ++ assertThat(restoreAll()).isZero(); ++ } ++ ++ @Test ++ public void aJournalFromAnotherChainOrOutsideTheWindowIsIgnored() throws Exception { ++ final Path journal = PqSealWindowStore.directoryIn(dataDirectory); ++ final BlockHeader inWindow = header(HEAD - 10L); ++ final BlockHeader tooOld = header(HEAD - PqSealCache.instance().retentionBlocks() - 50L); ++ PqSealWindowStore.append( ++ journal, ++ CHAIN_ID + 1L, ++ PqSealWindowStore.heard( ++ inWindow.getNumber(), inWindow.getHash().getBytes(), List.of(genuineSeal(0, inWindow)))); ++ PqSealWindowStore.append( ++ journal, ++ CHAIN_ID, ++ PqSealWindowStore.heard( ++ tooOld.getNumber(), tooOld.getHash().getBytes(), List.of(genuineSeal(0, tooOld)))); ++ PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID); ++ assertThat(restoreAll()) ++ .describedAs("another chain's frame and a height below the window: neither comes back") ++ .isZero(); ++ // positive pair: the same height, this chain ++ PqSealWindowStore.append( ++ journal, ++ CHAIN_ID, ++ PqSealWindowStore.heard( ++ inWindow.getNumber(), inWindow.getHash().getBytes(), List.of(genuineSeal(0, inWindow)))); ++ assertThat(restoreAll()).isEqualTo(1); ++ } ++ ++ @Test ++ public void oldSegmentsAreDeletedSoTheFootprintStaysBounded() throws Exception { ++ PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID); ++ final long window = PqSealCache.instance().retentionBlocks(); ++ final long heights = window + 5L * PqSealWindowStore.SEGMENT_BLOCKS; ++ final FalconSeal one = new FalconSeal(0, Bytes.wrap(new byte[650])); ++ for (long n = HEAD; n < HEAD + heights; n++) { ++ PqSealCache.instance().record(n, Hash.hash(Bytes.ofUnsignedLong(n)), List.of(one)); ++ } ++ final Path journal = PqSealWindowStore.directoryIn(dataDirectory); ++ final long highest = HEAD + heights - 1L; ++ try (Stream files = Files.list(journal)) { ++ final List segments = files.toList(); ++ assertThat(segments.size()) ++ .describedAs("segments kept: the window plus the one being written plus one of slack") ++ .isLessThanOrEqualTo((int) (window / PqSealWindowStore.SEGMENT_BLOCKS) + 2); ++ assertThat(segments) ++ .describedAs("no segment wholly below the window is left behind") ++ .doesNotContain(PqSealWindowStore.segmentFor(journal, highest - window - 2L * PqSealWindowStore.SEGMENT_BLOCKS)); ++ assertThat(segments).contains(PqSealWindowStore.segmentFor(journal, highest - 1L)); ++ } ++ } ++ ++ @Test ++ public void theJournalIsOffWhenPersistenceIsOffOrThePropertySaysSo() throws Exception { ++ final Path journal = PqSealWindowStore.directoryIn(dataDirectory); ++ // persistence never enabled: memory only, exactly as before this class existed ++ for (long n = HEAD; n < HEAD + 3L; n++) { ++ PqSealCache.instance().record(n, header(n).getHash(), List.of(genuineSeal(0, header(n)))); ++ } ++ assertThat(journal).doesNotExist(); ++ ++ // the operator's switch ++ System.setProperty(PqSealWindowStore.PROPERTY_PERSIST, "false"); ++ PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID); ++ for (long n = HEAD + 3L; n < HEAD + 6L; n++) { ++ PqSealCache.instance().record(n, header(n).getHash(), List.of(genuineSeal(0, header(n)))); ++ } ++ assertThat(journal).doesNotExist(); ++ assertThat(PqSealStore.fileIn(dataDirectory)) ++ .describedAs("the head file, which liveness depends on, is NOT governed by this switch") ++ .exists(); ++ ++ // positive pair: the switch back on ++ System.clearProperty(PqSealWindowStore.PROPERTY_PERSIST); ++ PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID); ++ for (long n = HEAD + 6L; n < HEAD + 9L; n++) { ++ PqSealCache.instance().record(n, header(n).getHash(), List.of(genuineSeal(0, header(n)))); ++ } ++ assertThat(journal).isDirectory(); ++ assertThat(PqSealWindowStore.frameCount(journal, CHAIN_ID)).isGreaterThanOrEqualTo(2); ++ } ++ ++ @Test ++ public void aRestoredWindowIsNotAppendedASecondTime() { ++ PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID); ++ for (long n = HEAD - 4L; n <= HEAD; n++) { ++ PqSealCache.instance().record(n, header(n).getHash(), List.of(genuineSeal(0, header(n)))); ++ } ++ final Path journal = PqSealWindowStore.directoryIn(dataDirectory); ++ assertThat(PqSealWindowStore.frameCount(journal, CHAIN_ID)).isEqualTo(4); ++ ++ PqSealCache.instance().clear(); ++ PqSealCache.instance() ++ .restoreFromDisk(HEAD, header(HEAD).getHash(), PqSignerRegistry.falconSealSupport()); ++ assertThat(restoreAll()).isEqualTo(4); ++ ++ // the chain moves on: exactly ONE new frame (the head, which was only in the head file) ++ PqSealCache.instance() ++ .record(HEAD + 1L, header(HEAD + 1L).getHash(), List.of(genuineSeal(0, header(HEAD + 1L)))); ++ assertThat(PqSealWindowStore.frameCount(journal, CHAIN_ID)) ++ .describedAs("four restored heights are not written again; the head is written once") ++ .isEqualTo(5); ++ } ++ ++ @Test ++ public void whatOneAppendCostsAgainstTheBlockInterval() throws Exception { ++ final Path journal = PqSealWindowStore.directoryIn(dataDirectory); ++ final List ten = new ArrayList<>(); ++ for (int i = 0; i < 10; i++) { ++ ten.add(new FalconSeal(i, Bytes.wrap(new byte[660]))); ++ } ++ final int rounds = 200; ++ final long t0 = System.nanoTime(); ++ for (int r = 0; r < rounds; r++) { ++ PqSealWindowStore.append( ++ journal, CHAIN_ID, PqSealWindowStore.heard(HEAD + r, Bytes32.leftPad(Bytes.of(r)), ten)); ++ } ++ final double meanMs = (System.nanoTime() - t0) / 1_000_000.0 / rounds; ++ System.out.printf( ++ "AERE FEREASTRA-SIGILII: one append of 10 seals costs %.3f ms on average, %.2f%% of a %d ms block%n", ++ meanMs, 100.0 * meanMs / BLOCK_INTERVAL_MS, BLOCK_INTERVAL_MS); ++ assertThat(meanMs) ++ .describedAs("an append on the consensus thread must stay far below the block interval") ++ .isLessThan(BLOCK_INTERVAL_MS * 0.05); ++ } ++ ++ /** ++ * THE SHARED VECTOR, literal for literal in the Nethermind-derived client's ++ * AereAip21SealWindowJournalProofTests: the two clients write the SAME frame payload. Chain 28001, ++ * block 5, hash 32 x 0xaa, one seal (index 1, signature 0x0102). The frame around it is ++ * length(4, big endian) || payload || crc32(payload)(4, big endian), java.util.zip.CRC32. ++ */ ++ @Test ++ public void theBytesOnDiskAreTheOnesTheOtherClientWrites() { ++ final Bytes hash = Bytes.fromHexString("0x" + "aa".repeat(32)); ++ final byte[] payload = ++ PqSealWindowStore.encode( ++ 28_001L, PqSealWindowStore.heard(5L, hash, List.of(new FalconSeal(1, Bytes.of(1, 2))))); ++ assertThat(Bytes.wrap(payload).toUnprefixedHexString()) ++ .isEqualTo( ++ "f84094414552452d50512d5345414c57494e444f572d31826d6105a0" ++ + "aa".repeat(32) ++ + "c5c401820102"); ++ final java.util.zip.CRC32 crc = new java.util.zip.CRC32(); ++ crc.update("123456789".getBytes(java.nio.charset.StandardCharsets.US_ASCII)); ++ assertThat(crc.getValue()).isEqualTo(0xCBF43926L); ++ } ++ ++ private int restoreAll() { ++ return PqSealCache.instance() ++ .restoreWindowFromDisk( ++ HEAD, n -> Optional.of(header(n).getHash()), PqSignerRegistry.falconSealSupport()); ++ } ++ ++ private BlockHeader header(final long number) { ++ return chain.computeIfAbsent( ++ number, n -> new BlockHeaderTestFixture().number(n).buildHeader()); ++ } ++ ++ private FalconSeal genuineSeal(final int index, final BlockHeader block) { ++ final Bytes32 m = ++ PqAnchor.commitMessage(CHAIN_ID, block.getNumber(), block.getHash().getBytes()); ++ return new FalconSeal(index, Bytes.wrap(falconSign(privateKeys.get(index), m))); ++ } ++ ++ private static void appendUnchecked( ++ final Path journal, final BlockHeader block, final List seals) { ++ try { ++ PqSealWindowStore.append( ++ journal, ++ CHAIN_ID, ++ PqSealWindowStore.heard(block.getNumber(), block.getHash().getBytes(), seals)); ++ } catch (final java.io.IOException e) { ++ throw new java.io.UncheckedIOException(e); ++ } ++ } ++ ++ private static byte[] falconSign(final FalconPrivateKeyParameters key, final Bytes32 m) { ++ final FalconSigner signer = new FalconSigner(); ++ signer.init(true, key); ++ return signer.generateSignature(m.toArray()); ++ } ++ ++ private static void resetFalconSingleton() throws Exception { ++ final Field f = FalconSealSupport.class.getDeclaredField("instance"); ++ f.setAccessible(true); ++ f.set(null, null); ++ } ++} +diff --git a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/jsonrpc/QbftJsonRpcMethods.java b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/jsonrpc/QbftJsonRpcMethods.java +index a0aaf3a16..674b39e15 100644 +--- a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/jsonrpc/QbftJsonRpcMethods.java ++++ b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/jsonrpc/QbftJsonRpcMethods.java +@@ -1,42 +1,49 @@ + /* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 ++ * ++ * Modifications Copyright contributors to the Aere Network. ++ * ++ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the ++ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was ++ * found, as section 4(c) requires. The change itself is in patches/0007-aere-pq-finality.patch. + */ + package org.hyperledger.besu.consensus.qbft.jsonrpc; + + import org.hyperledger.besu.config.BftConfigOptions; + import org.hyperledger.besu.consensus.common.BlockInterface; + import org.hyperledger.besu.consensus.common.bft.BftContext; + import org.hyperledger.besu.consensus.common.validator.ValidatorProvider; + import org.hyperledger.besu.consensus.qbft.jsonrpc.methods.QbftDiscardValidatorVote; + import org.hyperledger.besu.consensus.qbft.jsonrpc.methods.QbftGetPendingVotes; ++import org.hyperledger.besu.consensus.qbft.jsonrpc.methods.AereGetPqFinality; + import org.hyperledger.besu.consensus.qbft.jsonrpc.methods.QbftGetRequestTimeoutSeconds; + import org.hyperledger.besu.consensus.qbft.jsonrpc.methods.QbftGetSignerMetrics; + import org.hyperledger.besu.consensus.qbft.jsonrpc.methods.QbftGetValidatorsByBlockHash; + import org.hyperledger.besu.consensus.qbft.jsonrpc.methods.QbftGetValidatorsByBlockNumber; + import org.hyperledger.besu.consensus.qbft.jsonrpc.methods.QbftProposeValidatorVote; + import org.hyperledger.besu.ethereum.ProtocolContext; + import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis; + import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod; + import org.hyperledger.besu.ethereum.api.jsonrpc.methods.ApiGroupJsonRpcMethods; + import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; + import org.hyperledger.besu.ethereum.core.MiningConfiguration; + import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; + + import java.util.Map; + + /** The Qbft json rpc methods. */ + public class QbftJsonRpcMethods extends ApiGroupJsonRpcMethods { + + private final ProtocolContext context; + private final ValidatorProvider readOnlyValidatorProvider; +@@ -74,25 +81,27 @@ public class QbftJsonRpcMethods extends ApiGroupJsonRpcMethods { + @Override + protected Map create() { + final BlockchainQueries blockchainQueries = + new BlockchainQueries( + protocolSchedule, + context.getBlockchain(), + context.getWorldStateArchive(), + miningConfiguration); + final BftContext bftContext = context.getConsensusContext(BftContext.class); + final BlockInterface blockInterface = bftContext.getBlockInterface(); + final ValidatorProvider validatorProvider = bftContext.getValidatorProvider(); + + Map methods = + mapOf( + new QbftProposeValidatorVote(validatorProvider), + new QbftGetValidatorsByBlockNumber(blockchainQueries, readOnlyValidatorProvider), + new QbftDiscardValidatorVote(validatorProvider), + new QbftGetValidatorsByBlockHash(context.getBlockchain(), readOnlyValidatorProvider), + new QbftGetSignerMetrics(readOnlyValidatorProvider, blockInterface, blockchainQueries), + new QbftGetPendingVotes(validatorProvider), +- new QbftGetRequestTimeoutSeconds(bftConfig)); ++ new QbftGetRequestTimeoutSeconds(bftConfig), ++ // AIP-21 (AERE): per-block post-quantum finality, non-consensus, verified against the anchored registry ++ new AereGetPqFinality(blockchainQueries, readOnlyValidatorProvider)); + + return methods; + } + } +diff --git a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/jsonrpc/methods/AereGetPqFinality.java b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/jsonrpc/methods/AereGetPqFinality.java +new file mode 100755 +index 000000000..7fc5d24a5 +--- /dev/null ++++ b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/jsonrpc/methods/AereGetPqFinality.java +@@ -0,0 +1,254 @@ ++/* ++ * Copyright contributors to Besu / AERE Network. ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with ++ * the License. You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on ++ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the ++ * specific language governing permissions and limitations under the License. ++ * ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++package org.hyperledger.besu.consensus.qbft.jsonrpc.methods; ++ ++import org.hyperledger.besu.consensus.common.bft.BftHelpers; ++import org.hyperledger.besu.consensus.common.bft.FalconSeal; ++import org.hyperledger.besu.consensus.common.bft.PqSealCache; ++import org.hyperledger.besu.consensus.common.bft.PqSignerRegistry; ++import org.hyperledger.besu.consensus.common.bft.SchemeSeal; ++import org.hyperledger.besu.consensus.common.bft.SealSchemes; ++import org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer; ++import org.hyperledger.besu.consensus.common.validator.ValidatorProvider; ++import org.hyperledger.besu.datatypes.Hash; ++import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod; ++import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; ++import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters; ++import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.AbstractBlockParameterMethod; ++import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.BlockParameter; ++import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter.JsonRpcParameterException; ++import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; ++import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; ++import org.hyperledger.besu.ethereum.core.BlockHeader; ++ ++import java.util.ArrayList; ++import java.util.LinkedHashMap; ++import java.util.List; ++import java.util.Map; ++import java.util.Optional; ++import java.util.TreeSet; ++ ++import org.apache.tuweni.bytes.Bytes; ++ ++/** ++ * AIP-21 (AERE, 2026-09-18), first half: the post-quantum finality of ONE block, verifiable per block, ++ * without growing the header. ++ * ++ *

WHAT IT ANSWERS. Every block of chain 2800 is committed with a Falcon-512 seal on each Commit ++ * message (enforced since H1, 2026-09-05), and every validator keeps the seals it heard for recent ++ * blocks in {@link PqSealCache}. Those seals never reach the header (only every 128th block carries a ++ * certificate, over its parent), so until now the immediate post-quantum finality of a block was true ++ * and NOT observable. This method exposes it: for a block it returns the seals heard, re-verifies each ++ * one right now against the anchored registry over the exact message the validators signed, and gives ++ * the verdict "verified distinct signers >= the chain's commit quorum". ++ * ++ *

WHAT IT DOES NOT CLAIM. Seals travel only on Commit messages, which validators send to ++ * validators: a reader node hears none and answers {@code finality: "unavailable"} honestly. The ++ * retention window is the cache's (system property {@code aere.pq.finality.window}, blocks; default ++ * 256), so an older block answers "unavailable" too, and the permanent record for it is the anchor ++ * certificate. Hybrid extras (SLH-DSA seals on anchor parents) are listed as heard but NOT verified ++ * here ({@code verified: null}); their verification is the anchor certificate's job. Below the anchor ++ * form (never on a live chain today) the signed message is the round-specific commit digest, which ++ * cannot be rebuilt from the header, so the method says so instead of guessing. ++ * ++ *

NON-CONSENSUS. Nothing here changes what a block is; a node with or without this method imports ++ * the same chain. That is why it needs no activation height. ++ */ ++public class AereGetPqFinality extends AbstractBlockParameterMethod { ++ ++ /** Verifies one Falcon seal over the message signed for a block; injectable for tests. */ ++ @FunctionalInterface ++ public interface SealVerifier { ++ /** ++ * Verify. ++ * ++ * @param blockNumber the height ++ * @param validatorIndex the signer index in the anchored registry ++ * @param message the message that was signed ++ * @param signature the Falcon signature ++ * @return true iff it verifies against the key bound at that height ++ */ ++ boolean verify(long blockNumber, int validatorIndex, Bytes message, Bytes signature); ++ } ++ ++ /** Rebuilds the message the seals of a block signed, when that is possible from the header. */ ++ @FunctionalInterface ++ public interface MessageBuilder { ++ /** ++ * The signed message. ++ * ++ * @param blockNumber the height ++ * @param onchainHash the round-independent on-chain hash ++ * @return the message, or empty when the form at that height cannot be rebuilt from the header ++ */ ++ Optional messageFor(long blockNumber, Hash onchainHash); ++ } ++ ++ private final ValidatorProvider validatorProvider; ++ private final PqSealCache cache; ++ private final SealVerifier verifier; ++ private final MessageBuilder messageBuilder; ++ ++ /** ++ * Production wiring: the process-wide seal cache, the anchored Falcon registry at the block's ++ * height, and the anchor-form commit message. ++ * ++ * @param blockchainQueries the blockchain queries ++ * @param validatorProvider the read-only validator provider ++ */ ++ public AereGetPqFinality( ++ final BlockchainQueries blockchainQueries, final ValidatorProvider validatorProvider) { ++ this( ++ blockchainQueries, ++ validatorProvider, ++ PqSealCache.instance(), ++ (n, i, m, s) -> PqSignerRegistry.falconSealSupport().verifyAtHistoric(n, i, m, s), ++ (n, h) -> ++ PqAnchorProducer.sealMessageIsAnchorForm(n) ++ ? Optional.of(PqAnchorProducer.commitSealMessage(n, h.getBytes(), Hash.ZERO)) ++ : Optional.empty()); ++ } ++ ++ /** ++ * Full wiring, for tests. ++ * ++ * @param blockchainQueries the blockchain queries ++ * @param validatorProvider the validator provider ++ * @param cache the seal cache to read ++ * @param verifier the seal verifier ++ * @param messageBuilder the signed-message builder ++ */ ++ public AereGetPqFinality( ++ final BlockchainQueries blockchainQueries, ++ final ValidatorProvider validatorProvider, ++ final PqSealCache cache, ++ final SealVerifier verifier, ++ final MessageBuilder messageBuilder) { ++ super(blockchainQueries); ++ this.validatorProvider = validatorProvider; ++ this.cache = cache; ++ this.verifier = verifier; ++ this.messageBuilder = messageBuilder; ++ } ++ ++ @Override ++ public String getName() { ++ return RpcMethod.AERE_GET_PQ_FINALITY.getMethodName(); ++ } ++ ++ @Override ++ protected BlockParameter blockParameter(final JsonRpcRequestContext request) { ++ try { ++ return request.getOptionalParameter(0, BlockParameter.class).orElse(BlockParameter.LATEST); ++ } catch (final JsonRpcParameterException e) { ++ throw new InvalidJsonRpcParameters( ++ "Invalid block parameter (index 0)", RpcErrorType.INVALID_BLOCK_PARAMS, e); ++ } ++ } ++ ++ @Override ++ protected Object pendingResult(final JsonRpcRequestContext request) { ++ return describe(getBlockchainQueries().headBlockHeader()); ++ } ++ ++ @Override ++ protected Object resultByBlockNumber(final JsonRpcRequestContext request, final long blockNumber) { ++ return getBlockchainQueries().getBlockHeaderByNumber(blockNumber).map(this::describe).orElse(null); ++ } ++ ++ /** ++ * The finality record of one block. ++ * ++ * @param header the block's header ++ * @return the JSON-shaped record ++ */ ++ public Map describe(final BlockHeader header) { ++ final long number = header.getNumber(); ++ final Hash hash = header.getHash(); ++ final int validators = validatorProvider.getValidatorsForBlock(header).size(); ++ final int quorum = validators == 0 ? 0 : BftHelpers.calculateRequiredValidatorQuorum(validators); ++ final Optional message = messageBuilder.messageFor(number, hash); ++ final List heard = cache.sealsFor(number, hash); ++ final List extras = cache.extrasFor(number, hash); ++ ++ final List> seals = new ArrayList<>(); ++ final TreeSet verifiedSigners = new TreeSet<>(); ++ for (final FalconSeal seal : heard) { ++ final boolean ok = ++ message.isPresent() ++ && verifier.verify(number, seal.getValidatorIndex(), message.get(), seal.getSignature()); ++ if (ok) { ++ verifiedSigners.add(seal.getValidatorIndex()); ++ } ++ final Map s = new LinkedHashMap<>(); ++ s.put("scheme", "falcon-512"); ++ s.put("index", seal.getValidatorIndex()); ++ s.put("verified", ok); ++ s.put("signature", seal.getSignature().toHexString()); ++ seals.add(s); ++ } ++ final List> hybrid = new ArrayList<>(); ++ for (final SchemeSeal seal : extras) { ++ final Map s = new LinkedHashMap<>(); ++ s.put( ++ "scheme", ++ SealSchemes.byWireId(seal.getSchemeWireId()).map(x -> x.id()).orElse("wire-" + (seal.getSchemeWireId() & 0xff))); ++ s.put("index", seal.getValidatorIndex()); ++ s.put("verified", null); ++ s.put("signature", seal.getSignature().toHexString()); ++ hybrid.add(s); ++ } ++ ++ final String finality; ++ final String note; ++ if (message.isEmpty()) { ++ finality = "unverifiable-form"; ++ note = ++ "below the anchor form the signed message is the round-specific commit digest, which this node cannot rebuild from the header; nothing is guessed"; ++ } else if (heard.isEmpty()) { ++ finality = "unavailable"; ++ note = ++ "no seal heard for this block: seals travel on Commit messages between validators, so a reader node hears none, and a validator keeps only the last " ++ + cache.retentionBlocks() ++ + " blocks; the permanent record for older blocks is the anchor certificate"; ++ } else if (verifiedSigners.size() >= quorum && quorum > 0) { ++ finality = "post-quantum"; ++ note = "at least the commit quorum of distinct validators sealed this block hash with Falcon-512 and every counted seal verifies now against the anchored registry"; ++ } else if (!verifiedSigners.isEmpty()) { ++ finality = "partial"; ++ note = "fewer verified distinct signers than the commit quorum: this node did not hear enough seals, or some it heard no longer verify"; ++ } else { ++ finality = "none"; ++ note = "seals were heard but none verifies against the anchored registry at this height"; ++ } ++ ++ final Map out = new LinkedHashMap<>(); ++ out.put("blockNumber", number); ++ out.put("blockHash", hash.getBytes().toHexString()); ++ out.put("validators", validators); ++ out.put("quorum", quorum); ++ out.put("sealForm", message.isPresent() ? "anchor" : "commit-digest"); ++ out.put("signedMessage", message.map(Bytes::toHexString).orElse(null)); ++ out.put("falconSealsHeard", heard.size()); ++ out.put("falconSealsVerified", verifiedSigners.size()); ++ out.put("verifiedIndexes", new ArrayList<>(verifiedSigners)); ++ out.put("falconSeals", seals); ++ out.put("hybridSeals", hybrid); ++ out.put("finality", finality); ++ out.put("retentionBlocks", cache.retentionBlocks()); ++ out.put("note", note); ++ return out; ++ } ++} +diff --git a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/jsonrpc/methods/AereGetPqFinalityTest.java b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/jsonrpc/methods/AereGetPqFinalityTest.java +new file mode 100755 +index 000000000..b3b6d95ae +--- /dev/null ++++ b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/jsonrpc/methods/AereGetPqFinalityTest.java +@@ -0,0 +1,205 @@ ++/* ++ * Copyright contributors to Besu / AERE Network. ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with ++ * the License. You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on ++ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the ++ * specific language governing permissions and limitations under the License. ++ * ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++package org.hyperledger.besu.consensus.qbft.jsonrpc.methods; ++ ++import static org.assertj.core.api.Assertions.assertThat; ++import static org.mockito.ArgumentMatchers.any; ++import static org.mockito.Mockito.lenient; ++import static org.mockito.Mockito.when; ++ ++import org.hyperledger.besu.consensus.common.bft.FalconSeal; ++import org.hyperledger.besu.consensus.common.bft.PqSealCache; ++import org.hyperledger.besu.consensus.common.validator.ValidatorProvider; ++import org.hyperledger.besu.datatypes.Address; ++import org.hyperledger.besu.datatypes.Hash; ++import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest; ++import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; ++import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; ++import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; ++import org.hyperledger.besu.ethereum.core.BlockHeader; ++ ++import java.util.ArrayList; ++import java.util.List; ++import java.util.Map; ++import java.util.Optional; ++ ++import org.apache.tuweni.bytes.Bytes; ++import org.apache.tuweni.bytes.Bytes32; ++import org.junit.jupiter.api.AfterEach; ++import org.junit.jupiter.api.BeforeEach; ++import org.junit.jupiter.api.Test; ++import org.junit.jupiter.api.extension.ExtendWith; ++import org.mockito.Mock; ++import org.mockito.junit.jupiter.MockitoExtension; ++ ++/** ++ * AIP-21: the finality record is a MEASUREMENT of the seals heard, re-verified, against the chain's ++ * commit quorum; every claim it can make has its negative pair here (too few signers, none verifying, ++ * nothing heard, a form that cannot be rebuilt), and the verifier is consulted with the exact message ++ * and signature, so a stub that verifies nothing turns the verdict from post-quantum to none. ++ */ ++@ExtendWith(MockitoExtension.class) ++public class AereGetPqFinalityTest { ++ ++ private static final long NUMBER = 19_018_096L; ++ private static final Hash HASH = Hash.fromHexString("0xf776f80e1710dc0e0f1927958dd589b98fc35ce8b9854e4fdb12320459fbb3f1"); ++ private static final Bytes MESSAGE = Bytes32.fromHexStringLenient("0x1122"); ++ ++ @Mock private BlockchainQueries blockchainQueries; ++ @Mock private BlockHeader header; ++ @Mock private ValidatorProvider validatorProvider; ++ ++ private final PqSealCache cache = PqSealCache.instance(); ++ private final List verifierCalls = new ArrayList<>(); ++ ++ @BeforeEach ++ public void setUp() { ++ cache.clear(); ++ // lenient: the name and unknown-block cases never touch the header, and strict stubs would call that a defect ++ lenient().when(header.getNumber()).thenReturn(NUMBER); ++ lenient().when(header.getHash()).thenReturn(HASH); ++ } ++ ++ @AfterEach ++ public void tearDown() { ++ cache.clear(); ++ } ++ ++ private static List

validators(final int n) { ++ final List
out = new ArrayList<>(); ++ for (int i = 0; i < n; i++) { ++ out.add(Address.fromHexString(String.format("0x%040x", 0xA000 + i))); ++ } ++ return out; ++ } ++ ++ private void heard(final int count) { ++ final List seals = new ArrayList<>(); ++ for (int i = 0; i < count; i++) { ++ seals.add(new FalconSeal(i, Bytes.of(0x29, i, 7, 7, 7))); ++ } ++ cache.record(NUMBER, HASH, seals); ++ } ++ ++ private AereGetPqFinality method(final int verifyBelowIndex, final Optional message) { ++ return new AereGetPqFinality( ++ blockchainQueries, ++ validatorProvider, ++ cache, ++ (n, i, m, s) -> { ++ verifierCalls.add(new Object[] {n, i, m, s}); ++ return i < verifyBelowIndex; ++ }, ++ (n, h) -> message); ++ } ++ ++ @SuppressWarnings("unchecked") ++ private Map call(final AereGetPqFinality method, final Object... params) { ++ final JsonRpcRequestContext request = ++ new JsonRpcRequestContext(new JsonRpcRequest("2.0", method.getName(), params)); ++ final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); ++ return (Map) response.getResult(); ++ } ++ ++ @Test ++ public void nameIsTheAipMethod() { ++ assertThat(method(7, Optional.of(MESSAGE)).getName()).isEqualTo("aere_getPqFinality"); ++ } ++ ++ @Test ++ public void quorumOfVerifiedDistinctSignersIsPostQuantumFinality() { ++ when(validatorProvider.getValidatorsForBlock(any())).thenReturn(validators(7)); ++ when(blockchainQueries.getBlockHeaderByNumber(NUMBER)).thenReturn(Optional.of(header)); ++ heard(7); ++ final Map r = call(method(5, Optional.of(MESSAGE)), "0x" + Long.toHexString(NUMBER)); ++ assertThat(r.get("finality")).isEqualTo("post-quantum"); ++ assertThat(r.get("quorum")).isEqualTo(5); ++ assertThat(r.get("falconSealsHeard")).isEqualTo(7); ++ assertThat(r.get("falconSealsVerified")).isEqualTo(5); ++ assertThat(r.get("sealForm")).isEqualTo("anchor"); ++ assertThat(r.get("signedMessage")).isEqualTo(MESSAGE.toHexString()); ++ // the verifier saw exactly the message and each seal's own signature ++ assertThat(verifierCalls).hasSize(7); ++ assertThat(verifierCalls.get(3)[1]).isEqualTo(3); ++ assertThat(verifierCalls.get(3)[2]).isEqualTo(MESSAGE); ++ assertThat(verifierCalls.get(3)[3]).isEqualTo(Bytes.of(0x29, 3, 7, 7, 7)); ++ } ++ ++ @Test ++ public void oneSignerShortOfQuorumIsPartial() { ++ when(validatorProvider.getValidatorsForBlock(any())).thenReturn(validators(7)); ++ when(blockchainQueries.getBlockHeaderByNumber(NUMBER)).thenReturn(Optional.of(header)); ++ heard(7); ++ final Map r = call(method(4, Optional.of(MESSAGE)), "0x" + Long.toHexString(NUMBER)); ++ assertThat(r.get("finality")).isEqualTo("partial"); ++ assertThat(r.get("falconSealsVerified")).isEqualTo(4); ++ } ++ ++ @Test ++ public void negativeControlNothingVerifiesIsNone() { ++ when(validatorProvider.getValidatorsForBlock(any())).thenReturn(validators(7)); ++ when(blockchainQueries.getBlockHeaderByNumber(NUMBER)).thenReturn(Optional.of(header)); ++ heard(7); ++ final Map r = call(method(0, Optional.of(MESSAGE)), "0x" + Long.toHexString(NUMBER)); ++ assertThat(r.get("finality")).isEqualTo("none"); ++ assertThat(r.get("falconSealsVerified")).isEqualTo(0); ++ assertThat(r.get("falconSealsHeard")).isEqualTo(7); ++ } ++ ++ @Test ++ public void nothingHeardIsUnavailableNotFalse() { ++ when(validatorProvider.getValidatorsForBlock(any())).thenReturn(validators(7)); ++ when(blockchainQueries.getBlockHeaderByNumber(NUMBER)).thenReturn(Optional.of(header)); ++ final Map r = call(method(7, Optional.of(MESSAGE)), "0x" + Long.toHexString(NUMBER)); ++ assertThat(r.get("finality")).isEqualTo("unavailable"); ++ assertThat(r.get("falconSealsHeard")).isEqualTo(0); ++ assertThat(verifierCalls).isEmpty(); ++ } ++ ++ @Test ++ public void aFormThatCannotBeRebuiltIsSaidNotGuessed() { ++ when(validatorProvider.getValidatorsForBlock(any())).thenReturn(validators(7)); ++ when(blockchainQueries.getBlockHeaderByNumber(NUMBER)).thenReturn(Optional.of(header)); ++ heard(7); ++ final Map r = call(method(7, Optional.empty()), "0x" + Long.toHexString(NUMBER)); ++ assertThat(r.get("finality")).isEqualTo("unverifiable-form"); ++ assertThat(r.get("sealForm")).isEqualTo("commit-digest"); ++ assertThat(r.get("falconSealsVerified")).isEqualTo(0); ++ assertThat(verifierCalls).isEmpty(); ++ } ++ ++ @Test ++ public void latestIsTheDefaultBlock() { ++ when(validatorProvider.getValidatorsForBlock(any())).thenReturn(validators(10)); ++ // "latest" resolves through the head NUMBER, then the header by number (AbstractBlockParameterMethod) ++ when(blockchainQueries.headBlockNumber()).thenReturn(NUMBER); ++ when(blockchainQueries.getBlockHeaderByNumber(NUMBER)).thenReturn(Optional.of(header)); ++ heard(10); ++ final Map r = call(method(10, Optional.of(MESSAGE))); ++ assertThat(r.get("quorum")).isEqualTo(7); ++ assertThat(r.get("finality")).isEqualTo("post-quantum"); ++ assertThat(r.get("blockNumber")).isEqualTo(NUMBER); ++ } ++ ++ @Test ++ public void unknownBlockIsNull() { ++ when(blockchainQueries.getBlockHeaderByNumber(5L)).thenReturn(Optional.empty()); ++ final JsonRpcRequestContext request = ++ new JsonRpcRequestContext(new JsonRpcRequest("2.0", "aere_getPqFinality", new Object[] {"0x5"})); ++ final JsonRpcSuccessResponse response = ++ (JsonRpcSuccessResponse) method(7, Optional.of(MESSAGE)).response(request); ++ assertThat(response.getResult()).isNull(); ++ } ++} +diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java +index ecebef299..cdcaafcbe 100644 +--- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java ++++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java +@@ -1,33 +1,39 @@ + /* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 ++ * ++ * Modifications Copyright contributors to the Aere Network. ++ * ++ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the ++ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was ++ * found, as section 4(c) requires. The change itself is in patches/0007-aere-pq-finality.patch. + */ + package org.hyperledger.besu.ethereum.api.jsonrpc; + + import java.util.Collection; + import java.util.HashSet; + + public enum RpcMethod { + ADMIN_ADD_PEER("admin_addPeer"), + ADMIN_NODE_INFO("admin_nodeInfo"), + ADMIN_PEERS("admin_peers"), + ADMIN_REMOVE_PEER("admin_removePeer"), + ADMIN_CHANGE_LOG_LEVEL("admin_changeLogLevel"), + ADMIN_GENERATE_LOG_BLOOM_CACHE("admin_generateLogBloomCache"), + ADMIN_LOGS_REPAIR_CACHE("admin_logsRepairCache"), + ADMIN_LOGS_REMOVE_CACHE("admin_logsRemoveCache"), + DEBUG_ACCOUNT_AT("debug_accountAt"), + DEBUG_ACCOUNT_RANGE("debug_accountRange"), + DEBUG_METRICS("debug_metrics"), + DEBUG_RESYNC_WORLDSTATE("debug_resyncWorldState"), + DEBUG_SET_HEAD("debug_setHead"), +@@ -114,40 +120,42 @@ public enum RpcMethod { + ETH_SEND_RAW_TRANSACTION("eth_sendRawTransaction"), + ETH_SEND_TRANSACTION("eth_sendTransaction"), + ETH_SIMULATE_V1("eth_simulateV1"), + ETH_SUBSCRIBE("eth_subscribe"), + ETH_SYNCING("eth_syncing"), + ETH_UNINSTALL_FILTER("eth_uninstallFilter"), + ETH_UNSUBSCRIBE("eth_unsubscribe"), + IBFT_DISCARD_VALIDATOR_VOTE("ibft_discardValidatorVote"), + IBFT_GET_PENDING_VOTES("ibft_getPendingVotes"), + IBFT_GET_VALIDATORS_BY_BLOCK_HASH("ibft_getValidatorsByBlockHash"), + IBFT_GET_VALIDATORS_BY_BLOCK_NUMBER("ibft_getValidatorsByBlockNumber"), + IBFT_PROPOSE_VALIDATOR_VOTE("ibft_proposeValidatorVote"), + IBFT_GET_SIGNER_METRICS("ibft_getSignerMetrics"), + QBFT_DISCARD_VALIDATOR_VOTE("qbft_discardValidatorVote"), + QBFT_GET_PENDING_VOTES("qbft_getPendingVotes"), + QBFT_GET_VALIDATORS_BY_BLOCK_HASH("qbft_getValidatorsByBlockHash"), + QBFT_GET_VALIDATORS_BY_BLOCK_NUMBER("qbft_getValidatorsByBlockNumber"), + QBFT_PROPOSE_VALIDATOR_VOTE("qbft_proposeValidatorVote"), + QBFT_GET_SIGNER_METRICS("qbft_getSignerMetrics"), + QBFT_GET_REQUEST_TIMEOUT_SECONDS("qbft_getRequestTimeoutSeconds"), ++ // AIP-21 (AERE, 2026-09-18): the per-block post-quantum finality record, read from the seals heard on Commit messages ++ AERE_GET_PQ_FINALITY("aere_getPqFinality"), + MINER_CHANGE_TARGET_GAS_LIMIT("miner_changeTargetGasLimit"), + MINER_START("miner_start"), + MINER_STOP("miner_stop"), + MINER_GET_MIN_PRIORITY_FEE("miner_getMinPriorityFee"), + MINER_SET_MIN_PRIORITY_FEE("miner_setMinPriorityFee"), + MINER_GET_MIN_GAS_PRICE("miner_getMinGasPrice"), + MINER_SET_MIN_GAS_PRICE("miner_setMinGasPrice"), + MINER_GET_EXTRA_DATA("miner_getExtraData"), + MINER_SET_EXTRA_DATA("miner_setExtraData"), + NET_ENODE("net_enode"), + NET_LISTENING("net_listening"), + NET_PEER_COUNT("net_peerCount"), + NET_SERVICES("net_services"), + NET_VERSION("net_version"), + PERM_ADD_ACCOUNTS_TO_WHITELIST("perm_addAccountsToWhitelist"), + PERM_ADD_ACCOUNTS_TO_ALLOWLIST("perm_addAccountsToAllowlist"), + PERM_ADD_NODES_TO_WHITELIST("perm_addNodesToWhitelist"), + PERM_ADD_NODES_TO_ALLOWLIST("perm_addNodesToAllowlist"), + PERM_GET_ACCOUNTS_WHITELIST("perm_getAccountsWhitelist"), + PERM_GET_ACCOUNTS_ALLOWLIST("perm_getAccountsAllowlist"), diff --git a/run-node.sh b/run-node.sh index 0a34e2d..ea22880 100644 --- a/run-node.sh +++ b/run-node.sh @@ -47,6 +47,8 @@ PATCHES=( "0003-aere-pq-anchor.patch" "0004-aere-basefee-floor.patch" "0005-aere-eip2935-futureeips.patch" + "0006-aere-pq-transactions.patch" + "0007-aere-pq-finality.patch" ) # --- locate self ------------------------------------------------------------------------------- @@ -269,6 +271,7 @@ export BESU_OPTS="\ -Daere.pq.chainId=2800 \ -Daere.pq.hybridRegistry=$REG/hibrid-10.properties \ -Daere.pq.schemeSchedule=17047568:falcon-512+slh-dsa-sha2-128s \ +-Daere.pqtx.forkBlock=19900000 \ -Daere.pq.anchor.minSealsCeiling=$ANCHOR_MIN_SEALS_CEILING" # <<< OPTIUNI DERIVATE