Skip to content

Commit 6d26f56

Browse files
authored
feat: 13472 implement node delete handler (#13708)
Signed-off-by: Lev Povolotsky <[email protected]>
1 parent 8f8f67a commit 6d26f56

File tree

3 files changed

+266
-19
lines changed

3 files changed

+266
-19
lines changed

hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/handlers/NodeDeleteHandler.java

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,18 @@
1616

1717
package com.hedera.node.app.service.addressbook.impl.handlers;
1818

19+
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_NODE_ID;
20+
import static com.hedera.hapi.node.base.ResponseCodeEnum.NODE_DELETED;
21+
import static com.hedera.node.app.spi.workflows.HandleException.validateFalse;
22+
import static com.hedera.node.app.spi.workflows.PreCheckException.validateFalsePreCheck;
1923
import static java.util.Objects.requireNonNull;
2024

25+
import com.hedera.hapi.node.addressbook.NodeDeleteTransactionBody;
2126
import com.hedera.hapi.node.base.HederaFunctionality;
27+
import com.hedera.hapi.node.base.SubType;
28+
import com.hedera.hapi.node.state.addressbook.Node;
2229
import com.hedera.hapi.node.transaction.TransactionBody;
30+
import com.hedera.node.app.service.addressbook.impl.WritableNodeStore;
2331
import com.hedera.node.app.spi.fees.FeeContext;
2432
import com.hedera.node.app.spi.fees.Fees;
2533
import com.hedera.node.app.spi.workflows.HandleContext;
@@ -42,37 +50,47 @@ public NodeDeleteHandler() {
4250

4351
@Override
4452
public void pureChecks(@NonNull final TransactionBody txn) throws PreCheckException {
45-
// implement
53+
final NodeDeleteTransactionBody transactionBody = txn.nodeDeleteOrThrow();
54+
final long nodeId = transactionBody.nodeId();
55+
56+
validateFalsePreCheck(nodeId < 0, INVALID_NODE_ID);
4657
}
4758

4859
@Override
49-
public void preHandle(@NonNull final PreHandleContext context) throws PreCheckException {
50-
requireNonNull(context);
51-
52-
final var op = context.body().nodeDelete();
53-
throw new UnsupportedOperationException("need implementation");
54-
}
60+
public void preHandle(@NonNull final PreHandleContext context) throws PreCheckException {}
5561

5662
/**
57-
* Given the appropriate context, deletes a topic.
63+
* Given the appropriate context, deletes a node.
5864
*
5965
* @param context the {@link HandleContext} of the active transaction
6066
* @throws NullPointerException if one of the arguments is {@code null}
6167
*/
6268
@Override
6369
public void handle(@NonNull final HandleContext context) {
64-
requireNonNull(context, "The argument 'context' must not be null");
70+
requireNonNull(context);
71+
72+
final NodeDeleteTransactionBody transactionBody = context.body().nodeDeleteOrThrow();
73+
var nodeId = transactionBody.nodeId();
6574

66-
final var op = context.body().nodeDelete();
67-
throw new UnsupportedOperationException("need implementation");
75+
final var nodeStore = context.writableStore(WritableNodeStore.class);
76+
77+
Node node = nodeStore.get(nodeId);
78+
79+
validateFalse(node == null, INVALID_NODE_ID);
80+
81+
validateFalse(node.deleted(), NODE_DELETED);
82+
83+
/* Copy all the fields from existing, and mark deleted flag */
84+
final var nodeBuilder = node.copyBuilder().deleted(true);
85+
86+
/* --- Put the modified node. It will be in underlying state's modifications map.
87+
It will not be committed to state until commit is called on the state.--- */
88+
nodeStore.put(nodeBuilder.build());
6889
}
6990

7091
@NonNull
7192
@Override
7293
public Fees calculateFees(@NonNull final FeeContext feeContext) {
73-
requireNonNull(feeContext);
74-
final var op = feeContext.body();
75-
76-
throw new UnsupportedOperationException("need implementation");
94+
return feeContext.feeCalculator(SubType.DEFAULT).calculate();
7795
}
7896
}

hedera-node/hedera-addressbook-service-impl/src/test/java/com/hedera/node/app/service/addressbook/impl/test/handlers/AddressBookTestBase.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@ public class AddressBookTestBase {
5151
protected final AccountID accountId = AccountID.newBuilder().accountNum(3).build();
5252

5353
protected final AccountID payerId = AccountID.newBuilder().accountNum(2).build();
54-
protected final AccountID invalidId =
55-
AccountID.newBuilder().accountNum(Long.MAX_VALUE).build();
5654
protected final byte[] grpcCertificateHash = "grpcCertificateHash".getBytes();
5755
protected final byte[] gossipCaCertificate = "gossipCaCertificate".getBytes();
58-
protected final EntityNumber nodeId = EntityNumber.newBuilder().number(1L).build();
56+
protected final long WELL_KNOWN_NODE_ID = 1L;
57+
protected final EntityNumber nodeId =
58+
EntityNumber.newBuilder().number(WELL_KNOWN_NODE_ID).build();
5959
protected final EntityNumber nodeId2 = EntityNumber.newBuilder().number(3L).build();
6060
protected final Timestamp consensusTimestamp =
6161
Timestamp.newBuilder().seconds(1_234_567L).build();
@@ -113,6 +113,16 @@ protected void refreshStoresWithCurrentNodeInReadable() {
113113
writableStore = new WritableNodeStore(writableStates, configuration, storeMetricsService);
114114
}
115115

116+
protected void refreshStoresWithCurrentNodeInBothReadableAndWritable() {
117+
readableNodeState = readableNodeState();
118+
writableNodeState = writableNodeStateWithOneKey();
119+
given(readableStates.<EntityNumber, Node>get(NODES_KEY)).willReturn(readableNodeState);
120+
given(writableStates.<EntityNumber, Node>get(NODES_KEY)).willReturn(writableNodeState);
121+
readableStore = new ReadableNodeStoreImpl(readableStates);
122+
final var configuration = HederaTestConfigBuilder.createConfig();
123+
writableStore = new WritableNodeStore(writableStates, configuration, storeMetricsService);
124+
}
125+
116126
protected void refreshStoresWithCurrentNodeInWritable() {
117127
writableNodeState = writableNodeStateWithOneKey();
118128
given(writableStates.<EntityNumber, Node>get(NODES_KEY)).willReturn(writableNodeState);
@@ -160,6 +170,10 @@ protected MapReadableKVState<EntityNumber, Node> emptyReadableNodeState() {
160170
}
161171

162172
protected void givenValidNode() {
173+
givenValidNode(false);
174+
}
175+
176+
protected void givenValidNode(boolean deleted) {
163177
node = new Node(
164178
nodeId.number(),
165179
accountId,
@@ -169,7 +183,7 @@ protected void givenValidNode() {
169183
Bytes.wrap(gossipCaCertificate),
170184
Bytes.wrap(grpcCertificateHash),
171185
0,
172-
false);
186+
deleted);
173187
}
174188

175189
protected Node createNode() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/*
2+
* Copyright (C) 2023-2024 Hedera Hashgraph, LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.hedera.node.app.service.addressbook.impl.test.handlers;
18+
19+
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_NODE_ID;
20+
import static com.hedera.node.app.service.addressbook.impl.AddressBookServiceImpl.NODES_KEY;
21+
import static org.assertj.core.api.Assertions.assertThat;
22+
import static org.assertj.core.api.Assertions.assertThatCode;
23+
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
24+
import static org.assertj.core.api.AssertionsForClassTypes.catchThrowable;
25+
import static org.junit.jupiter.api.Assertions.assertThrows;
26+
import static org.mockito.ArgumentMatchers.notNull;
27+
import static org.mockito.BDDMockito.given;
28+
import static org.mockito.Mockito.lenient;
29+
import static org.mockito.Mockito.mock;
30+
31+
import com.hedera.hapi.node.addressbook.NodeDeleteTransactionBody;
32+
import com.hedera.hapi.node.base.ResponseCodeEnum;
33+
import com.hedera.hapi.node.base.TransactionID;
34+
import com.hedera.hapi.node.state.addressbook.Node;
35+
import com.hedera.hapi.node.state.common.EntityNumber;
36+
import com.hedera.hapi.node.transaction.TransactionBody;
37+
import com.hedera.node.app.service.addressbook.impl.ReadableNodeStoreImpl;
38+
import com.hedera.node.app.service.addressbook.impl.WritableNodeStore;
39+
import com.hedera.node.app.service.addressbook.impl.handlers.NodeDeleteHandler;
40+
import com.hedera.node.app.service.token.ReadableAccountStore;
41+
import com.hedera.node.app.spi.fees.FeeCalculator;
42+
import com.hedera.node.app.spi.fees.FeeContext;
43+
import com.hedera.node.app.spi.fees.Fees;
44+
import com.hedera.node.app.spi.metrics.StoreMetricsService;
45+
import com.hedera.node.app.spi.workflows.HandleContext;
46+
import com.hedera.node.app.spi.workflows.HandleException;
47+
import com.hedera.node.app.spi.workflows.PreCheckException;
48+
import com.hedera.node.config.testfixtures.HederaTestConfigBuilder;
49+
import com.swirlds.config.api.Configuration;
50+
import org.junit.jupiter.api.BeforeEach;
51+
import org.junit.jupiter.api.DisplayName;
52+
import org.junit.jupiter.api.Test;
53+
import org.junit.jupiter.api.extension.ExtendWith;
54+
import org.mockito.Mock;
55+
import org.mockito.Mock.Strictness;
56+
import org.mockito.junit.jupiter.MockitoExtension;
57+
58+
@ExtendWith(MockitoExtension.class)
59+
class NodeDeleteHandlerTest extends AddressBookTestBase {
60+
61+
@Mock
62+
private ReadableAccountStore accountStore;
63+
64+
@Mock
65+
private ReadableNodeStoreImpl mockStore;
66+
67+
@Mock(strictness = Strictness.LENIENT)
68+
private HandleContext handleContext;
69+
70+
@Mock
71+
private NodeDeleteHandler subject;
72+
73+
@Mock
74+
private StoreMetricsService storeMetricsService;
75+
76+
protected Configuration testConfig;
77+
78+
@BeforeEach
79+
void setUp() {
80+
mockStore = mock(ReadableNodeStoreImpl.class);
81+
subject = new NodeDeleteHandler();
82+
83+
writableNodeState = writableNodeStateWithOneKey();
84+
given(writableStates.<EntityNumber, Node>get(NODES_KEY)).willReturn(writableNodeState);
85+
testConfig = HederaTestConfigBuilder.createConfig();
86+
writableStore = new WritableNodeStore(writableStates, testConfig, storeMetricsService);
87+
lenient().when(handleContext.configuration()).thenReturn(testConfig);
88+
}
89+
90+
@Test
91+
@DisplayName("pureChecks throws exception when node id is negative or zero")
92+
public void testPureChecksThrowsExceptionWhenFileIdIsNull() {
93+
NodeDeleteTransactionBody transactionBody = mock(NodeDeleteTransactionBody.class);
94+
TransactionBody transaction = mock(TransactionBody.class);
95+
given(handleContext.body()).willReturn(transaction);
96+
given(transaction.nodeDeleteOrThrow()).willReturn(transactionBody);
97+
given(transactionBody.nodeId()).willReturn(-1L);
98+
99+
assertThatThrownBy(() -> subject.pureChecks(handleContext.body())).isInstanceOf(PreCheckException.class);
100+
var msg = assertThrows(PreCheckException.class, () -> subject.pureChecks(handleContext.body()));
101+
assertThat(msg.responseCode()).isEqualTo(INVALID_NODE_ID);
102+
}
103+
104+
@Test
105+
@DisplayName("pureChecks does not throw exception when node id is not null")
106+
public void testPureChecksDoesNotThrowExceptionWhenNodeIdIsNotNull() {
107+
given(handleContext.body()).willReturn(newDeleteTxn());
108+
109+
assertThatCode(() -> subject.pureChecks(handleContext.body())).doesNotThrowAnyException();
110+
}
111+
112+
@Test
113+
@DisplayName("check that fees are free for delete node trx")
114+
public void testCalculateFeesInvocations() {
115+
final var feeCtx = mock(FeeContext.class);
116+
final var feeCalc = mock(FeeCalculator.class);
117+
given(feeCtx.feeCalculator(notNull())).willReturn(feeCalc);
118+
given(feeCalc.calculate()).willReturn(Fees.FREE);
119+
120+
assertThat(subject.calculateFees(feeCtx)).isEqualTo(Fees.FREE);
121+
}
122+
123+
@Test
124+
@DisplayName("Fails handle if node doesn't exist")
125+
void fileDoesntExist() {
126+
final var txn = newDeleteTxn().nodeDeleteOrThrow();
127+
128+
writableNodeState = emptyWritableNodeState();
129+
given(writableStates.<EntityNumber, Node>get(NODES_KEY)).willReturn(writableNodeState);
130+
writableStore = new WritableNodeStore(writableStates, testConfig, storeMetricsService);
131+
given(handleContext.writableStore(WritableNodeStore.class)).willReturn(writableStore);
132+
133+
given(handleContext.body())
134+
.willReturn(TransactionBody.newBuilder().nodeDelete(txn).build());
135+
given(handleContext.writableStore(WritableNodeStore.class)).willReturn(writableStore);
136+
137+
HandleException thrown = (HandleException) catchThrowable(() -> subject.handle(handleContext));
138+
assertThat(thrown.getStatus()).isEqualTo(INVALID_NODE_ID);
139+
}
140+
141+
@Test
142+
@DisplayName("Node is null")
143+
void NodeIsNull() {
144+
final var txn = newDeleteTxn().nodeDeleteOrThrow();
145+
146+
node = null;
147+
148+
writableNodeState = writableNodeStateWithOneKey();
149+
given(writableStates.<EntityNumber, Node>get(NODES_KEY)).willReturn(writableNodeState);
150+
writableStore = new WritableNodeStore(writableStates, testConfig, storeMetricsService);
151+
given(handleContext.writableStore(WritableNodeStore.class)).willReturn(writableStore);
152+
153+
given(handleContext.body())
154+
.willReturn(TransactionBody.newBuilder().nodeDelete(txn).build());
155+
given(handleContext.writableStore(WritableNodeStore.class)).willReturn(writableStore);
156+
HandleException thrown = (HandleException) catchThrowable(() -> subject.handle(handleContext));
157+
assertThat(thrown.getStatus()).isEqualTo(INVALID_NODE_ID);
158+
}
159+
160+
@Test
161+
@DisplayName("Handle works as expected")
162+
void handleWorksAsExpected() {
163+
final var txn = newDeleteTxn().nodeDeleteOrThrow();
164+
165+
final var existingNode = writableStore.get(WELL_KNOWN_NODE_ID);
166+
assertThat(existingNode).isNotNull();
167+
assertThat(existingNode.deleted()).isFalse();
168+
169+
given(handleContext.body())
170+
.willReturn(TransactionBody.newBuilder().nodeDelete(txn).build());
171+
given(handleContext.writableStore(WritableNodeStore.class)).willReturn(writableStore);
172+
173+
subject.handle(handleContext);
174+
175+
final var changedFile = writableStore.get(WELL_KNOWN_NODE_ID);
176+
177+
assertThat(changedFile).isNotNull();
178+
assertThat(changedFile.deleted()).isTrue();
179+
}
180+
181+
@Test
182+
@DisplayName("Node already deleted returns error")
183+
void noFileKeys() {
184+
givenValidNode(true);
185+
refreshStoresWithCurrentNodeInBothReadableAndWritable();
186+
187+
final var txn = newDeleteTxn().nodeDeleteOrThrow();
188+
189+
final var existingNode = writableStore.get(WELL_KNOWN_NODE_ID);
190+
assertThat(existingNode).isNotNull();
191+
assertThat(existingNode.deleted()).isTrue();
192+
193+
given(handleContext.body())
194+
.willReturn(TransactionBody.newBuilder().nodeDelete(txn).build());
195+
given(handleContext.writableStore(WritableNodeStore.class)).willReturn(writableStore);
196+
// expect:
197+
assertFailsWith(() -> subject.handle(handleContext), ResponseCodeEnum.NODE_DELETED);
198+
}
199+
200+
private TransactionBody newDeleteTxn() {
201+
final var txnId = TransactionID.newBuilder().accountID(payerId).build();
202+
final var deleteFileBuilder = NodeDeleteTransactionBody.newBuilder().nodeId(WELL_KNOWN_NODE_ID);
203+
return TransactionBody.newBuilder()
204+
.transactionID(txnId)
205+
.nodeDelete(deleteFileBuilder.build())
206+
.build();
207+
}
208+
209+
private static void assertFailsWith(final Runnable something, final ResponseCodeEnum status) {
210+
assertThatThrownBy(something::run)
211+
.isInstanceOf(HandleException.class)
212+
.extracting(ex -> ((HandleException) ex).getStatus())
213+
.isEqualTo(status);
214+
}
215+
}

0 commit comments

Comments
 (0)