Skip to content

Commit f8c5fb0

Browse files
committed
feat(protocol): add protoLint script for enum validation
1 parent 039821c commit f8c5fb0

3 files changed

Lines changed: 191 additions & 0 deletions

File tree

gradle/verification-metadata.xml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,29 @@
2626
<sha256 value="26e82330157d6b844b67a8064945e206581e772977183e3e31fec6058aa9a59b" origin="Generated by Gradle"/>
2727
</artifact>
2828
</component>
29+
<component group="build.buf" name="buf" version="1.61.0">
30+
<artifact name="buf-1.61.0-linux-aarch_64.exe">
31+
<sha256 value="37bfa47e6ede6ac1b78b3e3c93a0911b7c71d5bb9ed9c684dfc0164167619bc1" origin="Generated by Gradle"/>
32+
</artifact>
33+
<artifact name="buf-1.61.0-linux-x86_64.exe">
34+
<sha256 value="d82849eca790bacb5b8885a027f31bbfbfdf72b2d93bc82bda8cb2ca20e4faca" origin="Generated by Gradle"/>
35+
</artifact>
36+
<artifact name="buf-1.61.0-osx-aarch_64.exe">
37+
<sha256 value="b7878d0c88673e067c3a5ac60fabf133b29be71ae3a37598f2e99ee5d3c5fd22" origin="Generated by Gradle"/>
38+
</artifact>
39+
<artifact name="buf-1.61.0-osx-x86_64.exe">
40+
<sha256 value="20323fa889d55893b67d402b714b04efe8916358ab29ccd2f70203439dc67b42" origin="Generated by Gradle"/>
41+
</artifact>
42+
<artifact name="buf-1.61.0-windows-aarch_64.exe">
43+
<sha256 value="467eec551d3a6f7c4a2a10cbfde8fe4635353035753cc0f8b9eec1a32d2c97e8" origin="Generated by Gradle"/>
44+
</artifact>
45+
<artifact name="buf-1.61.0-windows-x86_64.exe">
46+
<sha256 value="38bdff1ef30a9f97355df1c5377acad90b3b91355c9cafd0bf1c6d65c8172dd6" origin="Generated by Gradle"/>
47+
</artifact>
48+
<artifact name="buf-1.61.0.pom">
49+
<sha256 value="917774d5994717ae1bdb6dc731b9190f19d79465396262e3c44430c91f628e11" origin="Generated by Gradle"/>
50+
</artifact>
51+
</component>
2952
<component group="ch.qos.logback" name="logback-classic" version="1.2.13">
3053
<artifact name="logback-classic-1.2.13.jar">
3154
<sha256 value="937afb220b91d8a394d78befdbf587c71aeed289d582e2a91e72a7d92172371d" origin="Generated by Gradle"/>

protocol/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
apply plugin: 'com.google.protobuf'
2+
apply from: 'protoLint.gradle'
23

34
def protobufVersion = '3.25.8'
45
def grpcVersion = '1.75.0'

protocol/protoLint.gradle

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/**
2+
* This is a Gradle script for proto linting.
3+
*
4+
* Implementation:
5+
* 1. Integrates the 'buf' CLI tool to compile .proto files and generate a JSON AST (Abstract Syntax Tree) image.
6+
* 2. Uses Groovy's JsonSlurper to parse the AST image.
7+
* 3. Traverses all Enum definitions and validates them against preset rules.
8+
*
9+
* Current Validation:
10+
* Enforces the java-tron API evolution standard (see https://github.com/tronprotocol/java-tron/issues/6515).
11+
* Except for legacy enums in the 'legacyEnums' whitelist, all newly defined Enums MUST reserve index 0 for a field starting with 'UNKNOWN_'.
12+
* This ensures robust forward/backward compatibility during proto3 JSON serialization.
13+
*/
14+
import groovy.json.JsonSlurper
15+
import org.gradle.internal.os.OperatingSystem
16+
17+
// Define the required buf CLI version
18+
def bufVersion = "1.61.0"
19+
def currentOs = OperatingSystem.current()
20+
def platform = currentOs.isMacOsX() ? "osx" : (currentOs.isWindows() ? "windows" : "linux")
21+
def machine = rootProject.archInfo.isArm64 ? "aarch_64" : "x86_64"
22+
23+
// Create a custom configuration for the buf CLI tool to keep it isolated from the classpath
24+
configurations {
25+
bufTool
26+
}
27+
28+
// Depend on the buf executable published on Maven Central
29+
dependencies {
30+
bufTool "build.buf:buf:${bufVersion}:${platform}-${machine}@exe"
31+
}
32+
33+
task protoLint {
34+
group = "verification"
35+
description = "Validate Protobuf Enums using buf generated JSON AST. Enforces 'UNKNOWN_' prefix for index 0 to ensure JSON serialization backward compatibility."
36+
37+
// Explicitly declare dependency to avoid Gradle implicit dependency warnings
38+
// because generateProto outputs to the parent 'src' directory.
39+
dependsOn 'generateProto'
40+
41+
// Incremental build support: Only run if proto files or the script itself changes
42+
inputs.dir('src/main/protos')
43+
inputs.file('protoLint.gradle')
44+
45+
def markerFile = file("${buildDir}/tmp/protoLint.done")
46+
outputs.file(markerFile)
47+
48+
doLast {
49+
def bufExe = configurations.bufTool.singleFile
50+
if (!bufExe.exists() || !bufExe.canExecute()) {
51+
bufExe.setExecutable(true)
52+
}
53+
54+
// 1. Legacy Whitelist
55+
// Contains enums that existed before the 'UNKNOWN_' standard was enforced.
56+
// Format: "filename.proto:EnumName" or "filename.proto:MessageName.EnumName"
57+
def legacyEnums = [
58+
"core/contract/common.proto:ResourceCode",
59+
"core/contract/smart_contract.proto:SmartContract.ABI.Entry.EntryType",
60+
"core/contract/smart_contract.proto:SmartContract.ABI.Entry.StateMutabilityType",
61+
"core/Tron.proto:AccountType",
62+
"core/Tron.proto:ReasonCode",
63+
"core/Tron.proto:Proposal.State",
64+
"core/Tron.proto:MarketOrder.State",
65+
"core/Tron.proto:Permission.PermissionType",
66+
"core/Tron.proto:Transaction.Contract.ContractType",
67+
"core/Tron.proto:Transaction.Result.code",
68+
"core/Tron.proto:Transaction.Result.contractResult",
69+
"core/Tron.proto:TransactionInfo.code",
70+
"core/Tron.proto:BlockInventory.Type",
71+
"core/Tron.proto:Inventory.InventoryType",
72+
"core/Tron.proto:Items.ItemType",
73+
"core/Tron.proto:PBFTMessage.MsgType",
74+
"core/Tron.proto:PBFTMessage.DataType",
75+
"api/api.proto:Return.response_code",
76+
"api/api.proto:TransactionSignWeight.Result.response_code",
77+
"api/api.proto:TransactionApprovedList.Result.response_code",
78+
"api/zksnark.proto:ZksnarkResponse.Code"
79+
].collect { it.toString() } as Set
80+
81+
// 2. Build JSON AST Image using buf CLI
82+
def imageDir = file("${buildDir}/tmp/buf")
83+
def imageFile = file("${imageDir}/proto-ast.json")
84+
imageDir.mkdirs()
85+
86+
println "🔍 Generating Proto AST image using buf CLI..."
87+
88+
def bufConfig = '{"version":"v1beta1","build":{"roots":["src/main/protos","build/extracted-include-protos/main"]}}'
89+
90+
def execResult = exec {
91+
commandLine bufExe.absolutePath, 'build', '.', '--config', bufConfig, '-o', "${imageFile.absolutePath}#format=json"
92+
ignoreExitValue = true
93+
}
94+
95+
if (execResult.exitValue != 0) {
96+
throw new GradleException("Failed to generate AST image. Ensure your .proto files are valid. Buf exited with code ${execResult.exitValue}")
97+
}
98+
99+
if (!imageFile.exists()) {
100+
throw new GradleException("Failed to locate generated buf image at ${imageFile.absolutePath}")
101+
}
102+
103+
// 3. Parse AST and Validate Enums
104+
def descriptorSet
105+
try {
106+
descriptorSet = new JsonSlurper().parse(imageFile)
107+
} catch (Exception e) {
108+
throw new GradleException("Failed to parse buf generated JSON AST: ${e.message}", e)
109+
}
110+
111+
def errors = []
112+
113+
descriptorSet.file?.each { protoFile ->
114+
// Skip Google's and gRPC's internal protos as they are outside our control
115+
if (protoFile.name?.startsWith("google/") || protoFile.name?.startsWith("grpc/")) {
116+
return
117+
}
118+
119+
// A queue-based (BFS) approach to safely traverse all nested messages and enums
120+
// without using recursion, ensuring support for any nesting depth.
121+
def queue = []
122+
123+
// Initial seed: top-level enums and messages
124+
protoFile.enumType?.each { queue << [def: it, parentName: ""] }
125+
protoFile.messageType?.each { queue << [def: it, parentName: ""] }
126+
127+
while (!queue.isEmpty()) {
128+
def item = queue.remove(0)
129+
def definition = item.def
130+
def parentName = item.parentName
131+
132+
if (definition.value != null) {
133+
// This is an Enum definition
134+
def fullName = parentName ? "${parentName}.${definition.name}" : definition.name
135+
def identifier = "${protoFile.name}:${fullName}".toString()
136+
137+
if (!legacyEnums.contains(identifier)) {
138+
def zeroValue = definition.value?.find { it.number == 0 }
139+
if (zeroValue && !zeroValue.name?.startsWith("UNKNOWN_")) {
140+
errors << "[${protoFile.name}] Enum \"${fullName}\" has index 0: \"${zeroValue.name}\". It MUST start with \"UNKNOWN_\"."
141+
}
142+
}
143+
} else {
144+
// This is a Message definition, look for nested enums and nested messages
145+
def currentMsgName = parentName ? "${parentName}.${definition.name}" : definition.name
146+
147+
definition.enumType?.each { queue << [def: it, parentName: currentMsgName] }
148+
definition.nestedType?.each { queue << [def: it, parentName: currentMsgName] }
149+
}
150+
}
151+
}
152+
153+
// 4. Report Results
154+
if (!errors.isEmpty()) {
155+
println "\n🚨 [Protocol Design Violation] The following enums violate the java-tron API evolution standard (Issue #6515):"
156+
errors.each { println " - $it" }
157+
println "\n💡 Recommendation: To ensure proto3 JSON serialization backward compatibility, newly defined enums MUST reserve index 0 for an 'UNKNOWN_XXX' field.\n"
158+
throw new GradleException("Proto Enum validation failed. See above for details.")
159+
} else {
160+
println "✅ Proto Enum validation passed!"
161+
// Update marker file for Gradle incremental build cache
162+
markerFile.text = "Success: " + new Date().toString()
163+
}
164+
}
165+
}
166+
167+
check.dependsOn protoLint

0 commit comments

Comments
 (0)