PrivacyBlockProcessor.java
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.mainnet;
import static org.hyperledger.besu.ethereum.core.PrivacyParameters.FLEXIBLE_PRIVACY;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.enclave.Enclave;
import org.hyperledger.besu.enclave.EnclaveClientException;
import org.hyperledger.besu.enclave.types.ReceiveResponse;
import org.hyperledger.besu.ethereum.BlockProcessingResult;
import org.hyperledger.besu.ethereum.chain.Blockchain;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.Deposit;
import org.hyperledger.besu.ethereum.core.MutableWorldState;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.core.Withdrawal;
import org.hyperledger.besu.ethereum.privacy.PrivateStateGenesisAllocator;
import org.hyperledger.besu.ethereum.privacy.PrivateStateRehydration;
import org.hyperledger.besu.ethereum.privacy.PrivateStateRootResolver;
import org.hyperledger.besu.ethereum.privacy.PrivateTransactionWithMetadata;
import org.hyperledger.besu.ethereum.privacy.storage.PrivacyGroupHeadBlockMap;
import org.hyperledger.besu.ethereum.privacy.storage.PrivateBlockMetadata;
import org.hyperledger.besu.ethereum.privacy.storage.PrivateMetadataUpdater;
import org.hyperledger.besu.ethereum.privacy.storage.PrivateStateStorage;
import org.hyperledger.besu.ethereum.privacy.storage.PrivateTransactionMetadata;
import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PrivacyBlockProcessor implements BlockProcessor {
private static final Logger LOG = LoggerFactory.getLogger(PrivacyBlockProcessor.class);
private final BlockProcessor blockProcessor;
private final ProtocolSchedule protocolSchedule;
private final Enclave enclave;
private final PrivateStateStorage privateStateStorage;
private final WorldStateArchive privateWorldStateArchive;
private final PrivateStateRootResolver privateStateRootResolver;
private final PrivateStateGenesisAllocator privateStateGenesisAllocator;
private WorldStateArchive publicWorldStateArchive;
public PrivacyBlockProcessor(
final BlockProcessor blockProcessor,
final ProtocolSchedule protocolSchedule,
final Enclave enclave,
final PrivateStateStorage privateStateStorage,
final WorldStateArchive privateWorldStateArchive,
final PrivateStateRootResolver privateStateRootResolver,
final PrivateStateGenesisAllocator privateStateGenesisAllocator) {
this.blockProcessor = blockProcessor;
this.protocolSchedule = protocolSchedule;
this.enclave = enclave;
this.privateStateStorage = privateStateStorage;
this.privateWorldStateArchive = privateWorldStateArchive;
this.privateStateRootResolver = privateStateRootResolver;
this.privateStateGenesisAllocator = privateStateGenesisAllocator;
}
public void setPublicWorldStateArchive(final WorldStateArchive publicWorldStateArchive) {
this.publicWorldStateArchive = publicWorldStateArchive;
}
@Override
public BlockProcessingResult processBlock(
final Blockchain blockchain,
final MutableWorldState worldState,
final BlockHeader blockHeader,
final List<Transaction> transactions,
final List<BlockHeader> ommers,
final Optional<List<Withdrawal>> withdrawals,
final Optional<List<Deposit>> deposits,
final PrivateMetadataUpdater privateMetadataUpdater) {
if (privateMetadataUpdater != null) {
throw new IllegalArgumentException("PrivateMetadataUpdater passed in is not null.");
}
maybeRehydrate(blockchain, blockHeader, transactions);
final PrivateMetadataUpdater metadataUpdater =
new PrivateMetadataUpdater(blockHeader, privateStateStorage);
final BlockProcessingResult result =
blockProcessor.processBlock(
blockchain,
worldState,
blockHeader,
transactions,
ommers,
withdrawals,
deposits,
metadataUpdater);
metadataUpdater.commit();
return result;
}
void maybeRehydrate(
final Blockchain blockchain,
final BlockHeader blockHeader,
final List<Transaction> transactions) {
transactions.stream()
.filter(this::onchainAddToGroupPrivateMarkerTransactions)
.forEach(
pmt -> {
final Bytes32 privateTransactionsLookupId =
Bytes32.wrap(pmt.getPayload().slice(32, 32));
try {
final ReceiveResponse receiveResponse =
enclave.receive(privateTransactionsLookupId.toBase64String());
final List<PrivateTransactionWithMetadata> privateTransactionWithMetadataList =
PrivateTransactionWithMetadata.readListFromPayload(
Bytes.wrap(Base64.getDecoder().decode(receiveResponse.getPayload())));
final Bytes32 privacyGroupId =
Bytes32.wrap(
privateTransactionWithMetadataList
.get(0)
.getPrivateTransaction()
.getPrivacyGroupId()
.get());
final List<PrivateTransactionWithMetadata> actualListToRehydrate =
transactionsInGroupThatNeedToBeApplied(
blockHeader, privateTransactionWithMetadataList, privacyGroupId);
if (actualListToRehydrate.size() > 0) {
LOG.debug(
"Rehydrating privacy group {}, number of transactions to be rehydrated is {} out of a total number of {} transactions.",
privacyGroupId.toString(),
actualListToRehydrate.size(),
privateTransactionWithMetadataList.size());
final PrivateStateRehydration privateStateRehydration =
new PrivateStateRehydration(
privateStateStorage,
blockchain,
protocolSchedule,
publicWorldStateArchive,
privateWorldStateArchive,
privateStateRootResolver,
privateStateGenesisAllocator);
privateStateRehydration.rehydrate(actualListToRehydrate);
privateStateStorage
.updater()
.putAddDataKey(privacyGroupId, privateTransactionsLookupId)
.commit();
}
} catch (final EnclaveClientException e) {
// we were not being added because we have not found the add blob
}
});
}
private boolean onchainAddToGroupPrivateMarkerTransactions(final Transaction t) {
return t.getTo().isPresent()
&& t.getTo().equals(Optional.of(FLEXIBLE_PRIVACY))
&& t.getPayload().size() == 64;
}
private List<PrivateTransactionWithMetadata> transactionsInGroupThatNeedToBeApplied(
final BlockHeader blockHeader,
final List<PrivateTransactionWithMetadata> privateTransactionWithMetadataList,
final Bytes32 privacyGroupId) {
// if we are the member adding another member we do not have to rehydrate
// if we have been removed from the group at some point we only need to rehydrate from where we
// were removed
// if we are a new member we need to rehydrate the complete state
List<PrivateTransactionWithMetadata> actualList = privateTransactionWithMetadataList;
final Optional<PrivacyGroupHeadBlockMap> maybePrivacyGroupHeadBlockMap =
privateStateStorage.getPrivacyGroupHeadBlockMap(blockHeader.getParentHash());
if (maybePrivacyGroupHeadBlockMap.isPresent()) {
final PrivacyGroupHeadBlockMap privacyGroupHeadBlockMap = maybePrivacyGroupHeadBlockMap.get();
final Hash lastBlockWithTx = privacyGroupHeadBlockMap.get(privacyGroupId);
if (lastBlockWithTx != null) {
// we are or have been a member of the privacy group
final PrivateBlockMetadata nodeLatestBlockMetadata =
privateStateStorage
.getPrivateBlockMetadata(lastBlockWithTx, privacyGroupId)
.orElseThrow();
final List<PrivateTransactionMetadata> nodeLatestPrivateTxMetadataList =
nodeLatestBlockMetadata.getPrivateTransactionMetadataList();
final Hash nodeLatestStateRoot =
nodeLatestPrivateTxMetadataList
.get(nodeLatestPrivateTxMetadataList.size() - 1)
.getStateRoot();
final Hash latestStateRootFromRehydrationList =
privateTransactionWithMetadataList
.get(privateTransactionWithMetadataList.size() - 1)
.getPrivateTransactionMetadata()
.getStateRoot();
if (nodeLatestStateRoot.equals(latestStateRootFromRehydrationList)) {
// we are already on the latest state root, which means that we are the member adding a
// new member
actualList = Collections.emptyList();
} else {
// we are being added, but do not have to rehydrate all private transactions
final Hash nodeLatestPrivateMarkerTransactionHash =
nodeLatestPrivateTxMetadataList
.get(nodeLatestPrivateTxMetadataList.size() - 1)
.getPrivateMarkerTransactionHash();
for (int i = 0; i < privateTransactionWithMetadataList.size(); i++) {
if (!privateTransactionWithMetadataList
.get(i)
.getPrivateTransactionMetadata()
.getPrivateMarkerTransactionHash()
.equals(nodeLatestPrivateMarkerTransactionHash)) {
continue;
}
if (privateTransactionWithMetadataList.size() - 1 == i) {
actualList = Collections.emptyList(); // nothing needs to be re-hydrated
} else {
actualList =
privateTransactionWithMetadataList.subList(
i + 1, privateTransactionWithMetadataList.size());
}
break;
}
}
}
}
return actualList;
}
}