Skip to content

Commit

Permalink
Merge branch 'develop' into cherry-pick-12714-pureChecks-Create-to-de…
Browse files Browse the repository at this point in the history
…velop
  • Loading branch information
thomas-swirlds-labs authored May 9, 2024
2 parents 5dcc4e2 + 0ab8f3d commit 690f9c1
Show file tree
Hide file tree
Showing 57 changed files with 2,011 additions and 183 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,11 @@
import com.hedera.node.app.service.mono.state.virtual.VirtualBlobValue;
import com.hedera.node.app.service.mono.stream.RecordsRunningHashLeaf;
import com.hedera.node.app.service.mono.utils.EntityNum;
import com.swirlds.base.time.Time;
import com.swirlds.common.AutoCloseableNonThrowing;
import com.swirlds.common.config.singleton.ConfigurationHolder;
import com.swirlds.common.constructable.ConstructableRegistry;
import com.swirlds.common.constructable.ConstructableRegistryException;
import com.swirlds.common.context.DefaultPlatformContext;
import com.swirlds.common.crypto.CryptographyHolder;
import com.swirlds.common.metrics.noop.NoOpMetrics;
import com.swirlds.common.context.PlatformContext;
import com.swirlds.config.api.Configuration;
import com.swirlds.config.api.ConfigurationBuilder;
import com.swirlds.config.extensions.sources.LegacyFileConfigSource;
Expand Down Expand Up @@ -355,8 +352,7 @@ private Pair<ReservedSignedState, ServicesState> dehydrate(@NonNull final List<P

registerConstructables();

final var platformContext = new DefaultPlatformContext(
buildConfiguration(configurationPaths), new NoOpMetrics(), CryptographyHolder.get(), Time.getCurrent());
final var platformContext = PlatformContext.create(buildConfiguration(configurationPaths));

ReservedSignedState rss;
try {
Expand Down
1 change: 0 additions & 1 deletion hedera-node/cli-clients/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
requires com.swirlds.config.extensions;
requires com.swirlds.fchashmap;
requires com.swirlds.merkle;
requires com.swirlds.metrics.api;
requires com.swirlds.virtualmap;
requires io.github.classgraph;
requires org.apache.commons.lang3;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Introduce TokenClaimAirdrop transaction
## Purpose

We need to add a new functionality that would make it possible for an airdrop receiver to accept a pending airdrop transfer. This would be the only way for a receiver, which hasn't been associated to a given token to accept an airdropped token that has been in pending airdrops state.

## Goals

1. Define new `TokenClaimAirdrop` HAPI transaction
2. Implement token claim airdrop transaction handler logic

## Non-Goals

- Implement token claim airdrop in system contract functions

## Architecture

The implementation related to the new `TokenClaimAirdrop` transaction will be gated behind a `tokens.airdrops.claim.enabled` feature flag.

### HAPI updates

Create new transaction type as defined in the HIP:

```protobuf
/**
* Token claim airdrop<br/>
* Complete one or more pending transfers on behalf of the recipient(s) for each airdrop.<br/>
* The sender MUST have sufficient balance to fulfill the airdrop at the time of claim. If the
* sender does not have sufficient balance, the claim SHALL fail.
*
* Each pending airdrop successfully claimed SHALL be removed from state and SHALL NOT be available
* to claim again.
*
* Each claim SHALL be represented in the transaction body and SHALL NOT be restated
* in the record file.<br/>
* All claims MUST succeed for this transaction to succeed.
*/
message TokenClaimAirdropTransactionBody {
/**
* A list of one or more pending airdrop identifiers.<br/>
* This transaction MUST be signed by the account referenced in the `receiver_id` for
* each entry in this list.
* <p>
* This list MUST contain between 1 and 10 entries, inclusive.<br/>
* This list MUST NOT have any duplicate entries.
*/
repeated PendingAirdropId pending_airdrops = 1;
}
/**
* A unique, composite, identifier for a pending airdrop.
*
* Each pending airdrop SHALL be uniquely identified by a PendingAirdropId.
* A PendingAirdropId SHALL be recorded when created and MUST be provided in any transaction
* that would modify that pending airdrop (such as a `claimAirdrop` or `cancelAirdrop`).
*/
message PendingAirdropId {
/**
* A sending account.<br/>
* This is the account that initiated, and SHALL fund, this pending airdrop.<br/>
* This field is REQUIRED.
*/
AccountID sender_id = 1;
/**
* A receiving account.<br/>
* This is the ID of the account that SHALL receive the airdrop.<br/>
* This field is REQUIRED.
*/
AccountID receiver_id = 2;
oneof token_reference {
/**
* A token ID.<br/>
* This is the type of token for a fungible/common token airdrop.<br/>
* This field is REQUIRED for a fungible/common token and MUST NOT be used for a
* non-fungible/unique token.
*/
TokenID fungible_token_type = 3;
/**
* The id of a single NFT, consisting of a Token ID and serial number.<br/>
* This is the type of token for a non-fungible/unique token airdrop.<br/>
* This field is REQUIRED for a non-fungible/unique token and MUST NOT be used for a
* fungible/common token.
*/
NftID non_fungible_token = 4;
}
}
```

Add new RPC to `TokenService` :

```protobuf
service TokenService {
// ...
/**
* Claim one or more pending airdrops.<br/>
* This transaction MUST be signed by _each_ account *receiving* an airdrop to be claimed.<br>
* If a Sender lacks sufficient balance to fulfill the airdrop at the time the claim is made,
* that claim SHALL fail.
*/
rpc claimAirdrop (Transaction) returns (TransactionResponse);
}
```

### Fees

The basic `TokenClaimAirdrop` fee should be proportional to the number of airdrops being claimed in the transaction.

An update into the `feeSchedule` file would be needed to specify that.

### Services updates

- Update `TokenServiceDefinition` class to include the new RPC method definition for claiming airdrops
- Implement new `TokenClaimAirdropHandler` class which should be invoked when the gRPC server handles `TokenClaimAirdrop` transactions. The class should be responsible for:
- Pure checks: validation logic based only on the transaction body itself in order to verify if the transaction is valid one
- Verify that the pending airdrops list contains between 1 and 10 entries, inclusive
- Verify that the pending airdrops list does not have any duplicate entries
- Pre-handle:
- The transaction must be signed by the account referenced by a `receiver_id` for each entry in the pending airdrops list
- Confirm that for the given pending airdrops ids in the transaction there are corresponding pending transfers existing in state
- Check if the sender has sufficient balance to fulfill the airdrop
- Handle:
- Any additional validation depending on config or state i.e. semantics checks
- The business logic for claiming pending airdrops
- We need to create a token association between each `receiver_id` and `token_reference`, future rents for token association slot should be paid by `receiver_id`
- Since we would have the signature of the receiver, even if it's an account with `receiver_sig_required=true`, the claim would implicitly work properly
- Then we should transfer the claimed tokens to each `receiver_id`
- We can dispatch synthetic crypto transfer for this, but we must skip the assessment of custom fees
- In case of a fungible token claim
- Create a synthetic `CryptoTransfer` with the corresponding `sender`, `receiver`, `token` and `amount` based on the `PendingAirdropId` and the corresponding `PendingAirdropValue`
- Then delegate it to the `CryptoTransfer.handler()` to execute the transfer
- In case of an NFT claim
- Create a synthetic `CryptoTransfer` with the corresponding `sender`, `receiver`, `token` and `serial number` based on the based on the `PendingAirdropId`
- Then delegate it to `CryptoTransfer.handler()` to execute the transfer
- Token transfers and associations should be externalized using the `tokenTransferLists` and `automatic_token_associations` fields in the transaction record
- Fees calculation

### Zero-Balance accounts

An account with no open auto-association slots can receive airdrops but must send a `TokenClaimAirdrop` transaction, which requires a payer. If the account has zero hbars, then it can still claim the transfer if someone else is willing to pay for that transaction. For example, a Dapp could be the payer on the transaction. Both the Dapp and the account must sign the transaction.

### Hollow accounts

Any existing hollow accounts that were created before [HIP-904](https://hips.hedera.com/hip/hip-904) will have no or limited number of `maxAutoAssociations` depending on if they were created with HBAR or token transfer respectively. That means an airdrop of unassociated tokens to such accounts will result in a pending transfer.
Performing `TokenClaimAirdrop` for such hollow account will also complete the account by setting its key which will obtained from the required transaction signature. Completing the hollow account should not modify the `maxAutoAssociations` on the account. That should always be an explicit step by a user.

## Acceptance Tests

All of the expected behaviour described below should be present only if the new `TokenClaimAirdrop` feature flag is enabled.

- Given existing pending airdrop in state when valid `TokenClaimAirdrop` transaction containing entry for the same pending airdrop is performed then the `TokenClaimAirdrop` should succeed resulting in:
- the tokens being claimed should be automatically associated with the `receiver_id` account
- the tokens being claimed should be transferred to the `receiver_id` account
- the pending airdrop should be removed from state
- Given a successful `TokenClaimAirdrop` transaction having a hollow account as `receiver_id` should also complete the account without modifying its `maxAutoAssociations` value
- Given successful `TokenClaimAirdrop` when another `TokenClaimAirdrop` for the same airdrop is performed then the second `TokenClaimAirdrop` should fail
- `TokenClaimAirdrop` transaction with no pending airdrops entries should fail
- `TokenClaimAirdrop` transaction with more than 10 pending airdrops entries should fail
- `TokenClaimAirdrop` transaction containing duplicate entries should fail
- `TokenClaimAirdrop` transaction containing pending airdrops entries which do not exist in state should fail
- `TokenClaimAirdrop` transaction not signed by the account referenced by a `receiver_id` for each entry in the pending airdrops list should fail
- `TokenClaimAirdrop` transaction with a `sender_id` account that does not have sufficient balance of the claimed token should fail
Original file line number Diff line number Diff line change
Expand Up @@ -273,8 +273,8 @@ private void runHtsCallAndExpect(
component.config().getConfigData(HederaConfig.class),
HederaFunctionality.CONTRACT_CALL,
new PendingCreationMetadataRef()),
new HandleHederaNativeOperations(context),
new HandleSystemContractOperations(context));
new HandleHederaNativeOperations(context, null),
new HandleSystemContractOperations(context, null));
given(proxyUpdater.enhancement()).willReturn(enhancement);
given(frame.getWorldUpdater()).willReturn(proxyUpdater);
given(frame.getSenderAddress()).willReturn(sender);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import static java.util.Objects.requireNonNull;

import com.hedera.hapi.node.base.HederaFunctionality;
import com.hedera.hapi.node.base.Key;
import com.hedera.hapi.node.base.SubType;
import com.hedera.hapi.node.transaction.ExchangeRate;
import com.hedera.node.app.service.contract.impl.annotations.ChildTransactionResourcePrices;
Expand All @@ -42,6 +43,7 @@
import com.hedera.node.app.service.contract.impl.hevm.HederaEvmContext;
import com.hedera.node.app.service.contract.impl.hevm.HederaWorldUpdater;
import com.hedera.node.app.service.contract.impl.hevm.HydratedEthTxData;
import com.hedera.node.app.service.contract.impl.infra.EthTxSigsCache;
import com.hedera.node.app.service.contract.impl.infra.EthereumCallDataHydration;
import com.hedera.node.app.service.contract.impl.records.ContractOperationRecordBuilder;
import com.hedera.node.app.service.contract.impl.state.EvmFrameStateFactory;
Expand All @@ -55,6 +57,7 @@
import com.hedera.node.app.spi.workflows.FunctionalityResourcePrices;
import com.hedera.node.app.spi.workflows.HandleContext;
import com.hedera.node.config.data.HederaConfig;
import com.hedera.pbj.runtime.io.buffer.Bytes;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
Expand Down Expand Up @@ -127,6 +130,29 @@ static HydratedEthTxData maybeProvideHydratedEthTxData(
: null;
}

/**
* If the top-level transaction is an {@code EthereumTransaction}, provides an ECDSA {@link Key} with
* the public key of the sender address; otherwise returns {@code null}.
*
* @param ethTxSigsCache the cache of Ethereum transaction signatures
* @param hydratedEthTxData the hydrated Ethereum transaction data, if this is an {@code EthereumTransaction}
* @return the ECDSA {@link Key} with the public key of the sender address, or {@code null}
*/
@Provides
@Nullable
@TransactionScope
static Key provideSenderEcdsaKey(
@NonNull final EthTxSigsCache ethTxSigsCache, @Nullable final HydratedEthTxData hydratedEthTxData) {
if (hydratedEthTxData != null && hydratedEthTxData.isAvailable()) {
final var ethTxSigs = ethTxSigsCache.computeIfAbsent(hydratedEthTxData.ethTxDataOrThrow());
return Key.newBuilder()
.ecdsaSecp256k1(Bytes.wrap(ethTxSigs.publicKey()))
.build();
} else {
return null;
}
}

@Provides
@TransactionScope
static ActionSidecarContentTracer provideActionSidecarContentTracer() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

package com.hedera.node.app.service.contract.impl.exec.scope;

import static com.hedera.node.app.service.contract.impl.exec.scope.VerificationStrategy.Decision.INVALID;
import static com.hedera.node.app.service.contract.impl.exec.scope.VerificationStrategy.Decision.VALID;
import static java.util.Objects.requireNonNull;

import com.hedera.hapi.node.base.Key;
import edu.umd.cs.findbugs.annotations.NonNull;

Expand All @@ -35,11 +39,30 @@ public EitherOrVerificationStrategy(
this.secondStrategy = secondStrategy;
}

/**
* {@inheritDoc}
*
* <p>Returns {@link VerificationStrategy.Decision#INVALID} if both strategies agree the
* key's signature is invalid; {@link VerificationStrategy.Decision#VALID} if either strategy
* says the key is valid; and {@link VerificationStrategy.Decision#DELEGATE_TO_CRYPTOGRAPHIC_VERIFICATION}
* otherwise. (I.e., if one strategy says the key's signature is invalid but the other allows
* us to delegate.)
*
* @param key the key whose signature is to be verified
* @return the decision of the strategy
*/
@Override
public Decision decideForPrimitive(@NonNull final Key key) {
return firstStrategy.decideForPrimitive(key) == Decision.VALID
|| secondStrategy.decideForPrimitive(key) == Decision.VALID
? Decision.VALID
: Decision.INVALID;
requireNonNull(key);
final var firstDecision = firstStrategy.decideForPrimitive(key);
if (firstDecision == VALID) {
return VALID;
} else {
final var secondDecision = secondStrategy.decideForPrimitive(key);
if (secondDecision == VALID) {
return VALID;
}
return secondDecision == INVALID ? firstDecision : secondDecision;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import com.hedera.node.app.spi.workflows.HandleException;
import com.hedera.pbj.runtime.io.buffer.Bytes;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import javax.inject.Inject;
import org.hyperledger.besu.evm.frame.MessageFrame;

Expand All @@ -52,9 +53,13 @@
public class HandleHederaNativeOperations implements HederaNativeOperations {
private final HandleContext context;

@Nullable
private final Key maybeEthSenderKey;

@Inject
public HandleHederaNativeOperations(@NonNull final HandleContext context) {
public HandleHederaNativeOperations(@NonNull final HandleContext context, @Nullable final Key maybeEthSenderKey) {
this.context = requireNonNull(context);
this.maybeEthSenderKey = maybeEthSenderKey;
}

/**
Expand Down Expand Up @@ -154,7 +159,7 @@ public void finalizeHollowAccountAsContract(@NonNull final Bytes evmAddress) {
final AccountID toEntityId,
@NonNull final VerificationStrategy strategy) {
final var to = requireNonNull(getAccount(toEntityId));
final var signatureTest = strategy.asSignatureTestIn(context);
final var signatureTest = strategy.asSignatureTestIn(context, maybeEthSenderKey);
if (to.receiverSigRequired() && !signatureTest.test(to.keyOrThrow())) {
return INVALID_SIGNATURE;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import com.hedera.node.app.service.contract.impl.records.ContractCallRecordBuilder;
import com.hedera.node.app.spi.workflows.HandleContext;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.util.function.Predicate;
import javax.inject.Inject;
import org.apache.tuweni.bytes.Bytes;
Expand All @@ -50,17 +51,21 @@
public class HandleSystemContractOperations implements SystemContractOperations {
private final HandleContext context;

@Nullable
private final Key maybeEthSenderKey;

@Inject
public HandleSystemContractOperations(@NonNull final HandleContext context) {
public HandleSystemContractOperations(@NonNull final HandleContext context, @Nullable Key maybeEthSenderKey) {
this.context = requireNonNull(context);
this.maybeEthSenderKey = maybeEthSenderKey;
}

/**
* {@inheritDoc}
*/
@Override
public @NonNull Predicate<Key> activeSignatureTestWith(@NonNull final VerificationStrategy strategy) {
return strategy.asSignatureTestIn(context);
return strategy.asSignatureTestIn(context, maybeEthSenderKey);
}

/**
Expand Down
Loading

0 comments on commit 690f9c1

Please sign in to comment.