Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@
import org.tron.core.exception.ContractValidateException;
import org.tron.core.store.AccountStore;
import org.tron.core.store.DynamicPropertiesStore;
import org.tron.common.crypto.pqc.FNDSA;
import org.tron.common.crypto.pqc.MLDSA44;
import org.tron.common.crypto.pqc.MLDSA65;
import org.tron.common.crypto.pqc.PQSignatureRegistry;
import org.tron.protos.Protocol.Key;
import org.tron.protos.Protocol.Permission;
import org.tron.protos.Protocol.Permission.PermissionType;
import org.tron.protos.Protocol.SignatureScheme;
import org.tron.protos.Protocol.Transaction.Contract.ContractType;
import org.tron.protos.Protocol.Transaction.Result.code;
import org.tron.protos.contract.AccountContract.AccountPermissionUpdateContract;
Expand Down Expand Up @@ -95,15 +100,38 @@ private boolean checkPermission(Permission permission) throws ContractValidateEx
long weightSum = 0;
List<ByteString> addressList = permission.getKeysList()
.stream()
.map(x -> x.getAddress())
.map(Key::getAddress)
.filter(addr -> !addr.isEmpty())
.distinct()
.collect(toList());
if (addressList.size() != permission.getKeysList().size()) {
long nonEmptyAddrCount = permission.getKeysList().stream()
.map(Key::getAddress)
.filter(addr -> !addr.isEmpty())
.count();
if (addressList.size() != nonEmptyAddrCount) {
throw new ContractValidateException(
"address should be distinct in permission " + permission.getType());
}
validatePermissionScheme(permission);

List<ByteString> publicKeyList = permission.getKeysList()
.stream()
.map(k -> k.hasPqKey() ? k.getPqKey().getPublicKey() : ByteString.EMPTY)
.filter(pk -> !pk.isEmpty())
.distinct()
.collect(toList());
long nonEmptyPublicKeyCount = permission.getKeysList().stream()
.map(k -> k.hasPqKey() ? k.getPqKey().getPublicKey() : ByteString.EMPTY)
.filter(pk -> !pk.isEmpty())
.count();
if (publicKeyList.size() != nonEmptyPublicKeyCount) {
throw new ContractValidateException(
"public_key should be distinct in permission " + permission.getType());
}

for (Key key : permission.getKeysList()) {
if (!DecodeUtil.addressValid(key.getAddress().toByteArray())) {
if (!key.getAddress().isEmpty()
&& !DecodeUtil.addressValid(key.getAddress().toByteArray())) {
Comment on lines +133 to +134
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Empty addresses should not be allowed for UNKNOWN_SIG_SCHEME (legacy ECDSA) keys since the address is the identity used for signature verification. A key with empty address and no PQ key passes all validation but can never sign, potentially creating an unsatisfiable permission. Consider requiring a valid address when the key's scheme is UNKNOWN_SIG_SCHEME.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At actuator/src/main/java/org/tron/core/actuator/AccountPermissionUpdateActuator.java, line 133:

<comment>Empty addresses should not be allowed for `UNKNOWN_SIG_SCHEME` (legacy ECDSA) keys since the address is the identity used for signature verification. A key with empty address and no PQ key passes all validation but can never sign, potentially creating an unsatisfiable permission. Consider requiring a valid address when the key's scheme is `UNKNOWN_SIG_SCHEME`.</comment>

<file context>
@@ -125,7 +130,8 @@ private boolean checkPermission(Permission permission) throws ContractValidateEx
 
     for (Key key : permission.getKeysList()) {
-      if (!DecodeUtil.addressValid(key.getAddress().toByteArray())) {
+      if (!key.getAddress().isEmpty()
+          && !DecodeUtil.addressValid(key.getAddress().toByteArray())) {
         throw new ContractValidateException("key is not a validate address");
</file context>
Suggested change
if (!key.getAddress().isEmpty()
&& !DecodeUtil.addressValid(key.getAddress().toByteArray())) {
SignatureScheme scheme = keyScheme(key);
if (scheme == SignatureScheme.UNKNOWN_SIG_SCHEME
&& !DecodeUtil.addressValid(key.getAddress().toByteArray())) {
throw new ContractValidateException("key is not a validate address");
}
if (scheme != SignatureScheme.UNKNOWN_SIG_SCHEME
&& !key.getAddress().isEmpty()
&& !DecodeUtil.addressValid(key.getAddress().toByteArray())) {
Fix with Cubic

throw new ContractValidateException("key is not a validate address");
}
if (key.getWeight() <= 0) {
Expand Down Expand Up @@ -237,4 +265,75 @@ public ByteString getOwnerAddress() throws InvalidProtocolBufferException {
public long calcFee() {
return chainBaseManager.getDynamicPropertiesStore().getUpdateAccountPermissionFee();
}

private void validatePermissionScheme(Permission permission) throws ContractValidateException {
DynamicPropertiesStore dynamicStore = chainBaseManager.getDynamicPropertiesStore();

SignatureScheme first = keyScheme(permission.getKeysList().get(0));
for (Key key : permission.getKeysList()) {
SignatureScheme scheme = keyScheme(key);
if (scheme != first) {
throw new ContractValidateException(
"all keys in a permission must use the same scheme");
}
if (scheme == SignatureScheme.UNKNOWN_SIG_SCHEME) {
if (key.hasPqKey() && !key.getPqKey().getPublicKey().isEmpty()) {
throw new ContractValidateException(
"public_key must be empty when scheme is UNKNOWN_SIG_SCHEME");
}
} else {
if (!dynamicStore.isPqSchemeAllowed(scheme)) {
throw new ContractValidateException(
schemeNotActivatedMessage(scheme) + ", scheme " + scheme + " is not allowed");
}
int expected = expectedPublicKeyLength(scheme);
if (expected < 0) {
throw new ContractValidateException(
"unsupported signature scheme: " + scheme);
}
int actual = key.hasPqKey() ? key.getPqKey().getPublicKey().size() : 0;
if (actual != expected) {
throw new ContractValidateException(
"public_key length for " + scheme + " must be " + expected + " bytes, got "
+ actual);
}
}
}

if (permission.getType() == PermissionType.Witness
&& first != SignatureScheme.UNKNOWN_SIG_SCHEME
&& !PQSignatureRegistry.contains(first)) {
throw new ContractValidateException(
"Witness permission only supports legacy or registered PQ schemes, got " + first);
}
}

private static SignatureScheme keyScheme(Key key) {
return key.hasPqKey() ? key.getPqKey().getScheme() : SignatureScheme.UNKNOWN_SIG_SCHEME;
}

private static int expectedPublicKeyLength(SignatureScheme scheme) {
switch (scheme) {
case ML_DSA_44:
return MLDSA44.PUBLIC_KEY_LENGTH;
case ML_DSA_65:
return MLDSA65.PUBLIC_KEY_LENGTH;
case FN_DSA:
return FNDSA.PUBLIC_KEY_LENGTH;
default:
return -1;
}
}

private static String schemeNotActivatedMessage(SignatureScheme scheme) {
switch (scheme) {
case ML_DSA_44:
case ML_DSA_65:
return "ML-DSA is not activated";
case FN_DSA:
return "FN-DSA is not activated";
default:
return scheme + " is not activated";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import java.util.Arrays;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import org.tron.common.crypto.pqc.PQSignatureRegistry;
import org.tron.common.utils.DecodeUtil;
import org.tron.common.utils.StringUtil;
import org.tron.core.capsule.AccountCapsule;
Expand All @@ -15,6 +17,8 @@
import org.tron.core.exception.ContractValidateException;
import org.tron.core.store.AccountStore;
import org.tron.core.store.DynamicPropertiesStore;
import org.tron.protos.Protocol.PQPublicKey;
import org.tron.protos.Protocol.SignatureScheme;
import org.tron.protos.Protocol.Transaction.Contract.ContractType;
import org.tron.protos.Protocol.Transaction.Result.code;
import org.tron.protos.contract.AccountContract.AccountCreateContract;
Expand Down Expand Up @@ -120,6 +124,25 @@ public boolean validate() throws ContractValidateException {
throw new ContractValidateException("Account has existed");
}

if (contract.hasPqKey()) {
PQPublicKey pq = contract.getPqKey();
SignatureScheme scheme = pq.getScheme();
DynamicPropertiesStore dyn = chainBaseManager.getDynamicPropertiesStore();
if (!dyn.isPqSchemeAllowed(scheme)) {
throw new ContractValidateException("PQ scheme not activated: " + scheme);
}
byte[] pubKey = pq.getPublicKey().toByteArray();
if (pubKey.length != PQSignatureRegistry.getPublicKeyLength(scheme)) {
throw new ContractValidateException(
"Invalid PQ public key length for scheme " + scheme);
}
byte[] derived = PQSignatureRegistry.computeAddress(scheme, pubKey);
if (!Arrays.equals(derived, accountAddress)) {
throw new ContractValidateException(
"account_address does not match the address derived from pq_key");
}
}

return true;
}

Expand Down
26 changes: 25 additions & 1 deletion actuator/src/main/java/org/tron/core/utils/ProposalUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,28 @@ public static void validator(DynamicPropertiesStore dynamicPropertiesStore,
}
break;
}
case ALLOW_ML_DSA: {
if (dynamicPropertiesStore.getAllowMlDsa() == 1) {
throw new ContractValidateException(
"[ALLOW_ML_DSA] has been valid, no need to propose again");
}
if (value != 1) {
throw new ContractValidateException(
"This value[ALLOW_ML_DSA] is only allowed to be 1");
}
break;
}
case ALLOW_FN_DSA: {
if (dynamicPropertiesStore.getAllowFnDsa() == 1) {
throw new ContractValidateException(
"[ALLOW_FN_DSA] has been valid, no need to propose again");
}
if (value != 1) {
throw new ContractValidateException(
"This value[ALLOW_FN_DSA] is only allowed to be 1");
}
break;
}
default:
break;
}
Expand Down Expand Up @@ -971,7 +993,9 @@ public enum ProposalType { // current value, value range
ALLOW_TVM_BLOB(89), // 0, 1
PROPOSAL_EXPIRE_TIME(92), // (0, 31536003000)
ALLOW_TVM_SELFDESTRUCT_RESTRICTION(94), // 0, 1
ALLOW_TVM_OSAKA(96); // 0, 1
ALLOW_TVM_OSAKA(96), // 0, 1
ALLOW_ML_DSA(97), // 0, 1
ALLOW_FN_DSA(100); // 0, 1

private long code;

Expand Down
Loading
Loading