diff --git a/src/main/java/com/iota/iri/service/snapshot/impl/SnapshotServiceImpl.java b/src/main/java/com/iota/iri/service/snapshot/impl/SnapshotServiceImpl.java
index a2fe3644dc..f73f28e352 100644
--- a/src/main/java/com/iota/iri/service/snapshot/impl/SnapshotServiceImpl.java
+++ b/src/main/java/com/iota/iri/service/snapshot/impl/SnapshotServiceImpl.java
@@ -1,6 +1,5 @@
package com.iota.iri.service.snapshot.impl;
-import com.iota.iri.conf.IotaConfig;
import com.iota.iri.conf.SnapshotConfig;
import com.iota.iri.controllers.ApproveeViewModel;
import com.iota.iri.controllers.MilestoneViewModel;
@@ -23,12 +22,16 @@
import com.iota.iri.utils.log.ProgressLogger;
import com.iota.iri.utils.log.interval.IntervalProgressLogger;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.*;
-import java.util.stream.Collectors;
-
/**
*
* Creates a service instance that allows us to access the business logic for {@link Snapshot}s.
@@ -43,6 +46,36 @@ public class SnapshotServiceImpl implements SnapshotService {
*/
private static final Logger log = LoggerFactory.getLogger(SnapshotServiceImpl.class);
+ /**
+ *
+ * Holds a limit for the amount of milestones we go back in time when generating the solid entry points (to speed up
+ * the snapshot creation).
+ *
+ *
+ * Note: Since the snapshot creation is a "continuous" process where we build upon the information gathered during
+ * the creation of previous snapshots, we do not need to analyze all previous milestones but can rely on
+ * slowly gathering the missing information over time. While this might lead to a situation where the very
+ * first snapshots taken by a node might generate snapshot files that can not reliably be used by other nodes
+ * to sync it is still a reasonable trade-off to reduce the load on the nodes. We just assume that anybody who
+ * wants to share his snapshots with the community as a way to bootstrap new nodes will run his snapshot
+ * enabled node for a few hours before sharing his files (this is a problem in very rare edge cases when
+ * having back-referencing transactions anyway).
+ *
+ */
+ private static final int OUTER_SHELL_SIZE = 100;
+
+ /**
+ *
+ * Maximum age in milestones since creation of solid entry points.
+ *
+ *
+ * Since it is possible to artificially keep old solid entry points alive by periodically attaching new transactions
+ * to them, we limit the life time of solid entry points and ignore them whenever they become too old. This is a
+ * measure against a potential attack vector where somebody might try to blow up the meta data of local snapshots.
+ *
+ */
+ private static final int SOLID_ENTRY_POINT_LIFETIME = 1000;
+
/**
* Holds the tangle object which acts as a database interface.
*/
@@ -56,12 +89,7 @@ public class SnapshotServiceImpl implements SnapshotService {
/**
* Holds the config with important snapshot specific settings.
*/
- private final IotaConfig config;
-
- /**
- * Minimum depth for generating solid entrypoints due to coordinator allowing 15 MS back attachment
- */
- private static final int MIN_LS_DEPTH_MAINNET = 15 + 1;
+ private final SnapshotConfig config;
/**
* Implements the snapshot service. See interface for more information.
@@ -69,7 +97,7 @@ public class SnapshotServiceImpl implements SnapshotService {
* @param snapshotProvider gives us access to the relevant snapshots.
* @param config configuration with snapshot specific settings.
*/
- public SnapshotServiceImpl(Tangle tangle, SnapshotProvider snapshotProvider, IotaConfig config) {
+ public SnapshotServiceImpl(Tangle tangle, SnapshotProvider snapshotProvider, SnapshotConfig config) {
this.tangle = tangle;
this.snapshotProvider = snapshotProvider;
this.config = config;
@@ -250,7 +278,6 @@ public Snapshot generateSnapshot(MilestoneSolidifier milestoneSolidifier, Milest
public Map generateSolidEntryPoints(MilestoneViewModel targetMilestone) throws SnapshotException {
Map solidEntryPoints = new HashMap<>();
solidEntryPoints.put(Hash.NULL_HASH, targetMilestone.index());
- solidEntryPoints.put(targetMilestone.getHash(), targetMilestone.index());
processOldSolidEntryPoints(tangle, snapshotProvider, targetMilestone, solidEntryPoints);
processNewSolidEntryPoints(tangle, snapshotProvider, targetMilestone, solidEntryPoints);
@@ -496,9 +523,100 @@ private void persistLocalSnapshot(SnapshotProvider snapshotProvider, Snapshot ne
snapshotProvider.getInitialSnapshot().update(newSnapshot);
}
-
- private boolean isAboveMinMilestone(int fromIndex, int toIndex) {
- return toIndex - fromIndex <= getSepDepth();
+
+ /**
+ *
+ * This method determines if a transaction is orphaned when none of its approvers is confirmed by a milestone.
+ *
+ *
+ * Since there is no hard definition for when a transaction can be considered to be orphaned, we define orphaned in
+ * relation to a referenceTransaction. If the transaction or any of its direct or indirect approvers saw a
+ * transaction being attached to it, that arrived after our reference transaction, we consider it "not orphaned".
+ *
+ *
+ * Since we currently use milestones as reference transactions that are sufficiently old, this definition in fact is
+ * a relatively safe way to determine if a subtangle "above" a transaction got orphaned.
+ *
+ *
+ * @param tangle Tangle object which acts as a database interface
+ * @param transaction transaction that shall be checked
+ * @param referenceTransaction transaction that acts as a judge to the other transaction
+ * @param processedTransactions transactions that were visited already while trying to determine the orphaned status
+ * @return true if the transaction got orphaned and false otherwise
+ * @throws SnapshotException if anything goes wrong while determining the orphaned status
+ */
+ private boolean isProbablyOrphaned(Tangle tangle, TransactionViewModel transaction,
+ TransactionViewModel referenceTransaction, Set processedTransactions) throws SnapshotException {
+
+ AtomicBoolean nonOrphanedTransactionFound = new AtomicBoolean(false);
+ try {
+ DAGHelper.get(tangle).traverseApprovers(
+ transaction.getHash(),
+ currentTransaction -> !nonOrphanedTransactionFound.get(),
+ currentTransaction -> {
+ if (currentTransaction.getArrivalTime() / 1000L > referenceTransaction.getTimestamp()) {
+ nonOrphanedTransactionFound.set(true);
+ }
+ },
+ processedTransactions
+ );
+ } catch (TraversalException e) {
+ throw new SnapshotException("failed to determine orphaned status of " + transaction, e);
+ }
+
+ return !nonOrphanedTransactionFound.get();
+ }
+
+ /**
+ *
+ * We determine whether future milestones will approve {@param transactionHash}. This should aid in determining
+ * solid entry points.
+ *
+ *
+ * To check if the transaction has non-orphaned approvers we first check if any of its approvers got confirmed by a
+ * future milestone, since this is very cheap. If none of them got confirmed by another milestone we do the more
+ * expensive check from {@link #isProbablyOrphaned(Tangle, TransactionViewModel, TransactionViewModel, Set)}.
+ *
+ *
+ * Since solid entry points have a limited life time and to prevent potential problems due to temporary errors in
+ * the database, we assume that the checked transaction is not orphaned if any error occurs while determining its
+ * status, thus adding solid entry points. This is a storage <=> reliability trade off, since the only bad effect of
+ * having too many solid entry points) is a bigger snapshot file.
+ *
+ *
+ * @param tangle Tangle object which acts as a database interface
+ * @param transactionHash hash of the transaction that shall be checked
+ * @param targetMilestone milestone that is used as an anchor for our checks
+ * @return true if the transaction is a solid entry point and false otherwise
+ */
+ private boolean isNotOrphaned(Tangle tangle, Hash transactionHash, MilestoneViewModel targetMilestone) {
+ Set unconfirmedApprovers = new HashSet<>();
+
+ try {
+ for (Hash approverHash : ApproveeViewModel.load(tangle, transactionHash).getHashes()) {
+ TransactionViewModel approver = TransactionViewModel.fromHash(tangle, approverHash);
+
+ if (approver.snapshotIndex() > targetMilestone.index()) {
+ return true;
+ } else if (approver.snapshotIndex() == 0) {
+ unconfirmedApprovers.add(approver);
+ }
+ }
+
+ Set processedTransactions = new HashSet<>();
+ TransactionViewModel milestoneTransaction = TransactionViewModel.fromHash(tangle, targetMilestone.getHash());
+ for (TransactionViewModel unconfirmedApprover : unconfirmedApprovers) {
+ if (!isProbablyOrphaned(tangle, unconfirmedApprover, milestoneTransaction, processedTransactions)) {
+ return true;
+ }
+ }
+ } catch (Exception e) {
+ log.error("failed to determine the solid entry point status for transaction " + transactionHash, e);
+
+ return true;
+ }
+
+ return false;
}
/**
@@ -528,7 +646,9 @@ private void processOldSolidEntryPoints(Tangle tangle, SnapshotProvider snapshot
for (Map.Entry solidPoint : orgSolidEntryPoints.entrySet()) {
Hash hash = solidPoint.getKey();
int milestoneIndex = solidPoint.getValue();
- if (!Hash.NULL_HASH.equals(hash) && isAboveMinMilestone(milestoneIndex, targetMilestone.index())) {
+ if (!Hash.NULL_HASH.equals(hash)
+ && targetMilestone.index() - milestoneIndex <= SOLID_ENTRY_POINT_LIFETIME
+ && isNotOrphaned(tangle, hash, targetMilestone)) {
TransactionViewModel tvm = TransactionViewModel.fromHash(tangle, hash);
addTailsToSolidEntryPoints(milestoneIndex, solidEntryPoints, tvm);
solidEntryPoints.put(hash, milestoneIndex);
@@ -549,136 +669,62 @@ private void processOldSolidEntryPoints(Tangle tangle, SnapshotProvider snapshot
* This method retrieves the new solid entry points of the snapshot reference given by the target milestone.
*
*
- * A transaction is considered a solid entry point if it is a
- * bundle tail of a non-orphaned transaction that was approved by a milestone that is above the target milestone.
+ * A transaction is considered a solid entry point if it is a bundle tail that can be traversed down from a
+ * non-orphaned transaction that was approved by a milestone that is above the last local snapshot. Or if it is a
+ * bundle tail of a non-orphaned transaction that was approved by a milestone that is above the last local snapshot.
*
- * It iterates over all relevant unprocessed milestones and analyzes their directly and indirectly approved transactions.
- * Every transaction is checked for being SEP and added to {@param SolidEntryPoints} when required
+ * It iterates over all unprocessed milestones and analyzes their directly and indirectly approved transactions.
+ * Every transaction is checked for being not orphaned and the appropriate SEP is added to {@param SolidEntryPoints}
*
*
+ *
* @param tangle Tangle object which acts as a database interface
* @param snapshotProvider data provider for the {@link Snapshot}s that are relevant for the node
* @param targetMilestone milestone that is used to generate the solid entry points
* @param solidEntryPoints map that is used to collect the solid entry points
* @throws SnapshotException if anything goes wrong while determining the solid entry points
- * @see #getSolidEntryPoints(int, ProgressLogger)
+ * @see #isNotOrphaned(Tangle, Hash, MilestoneViewModel)
*/
private void processNewSolidEntryPoints(Tangle tangle, SnapshotProvider snapshotProvider,
MilestoneViewModel targetMilestone, Map solidEntryPoints) throws SnapshotException {
ProgressLogger progressLogger = new IntervalProgressLogger(
"Taking local snapshot [generating solid entry points]", log);
+
try {
- solidEntryPoints.putAll(getSolidEntryPoints(targetMilestone.index(), progressLogger));
+ progressLogger.start(Math.min(targetMilestone.index() - snapshotProvider.getInitialSnapshot().getIndex(),
+ OUTER_SHELL_SIZE));
+
+ MilestoneViewModel nextMilestone = targetMilestone;
+ while (nextMilestone != null && nextMilestone.index() > snapshotProvider.getInitialSnapshot().getIndex() &&
+ progressLogger.getCurrentStep() < progressLogger.getStepCount()) {
+
+ MilestoneViewModel currentMilestone = nextMilestone;
+ DAGHelper.get(tangle).traverseApprovees(
+ currentMilestone.getHash(),
+ currentTransaction -> currentTransaction.snapshotIndex() >= currentMilestone.index(),
+ currentTransaction -> {
+ if (isNotOrphaned(tangle, currentTransaction.getHash(), targetMilestone)) {
+ addTailsToSolidEntryPoints(targetMilestone.index(), solidEntryPoints,
+ currentTransaction);
+ }
+ }
+ );
+
+ solidEntryPoints.put(currentMilestone.getHash(), targetMilestone.index());
+
+ nextMilestone = MilestoneViewModel.findClosestPrevMilestone(tangle, currentMilestone.index(),
+ snapshotProvider.getInitialSnapshot().getIndex());
+
+ progressLogger.progress();
+ }
+
progressLogger.finish();
} catch (Exception e) {
progressLogger.abort(e);
- throw new SnapshotException("could not generate the solid entry points for " + targetMilestone, e);
- }
- }
-
- /**
- * Generates entrypoints based on target index down to the maximum depth of the node
- *
- * @param targetIndex The milestone index we target to generate entrypoints until.
- * @param progressLogger The logger we use to write progress of entrypoint generation
- * @return a map of entrypoints or null
when we were interrupted
- * @throws Exception When we fail to get entry points due to errors generally caused by db interaction
- */
- private Map getSolidEntryPoints(int targetIndex, ProgressLogger progressLogger) throws Exception {
- Map solidEntryPoints = new HashMap<>();
- solidEntryPoints.put(Hash.NULL_HASH, targetIndex);
- log.info("Generating entrypoints for {}", targetIndex);
-
- int sepDepth = getSepDepth();
- // Co back a but below the milestone. Limited to maxDepth or genisis
- int startIndex = Math.max(snapshotProvider.getInitialSnapshot().getIndex(), targetIndex - sepDepth
- ) + 1; // cant start at last snapshot now can we, could be 0!
-
- progressLogger.start(startIndex);
-
- // Iterate from a reasonable old milestone to the target index to check for solid entry points
- for (int milestoneIndex = startIndex; milestoneIndex <= targetIndex; milestoneIndex++) {
- if (Thread.currentThread().isInterrupted()) {
- throw new InterruptedException();
- }
-
- MilestoneViewModel milestone = MilestoneViewModel.get(tangle, milestoneIndex);
- if (milestone == null) {
- log.warn("Failed to find milestone {} during entry point analyzation", milestoneIndex);
- return null;
- }
-
- List approvees = getMilestoneApprovees(milestoneIndex, milestone);
- for (Hash approvee : approvees) {
- if (Thread.currentThread().isInterrupted()) {
- throw new InterruptedException();
- }
-
- if (isSolidEntryPoint(approvee, targetIndex)) {
- // A solid entry point should only be a tail transaction, otherwise the whole bundle can't be reproduced with a snapshot file
- TransactionViewModel tvm = TransactionViewModel.fromHash(tangle, approvee);
- addTailsToSolidEntryPoints(milestoneIndex, solidEntryPoints, tvm);
- }
- }
- progressLogger.progress();
- }
-
- return solidEntryPoints;
- }
- /**
- * Calculates minimum solid entrypoint depth based on network and maxDepth
- *
- * @return The amount of ms we go back under target snapshot for generation of solid entrypoints
- */
- private int getSepDepth() {
- return config.isTestnet() ? config.getMaxDepth() : Math.min(MIN_LS_DEPTH_MAINNET, config.getMaxDepth());
- }
-
- /**
- * isSolidEntryPoint checks whether any direct approver of the given transaction was confirmed
- * by a milestone which is above the target milestone.
- *
- * @param txHash The hash we check as an entrypoint
- * @param targetIndex
- * @return if the transaction is considered a solid entrypoint
- * @throws Exception on db error
- */
- private boolean isSolidEntryPoint(Hash txHash, int targetIndex) throws Exception {
- ApproveeViewModel approvers = ApproveeViewModel.load(tangle, txHash);
- if (approvers.getHashes().isEmpty()) {
- return false;
- }
-
- for (Hash approver : approvers.getHashes()) {
- TransactionViewModel tvm = TransactionViewModel.fromHash(tangle, approver);
- if (tvm != null && tvm.snapshotIndex() > targetIndex) {
- // confirmed by a later milestone than targetIndex => solidEntryPoint
- return true;
- }
+ throw new SnapshotException("could not generate the solid entry points for " + targetMilestone, e);
}
-
- return false;
- }
-
- /**
- * getMilestoneApprovees traverses a milestone and collects all tx that were
- * confirmed by that milestone or higher
- *
- * @param milestoneIndex
- * @param milestone
- * @return
- * @throws TraversalException
- */
- private List getMilestoneApprovees(int milestoneIndex, MilestoneViewModel milestone) throws TraversalException {
- List approvees = new LinkedList<>();
- DAGHelper.get(tangle).traverseApprovees(milestone.getHash(),
- currentTransaction -> currentTransaction.snapshotIndex() == milestoneIndex,
- currentTransaction -> {
- approvees.add(currentTransaction.getHash());
- });
- return approvees;
}
private void addTailsToSolidEntryPoints(int milestoneIndex, Map solidEntryPoints,