From f8c5fb06199f808518c0833e2fd536fa49bd8d00 Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Fri, 27 Mar 2026 18:25:34 +0800 Subject: [PATCH 1/6] feat(protocol): add protoLint script for enum validation --- gradle/verification-metadata.xml | 23 +++++ protocol/build.gradle | 1 + protocol/protoLint.gradle | 167 +++++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+) create mode 100644 protocol/protoLint.gradle diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 4d0bf1013d6..86880157f35 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -26,6 +26,29 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/protocol/build.gradle b/protocol/build.gradle index 789d27b6360..04d970b59db 100644 --- a/protocol/build.gradle +++ b/protocol/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.google.protobuf' +apply from: 'protoLint.gradle' def protobufVersion = '3.25.8' def grpcVersion = '1.75.0' diff --git a/protocol/protoLint.gradle b/protocol/protoLint.gradle new file mode 100644 index 00000000000..8095d1aebf4 --- /dev/null +++ b/protocol/protoLint.gradle @@ -0,0 +1,167 @@ +/** + * This is a Gradle script for proto linting. + * + * Implementation: + * 1. Integrates the 'buf' CLI tool to compile .proto files and generate a JSON AST (Abstract Syntax Tree) image. + * 2. Uses Groovy's JsonSlurper to parse the AST image. + * 3. Traverses all Enum definitions and validates them against preset rules. + * + * Current Validation: + * Enforces the java-tron API evolution standard (see https://github.com/tronprotocol/java-tron/issues/6515). + * Except for legacy enums in the 'legacyEnums' whitelist, all newly defined Enums MUST reserve index 0 for a field starting with 'UNKNOWN_'. + * This ensures robust forward/backward compatibility during proto3 JSON serialization. + */ +import groovy.json.JsonSlurper +import org.gradle.internal.os.OperatingSystem + +// Define the required buf CLI version +def bufVersion = "1.61.0" +def currentOs = OperatingSystem.current() +def platform = currentOs.isMacOsX() ? "osx" : (currentOs.isWindows() ? "windows" : "linux") +def machine = rootProject.archInfo.isArm64 ? "aarch_64" : "x86_64" + +// Create a custom configuration for the buf CLI tool to keep it isolated from the classpath +configurations { + bufTool +} + +// Depend on the buf executable published on Maven Central +dependencies { + bufTool "build.buf:buf:${bufVersion}:${platform}-${machine}@exe" +} + +task protoLint { + group = "verification" + description = "Validate Protobuf Enums using buf generated JSON AST. Enforces 'UNKNOWN_' prefix for index 0 to ensure JSON serialization backward compatibility." + + // Explicitly declare dependency to avoid Gradle implicit dependency warnings + // because generateProto outputs to the parent 'src' directory. + dependsOn 'generateProto' + + // Incremental build support: Only run if proto files or the script itself changes + inputs.dir('src/main/protos') + inputs.file('protoLint.gradle') + + def markerFile = file("${buildDir}/tmp/protoLint.done") + outputs.file(markerFile) + + doLast { + def bufExe = configurations.bufTool.singleFile + if (!bufExe.exists() || !bufExe.canExecute()) { + bufExe.setExecutable(true) + } + + // 1. Legacy Whitelist + // Contains enums that existed before the 'UNKNOWN_' standard was enforced. + // Format: "filename.proto:EnumName" or "filename.proto:MessageName.EnumName" + def legacyEnums = [ + "core/contract/common.proto:ResourceCode", + "core/contract/smart_contract.proto:SmartContract.ABI.Entry.EntryType", + "core/contract/smart_contract.proto:SmartContract.ABI.Entry.StateMutabilityType", + "core/Tron.proto:AccountType", + "core/Tron.proto:ReasonCode", + "core/Tron.proto:Proposal.State", + "core/Tron.proto:MarketOrder.State", + "core/Tron.proto:Permission.PermissionType", + "core/Tron.proto:Transaction.Contract.ContractType", + "core/Tron.proto:Transaction.Result.code", + "core/Tron.proto:Transaction.Result.contractResult", + "core/Tron.proto:TransactionInfo.code", + "core/Tron.proto:BlockInventory.Type", + "core/Tron.proto:Inventory.InventoryType", + "core/Tron.proto:Items.ItemType", + "core/Tron.proto:PBFTMessage.MsgType", + "core/Tron.proto:PBFTMessage.DataType", + "api/api.proto:Return.response_code", + "api/api.proto:TransactionSignWeight.Result.response_code", + "api/api.proto:TransactionApprovedList.Result.response_code", + "api/zksnark.proto:ZksnarkResponse.Code" + ].collect { it.toString() } as Set + + // 2. Build JSON AST Image using buf CLI + def imageDir = file("${buildDir}/tmp/buf") + def imageFile = file("${imageDir}/proto-ast.json") + imageDir.mkdirs() + + println "šŸ” Generating Proto AST image using buf CLI..." + + def bufConfig = '{"version":"v1beta1","build":{"roots":["src/main/protos","build/extracted-include-protos/main"]}}' + + def execResult = exec { + commandLine bufExe.absolutePath, 'build', '.', '--config', bufConfig, '-o', "${imageFile.absolutePath}#format=json" + ignoreExitValue = true + } + + if (execResult.exitValue != 0) { + throw new GradleException("Failed to generate AST image. Ensure your .proto files are valid. Buf exited with code ${execResult.exitValue}") + } + + if (!imageFile.exists()) { + throw new GradleException("Failed to locate generated buf image at ${imageFile.absolutePath}") + } + + // 3. Parse AST and Validate Enums + def descriptorSet + try { + descriptorSet = new JsonSlurper().parse(imageFile) + } catch (Exception e) { + throw new GradleException("Failed to parse buf generated JSON AST: ${e.message}", e) + } + + def errors = [] + + descriptorSet.file?.each { protoFile -> + // Skip Google's and gRPC's internal protos as they are outside our control + if (protoFile.name?.startsWith("google/") || protoFile.name?.startsWith("grpc/")) { + return + } + + // A queue-based (BFS) approach to safely traverse all nested messages and enums + // without using recursion, ensuring support for any nesting depth. + def queue = [] + + // Initial seed: top-level enums and messages + protoFile.enumType?.each { queue << [def: it, parentName: ""] } + protoFile.messageType?.each { queue << [def: it, parentName: ""] } + + while (!queue.isEmpty()) { + def item = queue.remove(0) + def definition = item.def + def parentName = item.parentName + + if (definition.value != null) { + // This is an Enum definition + def fullName = parentName ? "${parentName}.${definition.name}" : definition.name + def identifier = "${protoFile.name}:${fullName}".toString() + + if (!legacyEnums.contains(identifier)) { + def zeroValue = definition.value?.find { it.number == 0 } + if (zeroValue && !zeroValue.name?.startsWith("UNKNOWN_")) { + errors << "[${protoFile.name}] Enum \"${fullName}\" has index 0: \"${zeroValue.name}\". It MUST start with \"UNKNOWN_\"." + } + } + } else { + // This is a Message definition, look for nested enums and nested messages + def currentMsgName = parentName ? "${parentName}.${definition.name}" : definition.name + + definition.enumType?.each { queue << [def: it, parentName: currentMsgName] } + definition.nestedType?.each { queue << [def: it, parentName: currentMsgName] } + } + } + } + + // 4. Report Results + if (!errors.isEmpty()) { + println "\n🚨 [Protocol Design Violation] The following enums violate the java-tron API evolution standard (Issue #6515):" + errors.each { println " - $it" } + println "\nšŸ’” Recommendation: To ensure proto3 JSON serialization backward compatibility, newly defined enums MUST reserve index 0 for an 'UNKNOWN_XXX' field.\n" + throw new GradleException("Proto Enum validation failed. See above for details.") + } else { + println "āœ… Proto Enum validation passed!" + // Update marker file for Gradle incremental build cache + markerFile.text = "Success: " + new Date().toString() + } + } +} + +check.dependsOn protoLint From 0c78807a43ae5e6b8bfbbcead3f52769822e58d7 Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Fri, 27 Mar 2026 19:16:25 +0800 Subject: [PATCH 2/6] feat(protocol): optimize protoLint performance and caching --- protocol/protoLint.gradle | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/protocol/protoLint.gradle b/protocol/protoLint.gradle index 8095d1aebf4..a3fdd98b321 100644 --- a/protocol/protoLint.gradle +++ b/protocol/protoLint.gradle @@ -34,9 +34,9 @@ task protoLint { group = "verification" description = "Validate Protobuf Enums using buf generated JSON AST. Enforces 'UNKNOWN_' prefix for index 0 to ensure JSON serialization backward compatibility." - // Explicitly declare dependency to avoid Gradle implicit dependency warnings - // because generateProto outputs to the parent 'src' directory. - dependsOn 'generateProto' + // Explicitly depend on extractIncludeProto to ensure external protos + // are available in build/extracted-include-protos/main before buf runs. + dependsOn 'extractIncludeProto' // Incremental build support: Only run if proto files or the script itself changes inputs.dir('src/main/protos') @@ -118,14 +118,14 @@ task protoLint { // A queue-based (BFS) approach to safely traverse all nested messages and enums // without using recursion, ensuring support for any nesting depth. - def queue = [] + Queue queue = new ArrayDeque() // Initial seed: top-level enums and messages - protoFile.enumType?.each { queue << [def: it, parentName: ""] } - protoFile.messageType?.each { queue << [def: it, parentName: ""] } + protoFile.enumType?.each { queue.add([def: it, parentName: ""]) } + protoFile.messageType?.each { queue.add([def: it, parentName: ""]) } while (!queue.isEmpty()) { - def item = queue.remove(0) + def item = queue.poll() def definition = item.def def parentName = item.parentName @@ -152,14 +152,13 @@ task protoLint { // 4. Report Results if (!errors.isEmpty()) { - println "\n🚨 [Protocol Design Violation] The following enums violate the java-tron API evolution standard (Issue #6515):" + println "\nāŒ [Protocol Design Violation] The following enums violate the java-tron API evolution standard (Issue #6515):" errors.each { println " - $it" } - println "\nšŸ’” Recommendation: To ensure proto3 JSON serialization backward compatibility, newly defined enums MUST reserve index 0 for an 'UNKNOWN_XXX' field.\n" throw new GradleException("Proto Enum validation failed. See above for details.") } else { println "āœ… Proto Enum validation passed!" // Update marker file for Gradle incremental build cache - markerFile.text = "Success: " + new Date().toString() + markerFile.text = "Success" } } } From 68351218e617bffd596e625eb9c24b109d5d4e7e Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Mon, 30 Mar 2026 11:52:38 +0800 Subject: [PATCH 3/6] fix(proto): resolve gradle implicit task dependency warning --- protocol/protoLint.gradle | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/protocol/protoLint.gradle b/protocol/protoLint.gradle index a3fdd98b321..494fbb9b96d 100644 --- a/protocol/protoLint.gradle +++ b/protocol/protoLint.gradle @@ -34,9 +34,10 @@ task protoLint { group = "verification" description = "Validate Protobuf Enums using buf generated JSON AST. Enforces 'UNKNOWN_' prefix for index 0 to ensure JSON serialization backward compatibility." - // Explicitly depend on extractIncludeProto to ensure external protos - // are available in build/extracted-include-protos/main before buf runs. - dependsOn 'extractIncludeProto' + // Explicitly depend on: + // 1. extractIncludeProto: ensure external protos are available in build/extracted-include-protos/main. + // 2. generateProto: fix Gradle implicit dependency warning due to output directory overlap. + dependsOn 'extractIncludeProto', 'generateProto' // Incremental build support: Only run if proto files or the script itself changes inputs.dir('src/main/protos') From 4722d293d4fe43d9e1a2c8c7a761ef920ce0f32c Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Wed, 1 Apr 2026 23:48:00 +0800 Subject: [PATCH 4/6] docs(protocol): clarify enum discriminator in protoLint --- protocol/protoLint.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/protocol/protoLint.gradle b/protocol/protoLint.gradle index 494fbb9b96d..c452792ebe8 100644 --- a/protocol/protoLint.gradle +++ b/protocol/protoLint.gradle @@ -130,6 +130,8 @@ task protoLint { def definition = item.def def parentName = item.parentName + // In buf's JSON image, enums expose EnumDescriptorProto.value while + // message descriptors do not, so we use that field as the discriminator here. if (definition.value != null) { // This is an Enum definition def fullName = parentName ? "${parentName}.${definition.name}" : definition.name From 4942eda5838701645b8cc302c4eb7c62e3cf4c3c Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Tue, 7 Apr 2026 23:33:27 +0800 Subject: [PATCH 5/6] build(protocol): harden proto lint buf config --- protocol/protoLint.gradle | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/protocol/protoLint.gradle b/protocol/protoLint.gradle index c452792ebe8..449546b7bae 100644 --- a/protocol/protoLint.gradle +++ b/protocol/protoLint.gradle @@ -11,6 +11,7 @@ * Except for legacy enums in the 'legacyEnums' whitelist, all newly defined Enums MUST reserve index 0 for a field starting with 'UNKNOWN_'. * This ensures robust forward/backward compatibility during proto3 JSON serialization. */ +import groovy.json.JsonBuilder import groovy.json.JsonSlurper import org.gradle.internal.os.OperatingSystem @@ -39,10 +40,18 @@ task protoLint { // 2. generateProto: fix Gradle implicit dependency warning due to output directory overlap. dependsOn 'extractIncludeProto', 'generateProto' - // Incremental build support: Only run if proto files or the script itself changes + // Wire the include proto directory from the extractIncludeProto task's actual output + def extractTask = tasks.named('extractIncludeProto').get() + def includeProtoDir = extractTask.destDir.get().asFile + def includeProtoDirRel = projectDir.toPath().relativize(includeProtoDir.toPath()).toString() + + // Incremental build support: re-run when any file buf physically reads changes. + // Include protos are not lint targets, but buf reads them for import resolution, + // so they must be declared as inputs to keep the task cache hermetic. inputs.dir('src/main/protos') + inputs.dir(includeProtoDir) inputs.file('protoLint.gradle') - + def markerFile = file("${buildDir}/tmp/protoLint.done") outputs.file(markerFile) @@ -86,7 +95,7 @@ task protoLint { println "šŸ” Generating Proto AST image using buf CLI..." - def bufConfig = '{"version":"v1beta1","build":{"roots":["src/main/protos","build/extracted-include-protos/main"]}}' + def bufConfig = new JsonBuilder([version: "v1beta1", build: [roots: ["src/main/protos", includeProtoDirRel]]]).toString() def execResult = exec { commandLine bufExe.absolutePath, 'build', '.', '--config', bufConfig, '-o', "${imageFile.absolutePath}#format=json" From b40c63e919a86c9af0470c9509f5cb2bdc2f0dd3 Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Wed, 8 Apr 2026 19:57:44 +0800 Subject: [PATCH 6/6] docs: update comment to reflect dynamic include path derivation --- protocol/protoLint.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/protocol/protoLint.gradle b/protocol/protoLint.gradle index 449546b7bae..0c76ffa5cfe 100644 --- a/protocol/protoLint.gradle +++ b/protocol/protoLint.gradle @@ -36,7 +36,8 @@ task protoLint { description = "Validate Protobuf Enums using buf generated JSON AST. Enforces 'UNKNOWN_' prefix for index 0 to ensure JSON serialization backward compatibility." // Explicitly depend on: - // 1. extractIncludeProto: ensure external protos are available in build/extracted-include-protos/main. + // 1. extractIncludeProto: ensure external protos are extracted before buf runs. + // The include root is derived from that task's actual output below. // 2. generateProto: fix Gradle implicit dependency warning due to output directory overlap. dependsOn 'extractIncludeProto', 'generateProto'