Skip to content

Commit ae7dcc3

Browse files
committed
Merge remote-tracking branch 'origin/feature/hip-1299-changing-node-account-id' into feature/hip-1299-changing-node-account-id
2 parents c78f359 + ba6351c commit ae7dcc3

File tree

19 files changed

+1024
-35
lines changed

19 files changed

+1024
-35
lines changed

hiero-dependency-versions/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ plugins {
99
group = "org.hiero"
1010

1111
val bouncycastle = "1.80"
12-
val grpc = "1.74.0"
12+
val grpc = "1.75.0"
1313
val protobuf = "4.31.1"
1414
val slf4j = "2.0.17"
1515
val mockito = "5.19.0"

sdk/src/main/java/com/hedera/hashgraph/sdk/Endpoint.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,24 @@ public Endpoint setDomainName(String domainName) {
9898
return this;
9999
}
100100

101+
/**
102+
* Validate that the endpoint does not contain both an IP address and a domain name.
103+
*
104+
* @param endpoint the endpoint to validate
105+
* @throws IllegalArgumentException if both ipAddressV4 and domainName are present
106+
*/
107+
public static void validateNoIpAndDomain(Endpoint endpoint) {
108+
if (endpoint == null) {
109+
return;
110+
}
111+
if (endpoint.getAddress() != null) {
112+
var dn = endpoint.getDomainName();
113+
if (dn != null && !dn.isEmpty()) {
114+
throw new IllegalArgumentException("Endpoint must not contain both ipAddressV4 and domainName");
115+
}
116+
}
117+
}
118+
101119
/**
102120
* Create the protobuf.
103121
*

sdk/src/main/java/com/hedera/hashgraph/sdk/NodeCreateTransaction.java

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import com.hedera.hashgraph.sdk.proto.TransactionBody;
1010
import com.hedera.hashgraph.sdk.proto.TransactionResponse;
1111
import io.grpc.MethodDescriptor;
12+
import java.nio.charset.StandardCharsets;
1213
import java.util.ArrayList;
1314
import java.util.LinkedHashMap;
1415
import java.util.List;
@@ -136,8 +137,16 @@ public String getDescription() {
136137
* @param description The String to be set as the description of the node.
137138
* @return {@code this}
138139
*/
139-
public NodeCreateTransaction setDescription(String description) {
140+
public NodeCreateTransaction setDescription(@Nullable String description) {
140141
requireNotFrozen();
142+
if (description == null) {
143+
this.description = "";
144+
return this;
145+
}
146+
int byteLen = description.getBytes(StandardCharsets.UTF_8).length;
147+
if (byteLen > 100) {
148+
throw new IllegalArgumentException("description must not exceed 100 bytes when UTF-8 encoded");
149+
}
141150
this.description = description;
142151
return this;
143152
}
@@ -177,6 +186,12 @@ public List<Endpoint> getGossipEndpoints() {
177186
public NodeCreateTransaction setGossipEndpoints(List<Endpoint> gossipEndpoints) {
178187
requireNotFrozen();
179188
Objects.requireNonNull(gossipEndpoints);
189+
if (gossipEndpoints.size() > 10) {
190+
throw new IllegalArgumentException("gossipEndpoints must not contain more than 10 entries");
191+
}
192+
for (Endpoint endpoint : gossipEndpoints) {
193+
Endpoint.validateNoIpAndDomain(endpoint);
194+
}
180195
this.gossipEndpoints = new ArrayList<>(gossipEndpoints);
181196
return this;
182197
}
@@ -188,6 +203,10 @@ public NodeCreateTransaction setGossipEndpoints(List<Endpoint> gossipEndpoints)
188203
*/
189204
public NodeCreateTransaction addGossipEndpoint(Endpoint gossipEndpoint) {
190205
requireNotFrozen();
206+
if (gossipEndpoints.size() >= 10) {
207+
throw new IllegalArgumentException("gossipEndpoints must not contain more than 10 entries");
208+
}
209+
Endpoint.validateNoIpAndDomain(gossipEndpoint);
191210
gossipEndpoints.add(gossipEndpoint);
192211
return this;
193212
}
@@ -217,6 +236,12 @@ public List<Endpoint> getServiceEndpoints() {
217236
public NodeCreateTransaction setServiceEndpoints(List<Endpoint> serviceEndpoints) {
218237
requireNotFrozen();
219238
Objects.requireNonNull(serviceEndpoints);
239+
if (serviceEndpoints.size() > 8) {
240+
throw new IllegalArgumentException("serviceEndpoints must not contain more than 8 entries");
241+
}
242+
for (Endpoint endpoint : serviceEndpoints) {
243+
Endpoint.validateNoIpAndDomain(endpoint);
244+
}
220245
this.serviceEndpoints = new ArrayList<>(serviceEndpoints);
221246
return this;
222247
}
@@ -228,6 +253,10 @@ public NodeCreateTransaction setServiceEndpoints(List<Endpoint> serviceEndpoints
228253
*/
229254
public NodeCreateTransaction addServiceEndpoint(Endpoint serviceEndpoint) {
230255
requireNotFrozen();
256+
if (serviceEndpoints.size() >= 8) {
257+
throw new IllegalArgumentException("serviceEndpoints must not contain more than 8 entries");
258+
}
259+
Endpoint.validateNoIpAndDomain(serviceEndpoint);
231260
serviceEndpoints.add(serviceEndpoint);
232261
return this;
233262
}
@@ -252,8 +281,13 @@ public byte[] getGossipCaCertificate() {
252281
* @param gossipCaCertificate the DER encoding of the certificate presented.
253282
* @return {@code this}
254283
*/
255-
public NodeCreateTransaction setGossipCaCertificate(byte[] gossipCaCertificate) {
284+
public NodeCreateTransaction setGossipCaCertificate(@Nullable byte[] gossipCaCertificate) {
256285
requireNotFrozen();
286+
if (gossipCaCertificate != null) {
287+
if (gossipCaCertificate.length == 0) {
288+
throw new IllegalArgumentException("gossipCaCertificate must not be empty");
289+
}
290+
}
257291
this.gossipCaCertificate = gossipCaCertificate;
258292
return this;
259293
}
@@ -376,8 +410,28 @@ NodeCreateTransactionBody.Builder build() {
376410

377411
builder.setDescription(description);
378412

413+
// If gossip endpoints include FQDN but the network forbids it, prefer using an available IP
414+
// from service endpoints. We rewrite such gossip endpoints to use the first available service IP.
415+
byte[] fallbackServiceIp = null;
416+
for (Endpoint serviceEndpoint : serviceEndpoints) {
417+
if (serviceEndpoint.getAddress() != null) {
418+
fallbackServiceIp = serviceEndpoint.getAddress().clone();
419+
break;
420+
}
421+
}
422+
379423
for (Endpoint gossipEndpoint : gossipEndpoints) {
380-
builder.addGossipEndpoint(gossipEndpoint.toProtobuf());
424+
boolean hasFqdn = gossipEndpoint.getDomainName() != null
425+
&& !gossipEndpoint.getDomainName().isEmpty();
426+
boolean hasIp = gossipEndpoint.getAddress() != null;
427+
if (!hasIp && hasFqdn && fallbackServiceIp != null) {
428+
// rewrite to IP-only endpoint preserving the port
429+
Endpoint rewritten =
430+
new Endpoint().setAddress(fallbackServiceIp.clone()).setPort(gossipEndpoint.getPort());
431+
builder.addGossipEndpoint(rewritten.toProtobuf());
432+
} else {
433+
builder.addGossipEndpoint(gossipEndpoint.toProtobuf());
434+
}
381435
}
382436

383437
for (Endpoint serviceEndpoint : serviceEndpoints) {

sdk/src/main/java/com/hedera/hashgraph/sdk/NodeDeleteTransaction.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,13 @@ public long getNodeId() {
8282
*
8383
* @param nodeId the consensus node identifier in the network state.
8484
* @return {@code this}
85+
* @throws IllegalArgumentException if nodeId is negative
8586
*/
8687
public NodeDeleteTransaction setNodeId(long nodeId) {
8788
requireNotFrozen();
89+
if (nodeId < 0) {
90+
throw new IllegalArgumentException("NodeDeleteTransaction: 'nodeId' must be non-negative");
91+
}
8892
this.nodeId = nodeId;
8993
return this;
9094
}

sdk/src/main/java/com/hedera/hashgraph/sdk/NodeUpdateTransaction.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,13 @@ public long getNodeId() {
112112
*
113113
* @param nodeId the consensus node identifier in the network state.
114114
* @return {@code this}
115+
* @throws IllegalArgumentException if nodeId is negative
115116
*/
116117
public NodeUpdateTransaction setNodeId(long nodeId) {
117118
requireNotFrozen();
119+
if (nodeId < 0) {
120+
throw new IllegalArgumentException("nodeId must be non-negative");
121+
}
118122
this.nodeId = nodeId;
119123
return this;
120124
}
@@ -160,9 +164,13 @@ public String getDescription() {
160164
*
161165
* @param description The String to be set as the description of the node.
162166
* @return {@code this}
167+
* @throws IllegalArgumentException if description exceeds 100 bytes when encoded as UTF-8
163168
*/
164169
public NodeUpdateTransaction setDescription(String description) {
165170
requireNotFrozen();
171+
if (description != null && description.getBytes(java.nio.charset.StandardCharsets.UTF_8).length > 100) {
172+
throw new IllegalArgumentException("Description must not exceed 100 bytes when encoded as UTF-8");
173+
}
166174
this.description = description;
167175
return this;
168176
}
@@ -217,10 +225,20 @@ public List<Endpoint> getGossipEndpoints() {
217225
*
218226
* @param gossipEndpoints the list of service endpoints for gossip.
219227
* @return {@code this}
228+
* @throws IllegalArgumentException if the list is empty or contains more than 10 endpoints
220229
*/
221230
public NodeUpdateTransaction setGossipEndpoints(List<Endpoint> gossipEndpoints) {
222231
requireNotFrozen();
223232
Objects.requireNonNull(gossipEndpoints);
233+
if (gossipEndpoints.isEmpty()) {
234+
throw new IllegalArgumentException("Gossip endpoints list must not be empty");
235+
}
236+
if (gossipEndpoints.size() > 10) {
237+
throw new IllegalArgumentException("Gossip endpoints list must not contain more than 10 entries");
238+
}
239+
for (Endpoint endpoint : gossipEndpoints) {
240+
Endpoint.validateNoIpAndDomain(endpoint);
241+
}
224242
this.gossipEndpoints = new ArrayList<>(gossipEndpoints);
225243
return this;
226244
}
@@ -265,10 +283,20 @@ public List<Endpoint> getServiceEndpoints() {
265283
*
266284
* @param serviceEndpoints list of service endpoints for gRPC calls.
267285
* @return {@code this}
286+
* @throws IllegalArgumentException if the list is empty or contains more than 8 endpoints
268287
*/
269288
public NodeUpdateTransaction setServiceEndpoints(List<Endpoint> serviceEndpoints) {
270289
requireNotFrozen();
271290
Objects.requireNonNull(serviceEndpoints);
291+
if (serviceEndpoints.isEmpty()) {
292+
throw new IllegalArgumentException("Service endpoints list must not be empty");
293+
}
294+
if (serviceEndpoints.size() > 8) {
295+
throw new IllegalArgumentException("Service endpoints list must not contain more than 8 entries");
296+
}
297+
for (Endpoint endpoint : serviceEndpoints) {
298+
Endpoint.validateNoIpAndDomain(endpoint);
299+
}
272300
this.serviceEndpoints = new ArrayList<>(serviceEndpoints);
273301
return this;
274302
}
@@ -304,9 +332,13 @@ public byte[] getGossipCaCertificate() {
304332
*
305333
* @param gossipCaCertificate the DER encoding of the certificate presented.
306334
* @return {@code this}
335+
* @throws IllegalArgumentException if gossipCaCertificate is null or empty
307336
*/
308337
public NodeUpdateTransaction setGossipCaCertificate(byte[] gossipCaCertificate) {
309338
requireNotFrozen();
339+
if (gossipCaCertificate == null || gossipCaCertificate.length == 0) {
340+
throw new IllegalArgumentException("Gossip CA certificate must not be null or empty");
341+
}
310342
this.gossipCaCertificate = gossipCaCertificate;
311343
return this;
312344
}
@@ -334,9 +366,13 @@ public byte[] getGrpcCertificateHash() {
334366
*
335367
* @param grpcCertificateHash SHA-384 hash of the node gRPC TLS certificate.
336368
* @return {@code this}
369+
* @throws IllegalArgumentException if grpcCertificateHash is not 48 bytes (SHA-384 size) when non-empty
337370
*/
338371
public NodeUpdateTransaction setGrpcCertificateHash(byte[] grpcCertificateHash) {
339372
requireNotFrozen();
373+
if (grpcCertificateHash != null && grpcCertificateHash.length > 0 && grpcCertificateHash.length != 48) {
374+
throw new IllegalArgumentException("gRPC certificate hash must be exactly 48 bytes (SHA-384)");
375+
}
340376
this.grpcCertificateHash = grpcCertificateHash;
341377
return this;
342378
}
@@ -562,4 +598,12 @@ public NodeUpdateTransaction deleteGrpcWebProxyEndpoint() {
562598
this.grpcWebProxyEndpoint = new Endpoint();
563599
return this;
564600
}
601+
602+
/**
603+
* Validates that an endpoint does not contain both IP address and domain name.
604+
*
605+
* @param endpoint the endpoint to validate
606+
* @throws IllegalArgumentException if endpoint contains both IP address and domain name
607+
*/
608+
// validation moved to Endpoint.validateNoIpAndDomain
565609
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
package com.hedera.hashgraph.sdk;
3+
4+
import static org.assertj.core.api.Assertions.assertThatCode;
5+
import static org.junit.jupiter.api.Assertions.assertThrows;
6+
7+
import org.junit.jupiter.api.DisplayName;
8+
import org.junit.jupiter.api.Test;
9+
10+
public class EndpointTest {
11+
12+
@Test
13+
@DisplayName("validateNoIpAndDomain allows only IP")
14+
void validateAllowsOnlyIp() {
15+
var ep = new Endpoint().setAddress(new byte[] {127, 0, 0, 1}).setPort(50211);
16+
assertThatCode(() -> Endpoint.validateNoIpAndDomain(ep)).doesNotThrowAnyException();
17+
}
18+
19+
@Test
20+
@DisplayName("validateNoIpAndDomain allows only domain")
21+
void validateAllowsOnlyDomain() {
22+
var ep = new Endpoint().setDomainName("node1.test.local").setPort(50211);
23+
assertThatCode(() -> Endpoint.validateNoIpAndDomain(ep)).doesNotThrowAnyException();
24+
}
25+
26+
@Test
27+
@DisplayName("validateNoIpAndDomain throws when both IP and domain are set")
28+
void validateThrowsOnIpAndDomain() {
29+
var ep = new Endpoint()
30+
.setAddress(new byte[] {127, 0, 0, 1})
31+
.setDomainName("node1.test.local")
32+
.setPort(50211);
33+
assertThrows(IllegalArgumentException.class, () -> Endpoint.validateNoIpAndDomain(ep));
34+
}
35+
36+
@Test
37+
@DisplayName("validateNoIpAndDomain is no-op for null endpoint")
38+
void validateNoOpOnNull() {
39+
assertThatCode(() -> Endpoint.validateNoIpAndDomain(null)).doesNotThrowAnyException();
40+
}
41+
42+
@Test
43+
@DisplayName("validateNoIpAndDomain allows empty domain with IP")
44+
void validateAllowsEmptyDomainWithIp() {
45+
var ep = new Endpoint()
46+
.setAddress(new byte[] {10, 0, 0, 1})
47+
.setDomainName("")
48+
.setPort(50211);
49+
assertThatCode(() -> Endpoint.validateNoIpAndDomain(ep)).doesNotThrowAnyException();
50+
}
51+
}

0 commit comments

Comments
 (0)