Skip to content

Commit 2de63bb

Browse files
authored
feat(protocol): add protoLint check for enum validation (#6631)
* feat(protocol): add protoLint script for enum validation * feat(protocol): optimize protoLint performance and caching * fix(proto): resolve gradle implicit task dependency warning * docs(protocol): clarify enum discriminator in protoLint * build(protocol): harden proto lint buf config * docs: update comment to reflect dynamic include path derivation
1 parent 208807d commit 2de63bb

3 files changed

Lines changed: 203 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: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
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.JsonBuilder
15+
import groovy.json.JsonSlurper
16+
import org.gradle.internal.os.OperatingSystem
17+
18+
// Define the required buf CLI version
19+
def bufVersion = "1.61.0"
20+
def currentOs = OperatingSystem.current()
21+
def platform = currentOs.isMacOsX() ? "osx" : (currentOs.isWindows() ? "windows" : "linux")
22+
def machine = rootProject.archInfo.isArm64 ? "aarch_64" : "x86_64"
23+
24+
// Create a custom configuration for the buf CLI tool to keep it isolated from the classpath
25+
configurations {
26+
bufTool
27+
}
28+
29+
// Depend on the buf executable published on Maven Central
30+
dependencies {
31+
bufTool "build.buf:buf:${bufVersion}:${platform}-${machine}@exe"
32+
}
33+
34+
task protoLint {
35+
group = "verification"
36+
description = "Validate Protobuf Enums using buf generated JSON AST. Enforces 'UNKNOWN_' prefix for index 0 to ensure JSON serialization backward compatibility."
37+
38+
// Explicitly depend on:
39+
// 1. extractIncludeProto: ensure external protos are extracted before buf runs.
40+
// The include root is derived from that task's actual output below.
41+
// 2. generateProto: fix Gradle implicit dependency warning due to output directory overlap.
42+
dependsOn 'extractIncludeProto', 'generateProto'
43+
44+
// Wire the include proto directory from the extractIncludeProto task's actual output
45+
def extractTask = tasks.named('extractIncludeProto').get()
46+
def includeProtoDir = extractTask.destDir.get().asFile
47+
def includeProtoDirRel = projectDir.toPath().relativize(includeProtoDir.toPath()).toString()
48+
49+
// Incremental build support: re-run when any file buf physically reads changes.
50+
// Include protos are not lint targets, but buf reads them for import resolution,
51+
// so they must be declared as inputs to keep the task cache hermetic.
52+
inputs.dir('src/main/protos')
53+
inputs.dir(includeProtoDir)
54+
inputs.file('protoLint.gradle')
55+
56+
def markerFile = file("${buildDir}/tmp/protoLint.done")
57+
outputs.file(markerFile)
58+
59+
doLast {
60+
def bufExe = configurations.bufTool.singleFile
61+
if (!bufExe.exists() || !bufExe.canExecute()) {
62+
bufExe.setExecutable(true)
63+
}
64+
65+
// 1. Legacy Whitelist
66+
// Contains enums that existed before the 'UNKNOWN_' standard was enforced.
67+
// Format: "filename.proto:EnumName" or "filename.proto:MessageName.EnumName"
68+
def legacyEnums = [
69+
"core/contract/common.proto:ResourceCode",
70+
"core/contract/smart_contract.proto:SmartContract.ABI.Entry.EntryType",
71+
"core/contract/smart_contract.proto:SmartContract.ABI.Entry.StateMutabilityType",
72+
"core/Tron.proto:AccountType",
73+
"core/Tron.proto:ReasonCode",
74+
"core/Tron.proto:Proposal.State",
75+
"core/Tron.proto:MarketOrder.State",
76+
"core/Tron.proto:Permission.PermissionType",
77+
"core/Tron.proto:Transaction.Contract.ContractType",
78+
"core/Tron.proto:Transaction.Result.code",
79+
"core/Tron.proto:Transaction.Result.contractResult",
80+
"core/Tron.proto:TransactionInfo.code",
81+
"core/Tron.proto:BlockInventory.Type",
82+
"core/Tron.proto:Inventory.InventoryType",
83+
"core/Tron.proto:Items.ItemType",
84+
"core/Tron.proto:PBFTMessage.MsgType",
85+
"core/Tron.proto:PBFTMessage.DataType",
86+
"api/api.proto:Return.response_code",
87+
"api/api.proto:TransactionSignWeight.Result.response_code",
88+
"api/api.proto:TransactionApprovedList.Result.response_code",
89+
"api/zksnark.proto:ZksnarkResponse.Code"
90+
].collect { it.toString() } as Set
91+
92+
// 2. Build JSON AST Image using buf CLI
93+
def imageDir = file("${buildDir}/tmp/buf")
94+
def imageFile = file("${imageDir}/proto-ast.json")
95+
imageDir.mkdirs()
96+
97+
println "🔍 Generating Proto AST image using buf CLI..."
98+
99+
def bufConfig = new JsonBuilder([version: "v1beta1", build: [roots: ["src/main/protos", includeProtoDirRel]]]).toString()
100+
101+
def execResult = exec {
102+
commandLine bufExe.absolutePath, 'build', '.', '--config', bufConfig, '-o', "${imageFile.absolutePath}#format=json"
103+
ignoreExitValue = true
104+
}
105+
106+
if (execResult.exitValue != 0) {
107+
throw new GradleException("Failed to generate AST image. Ensure your .proto files are valid. Buf exited with code ${execResult.exitValue}")
108+
}
109+
110+
if (!imageFile.exists()) {
111+
throw new GradleException("Failed to locate generated buf image at ${imageFile.absolutePath}")
112+
}
113+
114+
// 3. Parse AST and Validate Enums
115+
def descriptorSet
116+
try {
117+
descriptorSet = new JsonSlurper().parse(imageFile)
118+
} catch (Exception e) {
119+
throw new GradleException("Failed to parse buf generated JSON AST: ${e.message}", e)
120+
}
121+
122+
def errors = []
123+
124+
descriptorSet.file?.each { protoFile ->
125+
// Skip Google's and gRPC's internal protos as they are outside our control
126+
if (protoFile.name?.startsWith("google/") || protoFile.name?.startsWith("grpc/")) {
127+
return
128+
}
129+
130+
// A queue-based (BFS) approach to safely traverse all nested messages and enums
131+
// without using recursion, ensuring support for any nesting depth.
132+
Queue queue = new ArrayDeque()
133+
134+
// Initial seed: top-level enums and messages
135+
protoFile.enumType?.each { queue.add([def: it, parentName: ""]) }
136+
protoFile.messageType?.each { queue.add([def: it, parentName: ""]) }
137+
138+
while (!queue.isEmpty()) {
139+
def item = queue.poll()
140+
def definition = item.def
141+
def parentName = item.parentName
142+
143+
// In buf's JSON image, enums expose EnumDescriptorProto.value while
144+
// message descriptors do not, so we use that field as the discriminator here.
145+
if (definition.value != null) {
146+
// This is an Enum definition
147+
def fullName = parentName ? "${parentName}.${definition.name}" : definition.name
148+
def identifier = "${protoFile.name}:${fullName}".toString()
149+
150+
if (!legacyEnums.contains(identifier)) {
151+
def zeroValue = definition.value?.find { it.number == 0 }
152+
if (zeroValue && !zeroValue.name?.startsWith("UNKNOWN_")) {
153+
errors << "[${protoFile.name}] Enum \"${fullName}\" has index 0: \"${zeroValue.name}\". It MUST start with \"UNKNOWN_\"."
154+
}
155+
}
156+
} else {
157+
// This is a Message definition, look for nested enums and nested messages
158+
def currentMsgName = parentName ? "${parentName}.${definition.name}" : definition.name
159+
160+
definition.enumType?.each { queue << [def: it, parentName: currentMsgName] }
161+
definition.nestedType?.each { queue << [def: it, parentName: currentMsgName] }
162+
}
163+
}
164+
}
165+
166+
// 4. Report Results
167+
if (!errors.isEmpty()) {
168+
println "\n❌ [Protocol Design Violation] The following enums violate the java-tron API evolution standard (Issue #6515):"
169+
errors.each { println " - $it" }
170+
throw new GradleException("Proto Enum validation failed. See above for details.")
171+
} else {
172+
println "✅ Proto Enum validation passed!"
173+
// Update marker file for Gradle incremental build cache
174+
markerFile.text = "Success"
175+
}
176+
}
177+
}
178+
179+
check.dependsOn protoLint

0 commit comments

Comments
 (0)