From 6370f2228411c9210ae46577c41616377b0d054e Mon Sep 17 00:00:00 2001 From: federico Date: Tue, 23 Jun 2026 22:53:39 +0800 Subject: [PATCH 1/4] fix(pqc): harden PQ auth sig checks and wallet UX --- README.md | 43 +++++++--- .../org/tron/common/crypto/pqc/MLDSA44.java | 12 +-- .../common/crypto/pqc/PQSchemeRegistry.java | 7 +- .../tron/common/utils/TransactionUtils.java | 35 +++++++- src/main/java/org/tron/keystore/Wallet.java | 12 +++ src/main/java/org/tron/walletcli/Client.java | 51 ++++++++---- .../org/tron/walletcli/WalletApiWrapper.java | 4 + .../cli/commands/WalletCommands.java | 82 +++++++++++++++++-- .../java/org/tron/walletserver/WalletApi.java | 41 ++++++++++ 9 files changed, 247 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 340a53712..d93343a8c 100644 --- a/README.md +++ b/README.md @@ -1812,9 +1812,10 @@ PQ key sizes: | `ML_DSA_44` | 32 B | 1312 B | 2560 B — encoded `rho‖K‖tr‖s1‖s2‖t0` (pk re-derivable from it) | fixed 2420 B | The address is derived with the same formula as ECDSA / SM2: -`0x41 ‖ Keccak-256(public_key)[12..32]`, so a PQ address is indistinguishable from a -regular T-address until it signs a transaction (PQ signatures land in -`Transaction.pq_auth_sig` instead of `Transaction.signature`). +`prefix ‖ Keccak-256(public_key)[12..32]`, where `prefix` is the network address +prefix (`DecodeUtil.addressPreFixByte`, `0x41` on mainnet/Nile). So a PQ address is +indistinguishable from a regular T-address until it signs a transaction (PQ +signatures land in `Transaction.pq_auth_sig` instead of `Transaction.signature`). ### Generate a PQ keypair (no keystore) @@ -1834,9 +1835,17 @@ publicKey (1312 bytes): <2624 hex chars> privateKey (2560 bytes): <5120 hex chars> privateKey persisted form ...: <5120 hex chars> -WARNING: store either the seed or the persisted private key securely. +WARNING: store the persisted private key securely — it is the portable backup. +The seed can also be used as a backup to re-derive this key. ``` +> **Backup caveat for `FN_DSA_512` (Falcon).** Falcon key generation uses +> floating-point sampling, so re-deriving the keypair from the 48-byte seed is +> only guaranteed to reproduce the *same* key on the same CPU architecture + JVM. +> For Falcon, **back up the persisted private key** (the portable form); treat the +> seed as same-architecture-only. ML-DSA-44 keygen is deterministic, so its seed +> is a fully portable backup. `GeneratePQKey` prints this caveat for Falcon. + ### Register a PQ wallet (creates a keystore) >RegisterWalletPQ [scheme] @@ -1859,8 +1868,12 @@ RegisterWalletPQ successful, keystore file: ./Wallet/TXxxxxxxxxxxxxxxxxxxxxxxxxx >ImportWalletPQ [scheme] > Import an existing PQ keypair. The argument is **required** and may be either the -hex string itself or a path to a file containing it. The format is auto-detected by -length against the chosen scheme: +hex string itself or a path to a file containing it. The two are disambiguated +without guessing: if the argument is itself a valid hex string of exactly the +seed or persisted-key length for the scheme, it is used as the key material +directly (even if a same-named file happens to exist); otherwise it is treated as +a file path. Within each source the form is then auto-detected by length against +the chosen scheme: > > - **FN_DSA_512**: 48-byte / 96-hex-char seed (derives the full keypair > deterministically), or 2176-byte / 4352-hex-char extended private key @@ -1923,15 +1936,25 @@ MASTER_PASSWORD='testpassword123A' \ java -jar build/libs/wallet-cli.jar --output json register-wallet-pq \ --name pqalice --scheme ML_DSA_44 -# Import an existing keypair from its persisted-private-key (or seed) hex +# Import an existing keypair. Prefer --key-file: it reads the hex from a file +# (or from stdin with '-') and keeps the secret out of shell history and the +# process list. '--extended-private-key-hex' still works but is discouraged +# because the secret is visible in shell history and in `ps`. Provide exactly +# one of the two — passing both is rejected. MASTER_PASSWORD='testpassword123A' \ java -jar build/libs/wallet-cli.jar --output json import-wallet-pq \ --name pqalice \ - --extended-private-key-hex + --key-file /tmp/my-falcon-key.hex +# Read from stdin instead of a file: MASTER_PASSWORD='testpassword123A' \ - java -jar build/libs/wallet-cli.jar --output json import-wallet-pq \ + printf '%s' "$PQ_KEY_HEX" | java -jar build/libs/wallet-cli.jar --output json import-wallet-pq \ --name pqalice --scheme ML_DSA_44 \ - --extended-private-key-hex + --key-file - +# Discouraged (secret exposed on the command line): +MASTER_PASSWORD='testpassword123A' \ + java -jar build/libs/wallet-cli.jar --output json import-wallet-pq \ + --name pqalice \ + --extended-private-key-hex ``` `generate-pq-key` returns a JSON envelope containing `scheme`, `address`, diff --git a/src/main/java/org/tron/common/crypto/pqc/MLDSA44.java b/src/main/java/org/tron/common/crypto/pqc/MLDSA44.java index 48d9e5d4c..7ea757146 100644 --- a/src/main/java/org/tron/common/crypto/pqc/MLDSA44.java +++ b/src/main/java/org/tron/common/crypto/pqc/MLDSA44.java @@ -3,14 +3,14 @@ import java.security.MessageDigest; import java.security.SecureRandom; import org.bouncycastle.crypto.AsymmetricCipherKeyPair; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAKeyGenerationParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAKeyPairGenerator; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAPrivateKeyParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAPublicKeyParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSASigner; +import org.bouncycastle.crypto.generators.MLDSAKeyPairGenerator; +import org.bouncycastle.crypto.params.MLDSAKeyGenerationParameters; +import org.bouncycastle.crypto.params.MLDSAParameters; +import org.bouncycastle.crypto.params.MLDSAPrivateKeyParameters; +import org.bouncycastle.crypto.params.MLDSAPublicKeyParameters; import org.bouncycastle.crypto.params.ParametersWithRandom; import org.bouncycastle.crypto.prng.FixedSecureRandom; +import org.bouncycastle.crypto.signers.MLDSASigner; import org.tron.protos.Protocol.PQScheme; /** diff --git a/src/main/java/org/tron/common/crypto/pqc/PQSchemeRegistry.java b/src/main/java/org/tron/common/crypto/pqc/PQSchemeRegistry.java index e416ad6ac..f5d38c32d 100644 --- a/src/main/java/org/tron/common/crypto/pqc/PQSchemeRegistry.java +++ b/src/main/java/org/tron/common/crypto/pqc/PQSchemeRegistry.java @@ -4,6 +4,7 @@ import java.util.EnumMap; import java.util.Map; import org.tron.common.crypto.Hash; +import org.tron.common.utils.DecodeUtil; import org.tron.protos.Protocol.PQScheme; /** @@ -253,8 +254,12 @@ public static byte[] deriveHash(PQScheme scheme, byte[] publicKey) { */ public static byte[] computeAddress(PQScheme scheme, byte[] publicKey) { byte[] h = deriveHash(scheme, publicKey); + if (h.length < 20) { + throw new IllegalStateException( + "deriveHash returned " + h.length + " bytes, need at least 20 for address derivation"); + } byte[] addr = new byte[21]; - addr[0] = 0x41; + addr[0] = DecodeUtil.addressPreFixByte; System.arraycopy(h, h.length - 20, addr, 1, 20); return addr; } diff --git a/src/main/java/org/tron/common/utils/TransactionUtils.java b/src/main/java/org/tron/common/utils/TransactionUtils.java index e5568ee47..b43a21264 100644 --- a/src/main/java/org/tron/common/utils/TransactionUtils.java +++ b/src/main/java/org/tron/common/utils/TransactionUtils.java @@ -53,6 +53,21 @@ public class TransactionUtils { private static final ThreadLocal PERMISSION_ID_OVERRIDE = new ThreadLocal<>(); + // Protobuf field number of Transaction.pq_auth_sig (Tron.proto). The Trident + // SDK's Chain.Transaction does not define this field, so on those objects PQ + // auth signatures are preserved only as unknown field 6. + // TODO(trident-pq): once the Trident SDK models pq_auth_sig on + // Chain.Transaction, drop this constant and use the generated accessors. + private static final int PQ_AUTH_SIG_FIELD_NUMBER = 6; + + /** True if the (possibly Trident-typed) transaction carries any PQ auth signature. */ + // TODO(trident-pq): replace the unknown-field probe with + // transaction.getPqAuthSigCount() > 0 when Trident's Chain.Transaction + // exposes pq_auth_sig natively. + private static boolean hasPqAuthSig(Chain.Transaction transaction) { + return transaction.getUnknownFields().hasField(PQ_AUTH_SIG_FIELD_NUMBER); + } + /** * Obtain a data bytes after removing the id and SHA-256(data) * @@ -306,6 +321,9 @@ public static Transaction sign(Transaction transaction, SignInterface myKey) { return transaction; } + // TODO(trident-pq): this Chain<->Protocol byte round-trip only exists because + // Trident's Chain.Transaction cannot carry pq_auth_sig. Drop the conversion + // and add the PQAuthSig directly once Trident models the field. public static Chain.Transaction signPQ( Chain.Transaction transaction, PQSignature signer, PQScheme scheme) throws InvalidProtocolBufferException { @@ -352,8 +370,13 @@ public static Chain.Transaction setTimestamp(Chain.Transaction transaction) { } public static Chain.Transaction setExpirationTime(Chain.Transaction transaction, boolean multi) { - if (transaction.getSignatureCount() == 0) { - long expirationTime = System.currentTimeMillis() + (multi ? 24L * 3600 * 1000 : 6 * 60 * 60 * 1000); + // Both ECDSA signatures and PQ auth signatures cover raw_data (the + // expiration lives there), so refuse to rewrite expiration once EITHER kind + // of signature is attached — otherwise an already-attached pq_auth_sig is + // silently invalidated (txid changes -> SIGERROR on broadcast). + if (transaction.getSignatureCount() == 0 && !hasPqAuthSig(transaction)) { + long expirationTime = + System.currentTimeMillis() + (multi ? 24L * 3600 * 1000 : 6 * 60 * 60 * 1000); Chain.Transaction.Builder builder = transaction.toBuilder(); Chain.Transaction.raw.Builder rowBuilder = transaction.getRawData().toBuilder(); @@ -364,6 +387,10 @@ public static Chain.Transaction setExpirationTime(Chain.Transaction transaction, return transaction; } + // TODO(trident-pq): the Chain<->Protocol byte round-trip is only needed to + // preserve pq_auth_sig (and read getPqAuthSigCount() in the Protocol overload) + // because Trident's Chain.Transaction lacks the field. Simplify once Trident + // models pq_auth_sig. public static Chain.Transaction setPermissionId(Chain.Transaction transaction, String tipString) throws CancelException, InvalidProtocolBufferException { return Chain.Transaction.parseFrom( @@ -372,7 +399,11 @@ public static Chain.Transaction setPermissionId(Chain.Transaction transaction, S public static Transaction setPermissionId(Transaction transaction, String tipString) throws CancelException { + // Changing permissionId mutates raw_data, which would invalidate any + // already-attached signature — including PQ auth signatures, which do not + // bump getSignatureCount(). Bail out if either kind is present. if (transaction.getSignatureCount() != 0 + || transaction.getPqAuthSigCount() != 0 || transaction.getRawData().getContract(0).getPermissionId() != 0) { return transaction; } diff --git a/src/main/java/org/tron/keystore/Wallet.java b/src/main/java/org/tron/keystore/Wallet.java index 1e39c9a24..2fc627903 100644 --- a/src/main/java/org/tron/keystore/Wallet.java +++ b/src/main/java/org/tron/keystore/Wallet.java @@ -283,6 +283,18 @@ public static boolean validPassword (byte[] password, WalletFile walletFile) validate(walletFile); + // PQ keystores may be seed-only (no main ciphertext/mac), so the ECDSA-style + // ciphertext/mac check below would wrongly report "Invalid password". Route + // them through the PQ verify-and-decrypt path, which validates whichever + // segments (ext and/or seed) are actually present. The non-null + non-empty + // scheme test matches how the rest of the codebase distinguishes PQ wallets + // from legacy ECDSA ones (scheme null/empty == ECDSA). + if (walletFile.getScheme() != null && !walletFile.getScheme().isEmpty()) { + PQKeyMaterial material = verifyAndDecryptPQ(password, walletFile); + material.clearSecrets(); + return true; + } + WalletFile.Crypto crypto = walletFile.getCrypto(); byte[] mac = ByteArray.fromHexString(crypto.getMac()); diff --git a/src/main/java/org/tron/walletcli/Client.java b/src/main/java/org/tron/walletcli/Client.java index d5700b791..01d1bcd1d 100755 --- a/src/main/java/org/tron/walletcli/Client.java +++ b/src/main/java/org/tron/walletcli/Client.java @@ -3241,7 +3241,19 @@ private void generatePQKey(String[] parameters) { System.out.println("address: " + addressStr); System.out.println("privateKey persisted form (" + persisted.length + " bytes): " + persistedStr); - System.out.println("WARNING: store the seed and the persisted private key securely."); + System.out.println("WARNING: store the persisted private key securely — it is the " + + "portable backup."); + if (scheme == PQScheme.FN_DSA_512) { + // FN-DSA (Falcon) keygen uses floating-point sampling; re-deriving the + // keypair from the seed is only guaranteed to reproduce the same key on + // the same CPU architecture + JVM. Prefer the persisted private key as + // the cross-platform backup. + System.out.println("WARNING: for " + scheme.name() + ", the seed is only guaranteed " + + "to reproduce this key on the same architecture/JVM. Back up the persisted " + + "private key for portability."); + } else { + System.out.println("The seed can also be used as a backup to re-derive this key."); + } } catch (Exception e) { System.out.println("GeneratePQKey " + failedHighlight() + " !!! " + e.getMessage()); } finally { @@ -3389,27 +3401,36 @@ private static String extractPQHexArg(String[] parameters) { private static PQKeyMaterial readPQKeyMaterialFromArg( PQScheme scheme, String hexOrPath, int expectedLen) { String hex = hexOrPath; - java.io.File file = new java.io.File(hexOrPath); - if (file.isFile()) { - try { - hex = new String(java.nio.file.Files.readAllBytes(file.toPath()), - java.nio.charset.StandardCharsets.US_ASCII); - } catch (IOException e) { - System.out.println("Failed to read file: " + e.getMessage()); - return null; - } - } - hex = hex.replaceAll("\\s+", ""); int seedLen = PQSchemeRegistry.getSeedLength(scheme); int seedHexLen = seedLen * 2; int extHexLen = expectedLen * 2; + // Disambiguate hex-vs-path: if the argument is already a hex string of + // exactly the seed or persisted-key length, treat it as the key material + // itself, even if a same-named file happens to exist. Only fall back to + // reading a file when the argument is not such a hex string. This avoids a + // valid key being shadowed by a coincidentally-named file. + String trimmedArg = hexOrPath.replaceAll("\\s+", ""); + boolean argIsKeyHex = + (trimmedArg.length() == seedHexLen || trimmedArg.length() == extHexLen) + && trimmedArg.matches("[0-9a-fA-F]+"); + if (!argIsKeyHex) { + File file = new File(hexOrPath); + if (file.isFile()) { + try { + hex = new String(Files.readAllBytes(file.toPath()), StandardCharsets.US_ASCII); + } catch (IOException e) { + System.out.println("Failed to read file: " + e.getMessage()); + return null; + } + } + } + hex = hex.replaceAll("\\s+", ""); if (hex.length() != seedHexLen && hex.length() != extHexLen) { System.out.println("Invalid PQ key length: got " + hex.length() + " hex chars, expected " + seedHexLen + " (seed) or " + extHexLen + " (persisted private key)."); return null; } - byte[] decoded = StringUtils.hexs2Bytes( - hex.getBytes(java.nio.charset.StandardCharsets.US_ASCII)); + byte[] decoded = StringUtils.hexs2Bytes(hex.getBytes(StandardCharsets.US_ASCII)); if (decoded == null) { System.out.println("Invalid PQ key hex (not a valid hex string)."); return null; @@ -3418,7 +3439,7 @@ private static PQKeyMaterial readPQKeyMaterialFromArg( try { PQSchemeRegistry.fromSeed(scheme, decoded); } catch (RuntimeException e) { - java.util.Arrays.fill(decoded, (byte) 0); + Arrays.fill(decoded, (byte) 0); System.out.println("Failed to derive " + scheme.name() + " keypair from seed: " + e.getMessage()); return null; diff --git a/src/main/java/org/tron/walletcli/WalletApiWrapper.java b/src/main/java/org/tron/walletcli/WalletApiWrapper.java index 1966518f2..48357874b 100644 --- a/src/main/java/org/tron/walletcli/WalletApiWrapper.java +++ b/src/main/java/org/tron/walletcli/WalletApiWrapper.java @@ -3379,6 +3379,10 @@ private boolean gasFreeTransferInternal(String receiver, long value, boolean sta return false; } WalletFile activeWalletFile = wallet.getWalletFile(); + // GasFree uses an EIP-712 off-chain permit signature (ECDSA secp256k1 via + // GasFreeApi.signOffChain; recovered with ecrecover by the GasFree + // service/contract), so it is inherently ECDSA-only. Lifting it requires the + // GasFree protocol itself to define a PQ signature scheme. if (activeWalletFile != null && activeWalletFile.getScheme() != null && !activeWalletFile.getScheme().isEmpty()) { diff --git a/src/main/java/org/tron/walletcli/cli/commands/WalletCommands.java b/src/main/java/org/tron/walletcli/cli/commands/WalletCommands.java index 4b35f07f3..acd42f596 100644 --- a/src/main/java/org/tron/walletcli/cli/commands/WalletCommands.java +++ b/src/main/java/org/tron/walletcli/cli/commands/WalletCommands.java @@ -1,5 +1,9 @@ package org.tron.walletcli.cli.commands; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.Arrays; import org.bouncycastle.util.encoders.Hex; import org.tron.common.crypto.pqc.PQSchemeRegistry; @@ -46,6 +50,39 @@ public static void register(CommandRegistry registry) { registerImportWalletPQ(registry); } + /** + * Reads hex-encoded PQ key material from a file, or from stdin when the path + * is "-". Keeps the secret out of the shell history and the process list. + * Returns the whitespace-stripped hex, or {@code null} (after emitting an + * error) on any I/O failure. + */ + private static String readKeyHexFromFileOrStdin(OutputFormatter out, String path) { + try { + byte[] raw; + if ("-".equals(path)) { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + byte[] chunk = new byte[4096]; + int n; + while ((n = System.in.read(chunk)) != -1) { + buf.write(chunk, 0, n); + } + raw = buf.toByteArray(); + } else { + raw = Files.readAllBytes(Paths.get(path)); + } + String hex = new String(raw, StandardCharsets.US_ASCII) + .replaceAll("\\s+", ""); + if (hex.isEmpty()) { + out.error("execution_error", "No key material read from " + path); + return null; + } + return hex; + } catch (java.io.IOException e) { + out.error("execution_error", "Failed to read key material: " + e.getMessage()); + return null; + } + } + private static PQScheme parsePQSchemeOption(OutputFormatter out, ParsedOptions opts) { String schemeName = opts.has("scheme") ? opts.getString("scheme") : "FN_DSA_512"; try { @@ -89,8 +126,16 @@ private static void registerGeneratePQKey(CommandRegistry registry) { json.put("public_key", Hex.toHexString(signer.getPublicKey())); json.put("address", address); json.put("private_key_persisted_form", Hex.toHexString(persisted)); - json.put("warning", - "store the seed and the persisted private key securely."); + // FN-DSA (Falcon) keygen uses floating-point sampling, so the + // seed only reproduces the key on the same architecture/JVM; + // steer Falcon users to the persisted private key as the + // portable backup. ML-DSA keygen is deterministic from the seed. + json.put("warning", scheme == PQScheme.FN_DSA_512 + ? "store the persisted private key securely — it is the portable " + + "backup. For " + scheme.name() + " the seed only reproduces this " + + "key on the same architecture/JVM." + : "store the persisted private key securely; the seed may also be " + + "used as a backup to re-derive this key."); out.success("Generated PQ key for address: " + address, json); } finally { java.util.Arrays.fill(seed, (byte) 0); @@ -149,19 +194,44 @@ private static void registerImportWalletPQ(CommandRegistry registry) { .description("Import a post-quantum wallet from a seed or extended private key") .option("name", "Wallet name", true) .option("scheme", "PQ signature scheme (default: FN_DSA_512)", false) + .option("key-file", + "Path to a file containing the hex-encoded key material, or '-' to read from stdin (preferred: keeps the secret out of shell history and the process list)", false) .option("extended-private-key-hex", - "Hex-encoded key material (either the 48-byte seed or the full extended private||public bytes)", true) + "Hex-encoded key material (either the 48-byte seed or the full extended private||public bytes). Discouraged: visible in shell history and 'ps'; prefer --key-file", false) .handler((ctx, opts, wrapper, out) -> { PQScheme scheme = parsePQSchemeOption(out, opts); if (scheme == null) { return; } - String hex = opts.getString("extended-private-key-hex"); + String hex; + boolean hasKeyFile = opts.has("key-file"); + boolean hasHexArg = opts.has("extended-private-key-hex"); + if (hasKeyFile && hasHexArg) { + // Reject rather than silently ignoring one: the user already + // exposed the --extended-private-key-hex secret on the command + // line, and picking one source implicitly is ambiguous. + out.usageError("Provide exactly one of --key-file or " + + "--extended-private-key-hex, not both", null); + return; + } + if (hasKeyFile) { + hex = readKeyHexFromFileOrStdin(out, opts.getString("key-file")); + if (hex == null) { + return; + } + } else if (hasHexArg) { + hex = opts.getString("extended-private-key-hex"); + } else { + out.usageError("Provide key material via --key-file (or '-' for " + + "stdin); --extended-private-key-hex is discouraged as it exposes " + + "the secret in shell history and the process list", null); + return; + } byte[] decoded; try { decoded = Hex.decode(hex); } catch (Exception e) { - out.usageError("Invalid hex for --extended-private-key-hex", null); + out.usageError("Invalid hex for key material", null); return; } int expectedExtLen = PQSchemeRegistry.getPersistedPrivateKeyLength(scheme); @@ -184,7 +254,7 @@ private static void registerImportWalletPQ(CommandRegistry registry) { extendedPriv = decoded; } else { Arrays.fill(decoded, (byte) 0); - out.usageError("extended-private-key-hex must decode to " + expectedSeedLen + out.usageError("Key material must decode to " + expectedSeedLen + " bytes (seed) or " + expectedExtLen + " bytes (persisted private key) for " + scheme.name() + " (got " + decoded.length + ")", null); return; diff --git a/src/main/java/org/tron/walletserver/WalletApi.java b/src/main/java/org/tron/walletserver/WalletApi.java index 96860939b..2f88888de 100644 --- a/src/main/java/org/tron/walletserver/WalletApi.java +++ b/src/main/java/org/tron/walletserver/WalletApi.java @@ -568,6 +568,9 @@ public static PQScheme getWalletPQScheme(WalletFile walletFile) { // must construct an ApiClient directly should call this before reading the // key out of the keystore, mirroring the pre-flight check in // {@code WalletApiWrapper.gasFreeTransferInternal}. + // TODO(trident-pq): this guard exists only because Trident's ApiClient-based + // signing path cannot attach pq_auth_sig. Once Trident supports PQ signing, + // route these operations through the PQ signing flow and remove the rejection. private void rejectPQForEcdsaSigning(String operation) { WalletFile wf = getWalletFile(); if (wf != null && wf.getScheme() != null && !wf.getScheme().isEmpty()) { @@ -3840,6 +3843,12 @@ public boolean deployContract( .toBuilder(); rawBuilder.setFeeLimit(feeLimit); transBuilder.setRawData(rawBuilder); + // Preserve unknown fields (notably pq_auth_sig / field 6, which Trident's + // Chain.Transaction does not model) so a PQ-carrying transaction is not + // silently stripped when rebuilt to inject the fee limit. + // TODO(trident-pq): once Trident models pq_auth_sig, copy it explicitly + // instead of relying on unknown-field preservation. + transBuilder.setUnknownFields(transactionExtention.getTransaction().getUnknownFields()); for (int i = 0; i < transactionExtention.getTransaction().getSignatureCount(); i++) { ByteString s = transactionExtention.getTransaction().getSignature(i); transBuilder.setSignature(i, s); @@ -3921,6 +3930,12 @@ public Pair deployContractForCli( .toBuilder(); rawBuilder.setFeeLimit(feeLimit); transBuilder.setRawData(rawBuilder); + // Preserve unknown fields (notably pq_auth_sig / field 6, which Trident's + // Chain.Transaction does not model) so a PQ-carrying transaction is not + // silently stripped when rebuilt to inject the fee limit. + // TODO(trident-pq): once Trident models pq_auth_sig, copy it explicitly + // instead of relying on unknown-field preservation. + transBuilder.setUnknownFields(transactionExtention.getTransaction().getUnknownFields()); for (int i = 0; i < transactionExtention.getTransaction().getSignatureCount(); i++) { ByteString s = transactionExtention.getTransaction().getSignature(i); transBuilder.setSignature(i, s); @@ -4022,6 +4037,12 @@ public Triple triggerContract( .toBuilder(); rawBuilder.setFeeLimit(feeLimit); transBuilder.setRawData(rawBuilder); + // Preserve unknown fields (notably pq_auth_sig / field 6, which Trident's + // Chain.Transaction does not model) so a PQ-carrying transaction is not + // silently stripped when rebuilt to inject the fee limit. + // TODO(trident-pq): once Trident models pq_auth_sig, copy it explicitly + // instead of relying on unknown-field preservation. + transBuilder.setUnknownFields(transactionExtention.getTransaction().getUnknownFields()); for (int i = 0; i < transactionExtention.getTransaction().getSignatureCount(); i++) { ByteString s = transactionExtention.getTransaction().getSignature(i); transBuilder.setSignature(i, s); @@ -4108,6 +4129,12 @@ public Triple triggerContractForCli( .toBuilder(); rawBuilder.setFeeLimit(feeLimit); transBuilder.setRawData(rawBuilder); + // Preserve unknown fields (notably pq_auth_sig / field 6, which Trident's + // Chain.Transaction does not model) so a PQ-carrying transaction is not + // silently stripped when rebuilt to inject the fee limit. + // TODO(trident-pq): once Trident models pq_auth_sig, copy it explicitly + // instead of relying on unknown-field preservation. + transBuilder.setUnknownFields(transactionExtention.getTransaction().getUnknownFields()); for (int i = 0; i < transactionExtention.getTransaction().getSignatureCount(); i++) { ByteString s = transactionExtention.getTransaction().getSignature(i); transBuilder.setSignature(i, s); @@ -4175,8 +4202,22 @@ public static long calculateBandwidth(Chain.Transaction transaction) { final long SIGNATURE_PER_BANDWIDTH = 67; final long MAX_RESULT_SIZE_IN_TX = 64; long byteLength = (long) Math.ceil(hexString.length() / 2.0); + // PQ auth signatures are far larger than an ECDSA signature and do not count + // toward getSignatureCount(). Trident's Chain.Transaction does not define + // pq_auth_sig (field 6), so it is preserved as unknown field 6; add its + // serialized size so the estimate is not biased low for PQ transactions. + // TODO(trident-pq): once Trident models pq_auth_sig, sum the serialized + // sizes of getPqAuthSigList() instead of reading unknown field 6. + final int PQ_AUTH_SIG_FIELD_NUMBER = 6; + long pqAuthSigBytes = 0; + com.google.protobuf.UnknownFieldSet unknownFields = transaction.getUnknownFields(); + if (unknownFields.hasField(PQ_AUTH_SIG_FIELD_NUMBER)) { + pqAuthSigBytes = + unknownFields.getField(PQ_AUTH_SIG_FIELD_NUMBER).getSerializedSize(PQ_AUTH_SIG_FIELD_NUMBER); + } long bandwidthBuffer = DATA_HEX_PROTOBUF_EXTRA + SIGNATURE_PER_BANDWIDTH * (transaction.getSignatureCount() + 1) + + pqAuthSigBytes + MAX_RESULT_SIZE_IN_TX; return byteLength + bandwidthBuffer; From 7042a2bede86f3ed56c4ac83cc9d673110a5b3e0 Mon Sep 17 00:00:00 2001 From: federico Date: Wed, 24 Jun 2026 15:06:45 +0800 Subject: [PATCH 2/4] feat(pqc): pass pq_scheme in getCanDelegatedMaxSize --- src/main/java/org/tron/walletcli/Client.java | 33 ++++++++++++++-- .../walletcli/cli/commands/QueryCommands.java | 39 ++++++++++++++++++- .../java/org/tron/walletserver/ApiClient.java | 29 ++++++++++++++ .../java/org/tron/walletserver/WalletApi.java | 20 ++++++++++ src/main/protos/api/api.proto | 1 + 5 files changed, 117 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/tron/walletcli/Client.java b/src/main/java/org/tron/walletcli/Client.java index 01d1bcd1d..8feb6c2b3 100755 --- a/src/main/java/org/tron/walletcli/Client.java +++ b/src/main/java/org/tron/walletcli/Client.java @@ -2224,18 +2224,20 @@ private void getCanWithdrawUnfreezeAmount(String[] parameters) throws CipherExce private void outputGetCanDelegatedMaxSizeTip() { - System.out.println("Using getcandelegatedmaxsize command needs 2 parameters like: "); - System.out.println("getcandelegatedmaxsize ownerAddress type"); + System.out.println("Using getcandelegatedmaxsize command needs 2 or 3 parameters like: "); + System.out.println("getcandelegatedmaxsize ownerAddress type [scheme]"); + System.out.println(" scheme (optional): FN_DSA_512, ML_DSA_44"); } private void getCanDelegatedMaxSize(String[] parameters) throws CipherException, IOException, CancelException { - if (parameters == null || !(parameters.length == 1 || parameters.length == 2)) { + if (parameters == null || !(parameters.length == 1 || parameters.length == 2 || parameters.length == 3)) { this.outputGetCanDelegatedMaxSizeTip(); return; } int index = 0; int type = 0; byte[] ownerAddress = null; + PQScheme scheme = null; if (parameters.length == 1) { try { @@ -2271,10 +2273,33 @@ private void getCanDelegatedMaxSize(String[] parameters) throws CipherException, this.outputGetCanDelegatedMaxSizeTip(); return; } + } else if (parameters.length == 3) { + ownerAddress = getAddressBytes(parameters[index++]); + if (ownerAddress == null) { + this.outputGetCanDelegatedMaxSizeTip(); + return; + } + + try { + type = Integer.parseInt(parameters[index++]); + if (ResourceCode.BANDWIDTH.ordinal() != type && ResourceCode.ENERGY.ordinal() != type) { + System.out.println("getcandelegatedmaxsize type must be: 0 or 1"); + return; + } + } catch (NumberFormatException nfe) { + this.outputGetCanDelegatedMaxSizeTip(); + return; + } + + scheme = parsePQScheme(new String[]{parameters[index]}); + if (scheme == null) { + System.out.println("Unsupported PQ scheme. Supported: " + supportedPQSchemes()); + return; + } } try { - long size = WalletApi.getCanDelegatedMaxSize(ownerAddress, type); + long size = WalletApi.getCanDelegatedMaxSize(ownerAddress, type, scheme); System.out.println("GetCanDelegatedMaxSize=" + size); System.out.println("GetCanDelegatedMaxSize " + successfulHighlight() + " !!!"); } catch (Exception e) { diff --git a/src/main/java/org/tron/walletcli/cli/commands/QueryCommands.java b/src/main/java/org/tron/walletcli/cli/commands/QueryCommands.java index b03aa74ce..7acedf02b 100644 --- a/src/main/java/org/tron/walletcli/cli/commands/QueryCommands.java +++ b/src/main/java/org/tron/walletcli/cli/commands/QueryCommands.java @@ -1,9 +1,13 @@ package org.tron.walletcli.cli.commands; import com.alibaba.fastjson.JSON; +import org.tron.common.crypto.pqc.PQSchemeRegistry; +import org.tron.protos.Protocol.PQScheme; import org.tron.walletcli.cli.CommandDefinition; import org.tron.walletcli.cli.CommandRegistry; import org.tron.walletcli.cli.OptionDef; +import org.tron.walletcli.cli.OutputFormatter; +import org.tron.walletcli.cli.ParsedOptions; import org.tron.walletserver.WalletApi; import org.tron.trident.proto.Chain; import org.tron.trident.proto.Common; @@ -23,6 +27,33 @@ private static CommandDefinition.Builder noAuthCommand() { return CommandDefinition.builder().authPolicy(CommandDefinition.AuthPolicy.NEVER); } + /** + * Parses the optional {@code --scheme} option as a registered PQ scheme. + * Returns {@code null} when the option is absent (meaning "use the ECDSA-sized + * estimate"), and also returns {@code null} after emitting an error when the + * option is present but invalid. Callers should check {@code opts.has("scheme")} + * to distinguish "not provided" from "invalid". + */ + private static PQScheme parseOptionalPQScheme(OutputFormatter out, ParsedOptions opts) { + if (!opts.has("scheme")) { + return null; + } + String schemeName = opts.getString("scheme"); + try { + PQScheme scheme = PQScheme.valueOf(schemeName); + if (!PQSchemeRegistry.contains(scheme)) { + out.usageError("Unsupported PQ scheme: " + schemeName + + ". Supported: FN_DSA_512, ML_DSA_44", null); + return null; + } + return scheme; + } catch (IllegalArgumentException e) { + out.usageError("Unknown PQ scheme: " + schemeName + + ". Supported: FN_DSA_512, ML_DSA_44", null); + return null; + } + } + public static void register(CommandRegistry registry) { registerGetAddress(registry); registerGetBalance(registry); @@ -653,11 +684,17 @@ private static void registerGetCanDelegatedMaxSize(CommandRegistry registry) { .description("Get max delegatable size for a resource type") .option("owner", "Owner address", true) .option("type", "Resource type (0=BANDWIDTH, 1=ENERGY)", true, OptionDef.Type.LONG) + .option("scheme", "PQ signature scheme (e.g. FN_DSA_512, ML_DSA_44). When set, the BANDWIDTH estimate reserves room for the PQAuthSig wire size instead of ECDSA", false) .handler((ctx, opts, wrapper, out) -> { int type = opts.getInt("type"); CommandSupport.requireResourceCode(out, "type", type); + PQScheme pqScheme = parseOptionalPQScheme(out, opts); + if (pqScheme == null && opts.has("scheme")) { + // parseOptionalPQScheme already emitted an error + return; + } long maxSize = WalletApi.getCanDelegatedMaxSize( - opts.getAccountAddress("owner"), type); + opts.getAccountAddress("owner"), type, pqScheme); Map json = new LinkedHashMap(); json.put("max_size", maxSize); out.success("Max delegatable size: " + maxSize, json); diff --git a/src/main/java/org/tron/walletserver/ApiClient.java b/src/main/java/org/tron/walletserver/ApiClient.java index b274b4d0d..6551d2cb1 100644 --- a/src/main/java/org/tron/walletserver/ApiClient.java +++ b/src/main/java/org/tron/walletserver/ApiClient.java @@ -25,6 +25,7 @@ import org.tron.trident.proto.Chain; import org.tron.trident.proto.Common; import org.tron.trident.proto.Contract; +import org.tron.protos.Protocol.PQScheme; import org.tron.trident.proto.Response; import org.tron.walletcli.ApiClientFactory; @@ -461,6 +462,34 @@ public long getCanDelegatedMaxSize(byte[] ownerAddress, int type) {// pass } } + /** + * Get the max delegatable size for a resource type, optionally accounting for + * the post-quantum signature overhead of a registered PQ scheme. + * + * @param ownerAddress owner address bytes + * @param type resource type (0 = BANDWIDTH, 1 = ENERGY) + * @param pqScheme the PQ scheme to reserve bandwidth for, or null/UNKNOWN_PQ_SCHEME + * for the default ECDSA-sized estimate + * @return max delegatable size + */ + public long getCanDelegatedMaxSize(byte[] ownerAddress, int type, PQScheme pqScheme) { + // TODO(trident-sdk): Trident's ApiWrapper.getCanDelegatedMaxSize does not yet accept a + // PQScheme parameter. Once the Trident SDK is updated (add pq_scheme to its + // CanDelegatedMaxSizeRequestMessage proto and regenerate), pass pqScheme to the + // ApiWrapper call below. Until then, the scheme hint is silently ignored and the + // node returns the ECDSA-sized estimate (backward-compatible default). + // + // When the SDK is ready, replace this body with: + // if (!emptySolidityNode) { + // return client.getCanDelegatedMaxSize(encode58Check(ownerAddress), type, + // pqScheme, SOLIDITY_NODE); + // } else { + // return client.getCanDelegatedMaxSize(encode58Check(ownerAddress), type, + // pqScheme, FULL_NODE); + // } + return getCanDelegatedMaxSize(ownerAddress, type); + } + public long getAvailableUnfreezeCount(byte[] ownerAddress) {// pass if (!emptySolidityNode) { return client.getAvailableUnfreezeCount(encode58Check(ownerAddress), SOLIDITY_NODE); diff --git a/src/main/java/org/tron/walletserver/WalletApi.java b/src/main/java/org/tron/walletserver/WalletApi.java index 2f88888de..e98adf94d 100644 --- a/src/main/java/org/tron/walletserver/WalletApi.java +++ b/src/main/java/org/tron/walletserver/WalletApi.java @@ -2577,6 +2577,12 @@ public boolean delegateResource(byte[] ownerAddress, long balance System.out.println("delegateBalance must be greater than or equal to 1 TRX"); return false; } + // TODO(pq-delegate): When delegating from a PQ wallet, the bandwidth consumed by the + // delegate transaction itself is larger (PQAuthSig vs ECDSA sig). Pass the active + // wallet's PQ scheme so the node reserves room for the larger signature — without it, + // the returned max may be too large and the subsequent delegate transaction could fail + // with insufficient bandwidth. Requires the Trident SDK to accept a PQScheme parameter + // in its getCanDelegatedMaxSize call (see TODO in ApiClient.java). long canDelegatedMaxSize = apiCli.getCanDelegatedMaxSize(ownerAddress, resourceCode); if (balance > canDelegatedMaxSize) { System.out.println("delegateBalance must be less than or equal to available FreezeV2 balance"); @@ -3015,6 +3021,20 @@ public static long getCanDelegatedMaxSize(byte[] ownerAddress, int type) { return apiCli.getCanDelegatedMaxSize(ownerAddress, type); } + /** + * Get the max delegatable size for a resource type, optionally accounting for + * the post-quantum signature overhead. + * + * @param ownerAddress owner address bytes + * @param type resource type (0 = BANDWIDTH, 1 = ENERGY) + * @param pqScheme the PQ scheme to reserve bandwidth for, or null/UNKNOWN_PQ_SCHEME + * for the default ECDSA-sized estimate + * @return max delegatable size + */ + public static long getCanDelegatedMaxSize(byte[] ownerAddress, int type, PQScheme pqScheme) { + return apiCli.getCanDelegatedMaxSize(ownerAddress, type, pqScheme); + } + public static long getAvailableUnfreezeCount(byte[] ownerAddress) { return apiCli.getAvailableUnfreezeCount(ownerAddress); } diff --git a/src/main/protos/api/api.proto b/src/main/protos/api/api.proto index 1b479a8a5..185a92b08 100644 --- a/src/main/protos/api/api.proto +++ b/src/main/protos/api/api.proto @@ -1111,6 +1111,7 @@ message GetAvailableUnfreezeCountResponseMessage { message CanDelegatedMaxSizeRequestMessage { int32 type = 1; bytes owner_address = 2; + PQScheme pq_scheme = 3; } message CanDelegatedMaxSizeResponseMessage { int64 max_size = 1; From 8bf69525e9cf1b71dd58b7ffb33e9c47186c90db Mon Sep 17 00:00:00 2001 From: federico Date: Wed, 24 Jun 2026 16:10:03 +0800 Subject: [PATCH 3/4] fix(pqc): avoid signer leak in validPassword and qualify Falcon seed docs --- README.md | 5 +- src/main/java/org/tron/keystore/Wallet.java | 64 ++++++++++++++++++--- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d93343a8c..d80720a25 100644 --- a/README.md +++ b/README.md @@ -1876,8 +1876,9 @@ a file path. Within each source the form is then auto-detected by length against the chosen scheme: > > - **FN_DSA_512**: 48-byte / 96-hex-char seed (derives the full keypair -> deterministically), or 2176-byte / 4352-hex-char extended private key -> (`f‖g‖F‖h`, used verbatim). +> deterministically **on the same CPU architecture and JVM**; cross-platform +> replay is not guaranteed — see the backup caveat above), or 2176-byte / +> 4352-hex-char extended private key (`f‖g‖F‖h`, used verbatim). > - **ML_DSA_44**: 32-byte / 64-hex-char seed, or 2560-byte / 5120-hex-char > encoded private key (`rho‖K‖tr‖s1‖s2‖t0`). > diff --git a/src/main/java/org/tron/keystore/Wallet.java b/src/main/java/org/tron/keystore/Wallet.java index 2fc627903..f31c566a9 100644 --- a/src/main/java/org/tron/keystore/Wallet.java +++ b/src/main/java/org/tron/keystore/Wallet.java @@ -285,14 +285,12 @@ public static boolean validPassword (byte[] password, WalletFile walletFile) // PQ keystores may be seed-only (no main ciphertext/mac), so the ECDSA-style // ciphertext/mac check below would wrongly report "Invalid password". Route - // them through the PQ verify-and-decrypt path, which validates whichever - // segments (ext and/or seed) are actually present. The non-null + non-empty - // scheme test matches how the rest of the codebase distinguishes PQ wallets - // from legacy ECDSA ones (scheme null/empty == ECDSA). + // them through a MAC-only validation path that does not decrypt key material + // or construct a signer, so no private-key bytes leak into heap. The + // non-null + non-empty scheme test matches how the rest of the codebase + // distinguishes PQ wallets from legacy ECDSA ones (scheme null/empty == ECDSA). if (walletFile.getScheme() != null && !walletFile.getScheme().isEmpty()) { - PQKeyMaterial material = verifyAndDecryptPQ(password, walletFile); - material.clearSecrets(); - return true; + return validatePasswordPQ(password, walletFile); } WalletFile.Crypto crypto = walletFile.getCrypto(); @@ -560,6 +558,58 @@ void clearSecrets() { } } + // Password-validation-only path for PQ keystores. Verifies MACs (and + // structural invariants) without decrypting any key material or constructing + // a signer, so no private-key bytes leak into heap. Used by validPassword to + // avoid the signer-retention issue of the full verifyAndDecryptPQ path. + private static boolean validatePasswordPQ(byte[] password, WalletFile walletFile) + throws CipherException { + if (walletFile.getScheme() == null) { + throw new CipherException("Wallet has no PQ scheme tag"); + } + final PQScheme scheme; + try { + scheme = PQScheme.valueOf(walletFile.getScheme()); + } catch (IllegalArgumentException e) { + throw new CipherException("Unsupported PQ scheme: " + walletFile.getScheme(), e); + } + if (!PQSchemeRegistry.contains(scheme)) { + throw new CipherException("Unsupported PQ scheme: " + scheme); + } + + validate(walletFile); + WalletFile.Crypto crypto = walletFile.getCrypto(); + + boolean extPresent = isExtSegmentPresent(crypto); + boolean seedPresent = isSeedSegmentPresent(crypto); + requireSegmentShape(crypto, extPresent, seedPresent); + if (!extPresent && !seedPresent) { + throw new CipherException( + "PQ wallet has neither ciphertext nor seedciphertext"); + } + + byte[] derivedKey = deriveScryptKey(password, crypto); + try { + if (extPresent) { + byte[] extCipherText = ByteArray.fromHexString(crypto.getCiphertext()); + byte[] storedMac = ByteArray.fromHexString(crypto.getMac()); + if (!Arrays.equals(generateMac(derivedKey, extCipherText), storedMac)) { + throw new CipherException("Invalid password provided"); + } + } + if (seedPresent) { + byte[] seedCipherText = ByteArray.fromHexString(crypto.getSeedciphertext()); + byte[] storedMac = ByteArray.fromHexString(crypto.getSeedmac()); + if (!Arrays.equals(generateMac(derivedKey, seedCipherText), storedMac)) { + throw new CipherException("Invalid password provided"); + } + } + return true; + } finally { + StringUtils.clear(derivedKey); + } + } + // Shared decrypt-and-verify core for PQ keystores, used by both decryptPQ and // reEncryptPQ so the security checks cannot drift between them. Resolves and // validates the scheme, verifies every persisted segment's MAC under From 23525f08afac35542acedf40cac64a3b137d9802 Mon Sep 17 00:00:00 2001 From: federico Date: Wed, 24 Jun 2026 17:08:13 +0800 Subject: [PATCH 4/4] refactor(pqc): share sig field const and drop dup validate --- src/main/java/org/tron/common/utils/TransactionUtils.java | 5 +++-- src/main/java/org/tron/keystore/Wallet.java | 1 - src/main/java/org/tron/walletcli/Client.java | 2 ++ .../org/tron/walletcli/cli/commands/QueryCommands.java | 2 +- src/main/java/org/tron/walletserver/WalletApi.java | 7 +++---- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/tron/common/utils/TransactionUtils.java b/src/main/java/org/tron/common/utils/TransactionUtils.java index b43a21264..96713c633 100644 --- a/src/main/java/org/tron/common/utils/TransactionUtils.java +++ b/src/main/java/org/tron/common/utils/TransactionUtils.java @@ -55,10 +55,11 @@ public class TransactionUtils { // Protobuf field number of Transaction.pq_auth_sig (Tron.proto). The Trident // SDK's Chain.Transaction does not define this field, so on those objects PQ - // auth signatures are preserved only as unknown field 6. + // auth signatures are preserved only as unknown field 6. Shared so the + // bandwidth estimate in WalletApi reads the same field number. // TODO(trident-pq): once the Trident SDK models pq_auth_sig on // Chain.Transaction, drop this constant and use the generated accessors. - private static final int PQ_AUTH_SIG_FIELD_NUMBER = 6; + public static final int PQ_AUTH_SIG_FIELD_NUMBER = 6; /** True if the (possibly Trident-typed) transaction carries any PQ auth signature. */ // TODO(trident-pq): replace the unknown-field probe with diff --git a/src/main/java/org/tron/keystore/Wallet.java b/src/main/java/org/tron/keystore/Wallet.java index f31c566a9..607060706 100644 --- a/src/main/java/org/tron/keystore/Wallet.java +++ b/src/main/java/org/tron/keystore/Wallet.java @@ -577,7 +577,6 @@ private static boolean validatePasswordPQ(byte[] password, WalletFile walletFile throw new CipherException("Unsupported PQ scheme: " + scheme); } - validate(walletFile); WalletFile.Crypto crypto = walletFile.getCrypto(); boolean extPresent = isExtSegmentPresent(crypto); diff --git a/src/main/java/org/tron/walletcli/Client.java b/src/main/java/org/tron/walletcli/Client.java index 8feb6c2b3..aa04a75e5 100755 --- a/src/main/java/org/tron/walletcli/Client.java +++ b/src/main/java/org/tron/walletcli/Client.java @@ -2227,6 +2227,8 @@ private void outputGetCanDelegatedMaxSizeTip() { System.out.println("Using getcandelegatedmaxsize command needs 2 or 3 parameters like: "); System.out.println("getcandelegatedmaxsize ownerAddress type [scheme]"); System.out.println(" scheme (optional): FN_DSA_512, ML_DSA_44"); + System.out.println(" note: scheme is currently a no-op pending Trident SDK support;" + + " the returned size is unaffected."); } private void getCanDelegatedMaxSize(String[] parameters) throws CipherException, IOException, CancelException { diff --git a/src/main/java/org/tron/walletcli/cli/commands/QueryCommands.java b/src/main/java/org/tron/walletcli/cli/commands/QueryCommands.java index 7acedf02b..c6ad54ac6 100644 --- a/src/main/java/org/tron/walletcli/cli/commands/QueryCommands.java +++ b/src/main/java/org/tron/walletcli/cli/commands/QueryCommands.java @@ -684,7 +684,7 @@ private static void registerGetCanDelegatedMaxSize(CommandRegistry registry) { .description("Get max delegatable size for a resource type") .option("owner", "Owner address", true) .option("type", "Resource type (0=BANDWIDTH, 1=ENERGY)", true, OptionDef.Type.LONG) - .option("scheme", "PQ signature scheme (e.g. FN_DSA_512, ML_DSA_44). When set, the BANDWIDTH estimate reserves room for the PQAuthSig wire size instead of ECDSA", false) + .option("scheme", "PQ signature scheme (e.g. FN_DSA_512, ML_DSA_44). Reserved for accounting the larger PQAuthSig wire size in the BANDWIDTH estimate; currently a no-op pending Trident SDK support, so the returned value is unaffected", false) .handler((ctx, opts, wrapper, out) -> { int type = opts.getInt("type"); CommandSupport.requireResourceCode(out, "type", type); diff --git a/src/main/java/org/tron/walletserver/WalletApi.java b/src/main/java/org/tron/walletserver/WalletApi.java index e98adf94d..499763fe8 100644 --- a/src/main/java/org/tron/walletserver/WalletApi.java +++ b/src/main/java/org/tron/walletserver/WalletApi.java @@ -4228,12 +4228,11 @@ public static long calculateBandwidth(Chain.Transaction transaction) { // serialized size so the estimate is not biased low for PQ transactions. // TODO(trident-pq): once Trident models pq_auth_sig, sum the serialized // sizes of getPqAuthSigList() instead of reading unknown field 6. - final int PQ_AUTH_SIG_FIELD_NUMBER = 6; long pqAuthSigBytes = 0; com.google.protobuf.UnknownFieldSet unknownFields = transaction.getUnknownFields(); - if (unknownFields.hasField(PQ_AUTH_SIG_FIELD_NUMBER)) { - pqAuthSigBytes = - unknownFields.getField(PQ_AUTH_SIG_FIELD_NUMBER).getSerializedSize(PQ_AUTH_SIG_FIELD_NUMBER); + if (unknownFields.hasField(TransactionUtils.PQ_AUTH_SIG_FIELD_NUMBER)) { + pqAuthSigBytes = unknownFields.getField(TransactionUtils.PQ_AUTH_SIG_FIELD_NUMBER) + .getSerializedSize(TransactionUtils.PQ_AUTH_SIG_FIELD_NUMBER); } long bandwidthBuffer = DATA_HEX_PROTOBUF_EXTRA + SIGNATURE_PER_BANDWIDTH * (transaction.getSignatureCount() + 1)