Skip to content

Commit 4fa87b3

Browse files
authored
chore: cherry pick #13479 - redirectForAccount proxy contract support (#13668)
Signed-off-by: lukelee-sl <[email protected]>
1 parent af794cf commit 4fa87b3

38 files changed

+1595
-401
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
contracts.chainId=298
22
contracts.precompile.atomicCryptoTransfer.enabled=true
3+
contracts.systemContract.accountService.enabled=true

hedera-node/configuration/previewnet/application.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
bootstrap.genesisPublicKey=c249a323c878f5b5e2daccda6d731e6fdc32f870228d1cd4fae559d947dbc36c
88
contracts.chainId=297
99
contracts.precompile.atomicCryptoTransfer.enabled=true
10+
contracts.systemContract.accountService.enabled=true
1011
ledger.id=0x02

hedera-node/hedera-app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ xtestModuleInfo {
115115
requires("org.mockito")
116116
requires("org.mockito.junit.jupiter")
117117
requires("tuweni.bytes")
118+
requires("tuweni.units")
118119
runtimeOnly("io.netty.transport.epoll.linux.x86_64")
119120
runtimeOnly("io.netty.transport.epoll.linux.aarch_64")
120121
}

hedera-node/hedera-app/src/xtest/java/contract/AbstractContractXTest.java

Lines changed: 124 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@
5757
import com.hedera.node.app.service.contract.impl.exec.scope.VerificationStrategies;
5858
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.common.Call;
5959
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.common.CallAddressChecks;
60+
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.has.HasCallAttempt;
61+
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.has.HasCallFactory;
6062
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.HtsCallAttempt;
6163
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.HtsCallFactory;
6264
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.SyntheticIds;
@@ -86,9 +88,11 @@
8688
import java.util.Map;
8789
import java.util.Optional;
8890
import java.util.function.Consumer;
91+
import org.apache.tuweni.units.bigints.UInt256;
8992
import org.hyperledger.besu.datatypes.Wei;
9093
import org.hyperledger.besu.evm.frame.MessageFrame;
9194
import org.hyperledger.besu.evm.precompile.PrecompiledContract;
95+
import org.jetbrains.annotations.NotNull;
9296
import org.junit.jupiter.api.BeforeEach;
9397
import org.mockito.Mock;
9498
import org.mockito.Mockito;
@@ -114,15 +118,24 @@ public abstract class AbstractContractXTest extends AbstractXTest {
114118
@Mock
115119
private CallAddressChecks addressChecks;
116120

117-
private HtsCallFactory callAttemptFactory;
121+
private HtsCallFactory callHtsAttemptFactory;
122+
123+
private HasCallFactory callHasAttemptFactory;
118124

119125
protected ContractScaffoldingComponent component;
120126

127+
public enum SystemContractCallType {
128+
HTS_CALL,
129+
HAS_CALL
130+
}
131+
121132
@BeforeEach
122133
void setUp() {
123134
component = DaggerContractScaffoldingComponent.factory().create(metrics, configuration(), storeMetricsService);
124-
callAttemptFactory = new HtsCallFactory(
135+
callHtsAttemptFactory = new HtsCallFactory(
125136
LIVE_SYNTHETIC_IDS, addressChecks, LIVE_VERIFICATION_STRATEGIES, component.callHtsTranslators());
137+
callHasAttemptFactory = new HasCallFactory(
138+
LIVE_SYNTHETIC_IDS, addressChecks, LIVE_VERIFICATION_STRATEGIES, component.callHasTranslators());
126139
}
127140

128141
protected Configuration configuration() {
@@ -196,13 +209,18 @@ private void runHtsCallAndExpectOnSuccess(
196209
@NonNull final org.apache.tuweni.bytes.Bytes input,
197210
@NonNull final Consumer<org.apache.tuweni.bytes.Bytes> outputAssertions,
198211
@Nullable final String context) {
199-
runHtsCallAndExpect(requiresDelegatePermission, sender, input, resultOnlyAssertion(result -> {
200-
assertEquals(
201-
MessageFrame.State.COMPLETED_SUCCESS,
202-
result.getState(),
203-
Optional.ofNullable(context).orElse("An unspecified operation") + " should have succeeded");
204-
outputAssertions.accept(result.getOutput());
205-
}));
212+
runCallAndExpect(
213+
requiresDelegatePermission,
214+
SystemContractCallType.HTS_CALL,
215+
sender,
216+
input,
217+
resultOnlyAssertion(result -> {
218+
assertEquals(
219+
MessageFrame.State.COMPLETED_SUCCESS,
220+
result.getState(),
221+
Optional.ofNullable(context).orElse("An unspecified operation") + " should have succeeded");
222+
outputAssertions.accept(result.getOutput());
223+
}));
206224
}
207225

208226
protected void runHtsCallAndExpectRevert(
@@ -233,7 +251,7 @@ private void internalRunHtsCallAndExpectRevert(
233251
@NonNull final org.apache.tuweni.bytes.Bytes input,
234252
@NonNull final ResponseCodeEnum status,
235253
@Nullable final String context) {
236-
runHtsCallAndExpect(false, sender, input, resultOnlyAssertion(result -> {
254+
runCallAndExpect(false, SystemContractCallType.HTS_CALL, sender, input, resultOnlyAssertion(result -> {
237255
assertEquals(
238256
MessageFrame.State.REVERT,
239257
result.getState(),
@@ -248,8 +266,59 @@ private void internalRunHtsCallAndExpectRevert(
248266
}));
249267
}
250268

251-
private void runHtsCallAndExpect(
269+
protected void runHasCallAndExpectOnSuccess(
270+
@NonNull final org.hyperledger.besu.datatypes.Address sender,
271+
@NonNull final org.apache.tuweni.bytes.Bytes input,
272+
@NonNull final Consumer<org.apache.tuweni.bytes.Bytes> outputAssertions,
273+
@Nullable final String context) {
274+
runCallAndExpect(false, SystemContractCallType.HAS_CALL, sender, input, resultOnlyAssertion(result -> {
275+
assertEquals(
276+
MessageFrame.State.COMPLETED_SUCCESS,
277+
result.getState(),
278+
Optional.ofNullable(context).orElse("An unspecified operation") + " should have succeeded");
279+
outputAssertions.accept(result.getOutput());
280+
}));
281+
}
282+
283+
protected void runHasCallAndExpectRevert(
284+
@NonNull final org.hyperledger.besu.datatypes.Address sender,
285+
@NonNull final org.apache.tuweni.bytes.Bytes input,
286+
@NonNull final ResponseCodeEnum status,
287+
@NonNull final String context,
288+
final boolean useOrdinal) {
289+
internalRunHasCallAndExpectRevert(sender, input, status, context, useOrdinal);
290+
}
291+
292+
private void internalRunHasCallAndExpectRevert(
293+
@NonNull final org.hyperledger.besu.datatypes.Address sender,
294+
@NonNull final org.apache.tuweni.bytes.Bytes input,
295+
@NonNull final ResponseCodeEnum status,
296+
@Nullable final String context,
297+
final boolean useOrdinal) {
298+
runCallAndExpect(false, SystemContractCallType.HAS_CALL, sender, input, resultOnlyAssertion(result -> {
299+
assertEquals(
300+
MessageFrame.State.REVERT,
301+
result.getState(),
302+
Optional.ofNullable(context).orElse("An unspecified operation") + " should have reverted");
303+
ResponseCodeEnum actualReason;
304+
if (useOrdinal) {
305+
actualReason = ResponseCodeEnum.fromProtobufOrdinal(
306+
UInt256.fromBytes(result.getOutput()).intValue());
307+
} else {
308+
actualReason = ResponseCodeEnum.fromString(
309+
new String(result.getOutput().toArrayUnsafe()));
310+
}
311+
assertEquals(
312+
status,
313+
actualReason,
314+
"'" + Optional.ofNullable(context).orElse("An unspecified operation")
315+
+ "' should have reverted with " + status + " but instead reverted with " + actualReason);
316+
}));
317+
}
318+
319+
private void runCallAndExpect(
252320
final boolean requiresDelegatePermission,
321+
@NonNull final SystemContractCallType callType,
253322
@NonNull final org.hyperledger.besu.datatypes.Address sender,
254323
@NonNull final org.apache.tuweni.bytes.Bytes input,
255324
@NonNull final Consumer<Call.PricedResult> resultAssertions) {
@@ -288,15 +357,39 @@ private void runHtsCallAndExpect(
288357
given(addressChecks.hasParentDelegateCall(frame)).willReturn(requiresDelegatePermission);
289358
Mockito.lenient().when(frame.getValue()).thenReturn(Wei.MAX_WEI);
290359

291-
final var attempt = callAttemptFactory.createCallAttemptFrom(input, DIRECT_OR_PROXY_REDIRECT, frame);
292-
final var call = attempt.asExecutableCall();
360+
Call.PricedResult pricedResult =
361+
switch (callType) {
362+
case HTS_CALL -> {
363+
yield getHtsPricedResult(input);
364+
}
365+
case HAS_CALL -> {
366+
yield getHasPricedResult(input);
367+
}
368+
};
293369

294-
final var pricedResult = requireNonNull(call).execute(frame);
295370
resultAssertions.accept(pricedResult);
296371
// Note that committing a reverted calls should have no effect on state
297372
((SavepointStackImpl) context.savepointStack()).commitFullStack();
298373
}
299374

375+
@NotNull
376+
private Call.PricedResult getHtsPricedResult(@NonNull org.apache.tuweni.bytes.Bytes input) {
377+
final var attempt = callHtsAttemptFactory.createCallAttemptFrom(input, DIRECT_OR_PROXY_REDIRECT, frame);
378+
final var call = attempt.asExecutableCall();
379+
380+
final var pricedResult = requireNonNull(call).execute(frame);
381+
return pricedResult;
382+
}
383+
384+
@NotNull
385+
private Call.PricedResult getHasPricedResult(@NonNull org.apache.tuweni.bytes.Bytes input) {
386+
final var attempt = callHasAttemptFactory.createCallAttemptFrom(input, DIRECT_OR_PROXY_REDIRECT, frame);
387+
final var call = attempt.asExecutableCall();
388+
389+
final var pricedResult = requireNonNull(call).execute(frame);
390+
return pricedResult;
391+
}
392+
300393
protected TransactionBody createCallTransactionBody(
301394
final AccountID payer,
302395
final long value,
@@ -379,6 +472,23 @@ public static org.apache.tuweni.bytes.Bytes bytesForRedirect(
379472
org.apache.tuweni.bytes.Bytes.of(subSelector));
380473
}
381474

475+
//
476+
// Encode given a ByteBuffer and accountId input bytes for a call to a given contract.
477+
// Largely, this is used to encode the call to redirectToAccount() proxy contract for testing purposes.
478+
// Copied here from TestHelpers as that class is not available in this package.
479+
public static org.apache.tuweni.bytes.Bytes bytesForRedirectForAccount(
480+
final ByteBuffer encodedErcCall, final AccountID accountID) {
481+
return bytesForRedirectForAccount(encodedErcCall.array(), asLongZeroAddress(accountID.accountNum()));
482+
}
483+
484+
public static org.apache.tuweni.bytes.Bytes bytesForRedirectForAccount(
485+
final byte[] subSelector, final org.hyperledger.besu.datatypes.Address accountAddress) {
486+
return org.apache.tuweni.bytes.Bytes.concatenate(
487+
org.apache.tuweni.bytes.Bytes.wrap(HasCallAttempt.REDIRECT_FOR_ACCOUNT.selector()),
488+
accountAddress,
489+
org.apache.tuweni.bytes.Bytes.of(subSelector));
490+
}
491+
382492
public static org.apache.tuweni.bytes.Bytes asBytesResult(final ByteBuffer encoded) {
383493
return org.apache.tuweni.bytes.Bytes.wrap(encoded.array());
384494
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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 contract;
18+
19+
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ALLOWANCE_OWNER_ID;
20+
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS;
21+
import static contract.HtsErc721TransferXTestConstants.APPROVED_ADDRESS;
22+
import static contract.HtsErc721TransferXTestConstants.APPROVED_BESU_ADDRESS;
23+
import static contract.HtsErc721TransferXTestConstants.APPROVED_HEADLONG_ADDRESS;
24+
import static contract.HtsErc721TransferXTestConstants.APPROVED_ID;
25+
import static contract.XTestConstants.INVALID_ID;
26+
import static contract.XTestConstants.OWNER_ADDRESS;
27+
import static contract.XTestConstants.OWNER_HEADLONG_ADDRESS;
28+
import static contract.XTestConstants.OWNER_ID;
29+
import static contract.XTestConstants.SENDER_CONTRACT_ID_KEY;
30+
import static org.junit.jupiter.api.Assertions.assertEquals;
31+
32+
import com.esaulpaugh.headlong.abi.Tuple;
33+
import com.hedera.hapi.node.base.AccountID;
34+
import com.hedera.hapi.node.state.primitives.ProtoBytes;
35+
import com.hedera.hapi.node.state.token.Account;
36+
import com.hedera.hapi.node.state.token.AccountCryptoAllowance;
37+
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.has.hbarallowance.HbarAllowanceTranslator;
38+
import java.math.BigInteger;
39+
import java.util.HashMap;
40+
import java.util.Map;
41+
import org.apache.tuweni.bytes.Bytes;
42+
43+
/**
44+
* Exercises hbar allowance for a token via the following steps relative to an {@code OWNER} and {@code SENDER} accounts:
45+
* <ol>
46+
* <li>Get Hbar Allowance of OWNER for SPENDER via {@link HbarAllowanceTranslator#HBAR_ALLOWANCE_PROXY}.</li>
47+
* <li>Get Hbar Allowance of OWNER for SPENDER via {@link HbarAllowanceTranslator#HBAR_ALLOWANCE}.</li>
48+
* <li>Fail Hbar Allowance if OWNER is not found.</li>
49+
* </ol>
50+
*/
51+
public class HbarAllowanceXTest extends AbstractContractXTest {
52+
@Override
53+
protected void doScenarioOperations() {
54+
runHasCallAndExpectOnSuccess(
55+
APPROVED_BESU_ADDRESS,
56+
bytesForRedirectForAccount(
57+
HbarAllowanceTranslator.HBAR_ALLOWANCE_PROXY.encodeCallWithArgs(APPROVED_HEADLONG_ADDRESS),
58+
OWNER_ID),
59+
output -> assertEquals(
60+
asBytesResult(HbarAllowanceTranslator.HBAR_ALLOWANCE_PROXY
61+
.getOutputs()
62+
.encodeElements((long) SUCCESS.getNumber(), BigInteger.valueOf(1_000L))),
63+
output),
64+
"Successful execution of hbarAllowance for SENDER by OWNER.");
65+
runHasCallAndExpectOnSuccess(
66+
APPROVED_BESU_ADDRESS,
67+
Bytes.wrapByteBuffer(HbarAllowanceTranslator.HBAR_ALLOWANCE.encodeCall(
68+
Tuple.of(OWNER_HEADLONG_ADDRESS, APPROVED_HEADLONG_ADDRESS))),
69+
output -> assertEquals(
70+
asBytesResult(HbarAllowanceTranslator.HBAR_ALLOWANCE_PROXY
71+
.getOutputs()
72+
.encodeElements((long) SUCCESS.getNumber(), BigInteger.valueOf(1_000L))),
73+
output),
74+
"Successful execution of hbarAllowance for SENDER by OWNER.");
75+
runHasCallAndExpectRevert(
76+
APPROVED_BESU_ADDRESS,
77+
bytesForRedirectForAccount(
78+
HbarAllowanceTranslator.HBAR_ALLOWANCE_PROXY.encodeCallWithArgs(APPROVED_HEADLONG_ADDRESS),
79+
INVALID_ID),
80+
INVALID_ALLOWANCE_OWNER_ID,
81+
"Failed execution of hbarAllowance when OWNER is not found.",
82+
false);
83+
}
84+
85+
@Override
86+
protected Map<ProtoBytes, AccountID> initialAliases() {
87+
return new HashMap<>() {
88+
{
89+
put(ProtoBytes.newBuilder().value(OWNER_ADDRESS).build(), OWNER_ID);
90+
put(ProtoBytes.newBuilder().value(APPROVED_ADDRESS).build(), APPROVED_ID);
91+
}
92+
};
93+
}
94+
95+
@Override
96+
protected Map<AccountID, Account> initialAccounts() {
97+
return new HashMap<>() {
98+
{
99+
put(
100+
OWNER_ID,
101+
Account.newBuilder()
102+
.accountId(OWNER_ID)
103+
.alias(OWNER_ADDRESS)
104+
.key(SENDER_CONTRACT_ID_KEY)
105+
.cryptoAllowances(AccountCryptoAllowance.newBuilder()
106+
.amount(1_000L)
107+
.spenderId(APPROVED_ID)
108+
.build())
109+
.build());
110+
put(
111+
APPROVED_ID,
112+
Account.newBuilder()
113+
.accountId(APPROVED_ID)
114+
.alias(APPROVED_ADDRESS)
115+
.build());
116+
}
117+
};
118+
}
119+
}

0 commit comments

Comments
 (0)