Skip to content
This repository was archived by the owner on Aug 23, 2020. It is now read-only.

MilestoneTracker and LedgerValidator rework #1151

38 changes: 38 additions & 0 deletions src/main/java/com/iota/iri/service/ledger/LedgerException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.iota.iri.service.ledger;

/**
* This class is used to wrap exceptions that are specific to the ledger logic.
*
* It allows us to distinct between the different kinds of errors that can happen during the execution of the code.
*/
public class LedgerException extends Exception {
/**
* Constructor of the exception which allows us to provide a specific error message and the cause of the error.
*
* @param message reason why this error occurred
* @param cause wrapped exception that caused this error
*/
public LedgerException(String message, Throwable cause) {
super(message, cause);
}

/**
* Constructor of the exception which allows us to provide a specific error message without having an underlying
* cause.
*
* @param message reason why this error occurred
*/
public LedgerException(String message) {
super(message);
}

/**
* Constructor of the exception which allows us to wrap the underlying cause of the error without providing a
* specific reason.
*
* @param cause wrapped exception that caused this error
*/
public LedgerException(Throwable cause) {
super(cause);
}
}
80 changes: 80 additions & 0 deletions src/main/java/com/iota/iri/service/ledger/LedgerService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.iota.iri.service.ledger;

import com.iota.iri.controllers.MilestoneViewModel;
import com.iota.iri.model.Hash;

import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* Represents the service that contains all the relevant business logic for modifying and calculating the ledger
* state.<br />
* <br />
* This class is stateless and does not hold any domain specific models.<br />
*/
public interface LedgerService {
/**
* This method applies the given milestone to the ledger state.<br />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* This method applies the given milestone to the ledger state.<br />
* Applies the given milestone to the ledger state.<br />

* <br />
* It first marks the transactions that were confirmed by this milestones as confirmed by setting their
* corresponding {@code snapshotIndex} value. Then it generates the {@link com.iota.iri.model.StateDiff} that
* reflects the accumulated balance changes of all these transactions and applies it to the latest Snapshot.<br />
*
* @param milestone the milestone that shall be applied
* @return {@code true} if the milestone could be applied to the ledger and {@code false} otherwise
* @throws LedgerException if anything goes wrong while modifying the ledger state
*/
boolean applyMilestoneToLedger(MilestoneViewModel milestone) throws LedgerException;

/**
* This method checks the consistency of the combined balance changes of the given tips.<br />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* This method checks the consistency of the combined balance changes of the given tips.<br />
* Checks the consistency of the combined balance changes of the given tips.<br />

* <br />
* It simply calculates the balance changes of the tips and then combines them to verify that they are leading to a
* consistent ledger state (which means that they are not containing any double-spends or spends of non-existent
* IOTA).<br />
*
* @param hashes a list of hashes that reference the chosen tips
* @return {@code true} if the tips are consistent and {@code false} otherwise
* @throws LedgerException if anything unexpected happens while checking the consistency of the tips
*/
boolean tipsConsistent(List<Hash> hashes) throws LedgerException;

/**
* This method checks if the balance changes of the transactions that are referenced by the given tip are
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* This method checks if the balance changes of the transactions that are referenced by the given tip are
* Checks if the balance changes of the transactions that are referenced by the given tip are

* consistent.<br />
* <br />
* It first calculates the balance changes, then adds them to the given {@code diff} and finally checks their
* consistency. If we are only interested in the changes that are referenced by the given {@code tip} we need to
* pass in an empty map for the {@code diff} parameter.<br />
* <br />
* The {@code diff} as well as the {@code approvedHashes} parameters are modified, so they will contain the new
* balance changes and the approved transactions after this method terminates.<br />
*
* @param approvedHashes a set of transaction hashes that shall be considered to be approved already (and that
* consequently shall be excluded from the calculation)
* @param diff a map of balances associated to their address that shall be used as a basis for the balance
* @param tip the tip that will have its approvees checked
* @return {@code true} if the balance changes are consistent and {@code false} otherwise
* @throws LedgerException if anything unexpected happens while determining the consistency
*/
boolean isBalanceDiffConsistent(Set<Hash> approvedHashes, Map<Hash, Long> diff, Hash tip) throws LedgerException;

/**
* This method generates the accumulated balance changes of the transactions that are "approved" by the given
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* This method generates the accumulated balance changes of the transactions that are "approved" by the given
* Generates the accumulated balance changes of the transactions that are "approved" by the given

* milestone.<br />
* <br />
* It simply iterates over all approvees that still belong to the given milestone and that have not been
* processed already (by being part of the {@code visitedNonMilestoneSubtangleHashes} set) and collects their
* balance changes.<br />
*
* @param visitedTransactions a set of transaction hashes that shall be considered to be visited already
* @param milestoneHash the milestone that is examined regarding its approved transactions
* @param milestoneIndex the milestone index
* @return a map of the balance changes (addresses associated to their balance) or {@code null} if the balance could
* not be generated due to inconsistencies
* @throws LedgerException if anything unexpected happens while generating the balance changes
*/
Map<Hash, Long> generateBalanceDiff(Set<Hash> visitedTransactions, Hash milestoneHash, int milestoneIndex) throws
LedgerException;
}
258 changes: 258 additions & 0 deletions src/main/java/com/iota/iri/service/ledger/impl/LedgerServiceImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
package com.iota.iri.service.ledger.impl;

import com.iota.iri.BundleValidator;
import com.iota.iri.controllers.MilestoneViewModel;
import com.iota.iri.controllers.StateDiffViewModel;
import com.iota.iri.controllers.TransactionViewModel;
import com.iota.iri.model.Hash;
import com.iota.iri.service.ledger.LedgerException;
import com.iota.iri.service.ledger.LedgerService;
import com.iota.iri.service.milestone.MilestoneService;
import com.iota.iri.service.snapshot.SnapshotException;
import com.iota.iri.service.snapshot.SnapshotProvider;
import com.iota.iri.service.snapshot.SnapshotService;
import com.iota.iri.service.snapshot.impl.SnapshotStateDiffImpl;
import com.iota.iri.storage.Tangle;

import java.util.*;

/**
* Represents the service that contains all the relevant business logic for modifying and calculating the ledger
* state.<br />
* <br />
* This class is stateless and does not hold any domain specific models.<br />
*/
public class LedgerServiceImpl implements LedgerService {
/**
* Holds the tangle object which acts as a database interface.<br />
*/
private final Tangle tangle;

/**
* Holds the snapshot provider which gives us access to the relevant snapshots.<br />
*/
private final SnapshotProvider snapshotProvider;

/**
* Holds a reference to the service instance containing the business logic of the snapshot package.<br />
*/
private final SnapshotService snapshotService;

/**
* Holds a reference to the service instance containing the business logic of the milestone package.<br />
*/
private final MilestoneService milestoneService;

/**
* Creates a service instance that allows us to perform ledger state specific operations.<br />
* <br />
* It simply stores the passed in dependencies in the internal properties.<br />
*
* @param tangle Tangle object which acts as a database interface
* @param snapshotProvider snapshot provider which gives us access to the relevant snapshots
* @param snapshotService service instance of the snapshot package that allows us to rollback ledger states
* @param milestoneService contains the important business logic when dealing with milestones
*/
public LedgerServiceImpl(Tangle tangle, SnapshotProvider snapshotProvider, SnapshotService snapshotService,
MilestoneService milestoneService) {

this.tangle = tangle;
this.snapshotProvider = snapshotProvider;
this.snapshotService = snapshotService;
this.milestoneService = milestoneService;
}

@Override
public boolean applyMilestoneToLedger(MilestoneViewModel milestone) throws LedgerException {
if(generateStateDiff(milestone)) {
try {
snapshotService.replayMilestones(snapshotProvider.getLatestSnapshot(), milestone.index());
} catch (SnapshotException e) {
throw new LedgerException("failed to apply the balance changes to the ledger state", e);
}

return true;
}

return false;
}

@Override
public boolean tipsConsistent(List<Hash> tips) throws LedgerException {
Set<Hash> visitedHashes = new HashSet<>();
Map<Hash, Long> diff = new HashMap<>();
for (Hash tip : tips) {
if (!isBalanceDiffConsistent(visitedHashes, diff, tip)) {
return false;
}
}

return true;
}

@Override
public boolean isBalanceDiffConsistent(Set<Hash> approvedHashes, Map<Hash, Long> diff, Hash tip) throws
LedgerException {

try {
if (!TransactionViewModel.fromHash(tangle, tip).isSolid()) {
return false;
}
} catch (Exception e) {
throw new LedgerException("failed to check the consistency of the balance changes", e);
}

if (approvedHashes.contains(tip)) {
return true;
}
Set<Hash> visitedHashes = new HashSet<>(approvedHashes);
Map<Hash, Long> currentState = generateBalanceDiff(visitedHashes, tip,
snapshotProvider.getLatestSnapshot().getIndex());
if (currentState == null) {
return false;
}
diff.forEach((key, value) -> {
if (currentState.computeIfPresent(key, ((hash, aLong) -> value + aLong)) == null) {
currentState.putIfAbsent(key, value);
}
});
boolean isConsistent = snapshotProvider.getLatestSnapshot().patchedState(new SnapshotStateDiffImpl(
currentState)).isConsistent();
if (isConsistent) {
diff.putAll(currentState);
approvedHashes.addAll(visitedHashes);
}
return isConsistent;
}

@Override
public Map<Hash, Long> generateBalanceDiff(Set<Hash> visitedTransactions, Hash milestoneHash, int milestoneIndex)
throws LedgerException {

Map<Hash, Long> state = new HashMap<>();
Set<Hash> countedTx = new HashSet<>();

snapshotProvider.getInitialSnapshot().getSolidEntryPoints().keySet().forEach(solidEntryPointHash -> {
visitedTransactions.add(solidEntryPointHash);
countedTx.add(solidEntryPointHash);
});

final Queue<Hash> nonAnalyzedTransactions = new LinkedList<>(Collections.singleton(milestoneHash));
Hash transactionPointer;
while ((transactionPointer = nonAnalyzedTransactions.poll()) != null) {
if (visitedTransactions.add(transactionPointer)) {

try {
final TransactionViewModel transactionViewModel = TransactionViewModel.fromHash(tangle,
transactionPointer);

if (milestoneService.transactionBelongsToMilestone(transactionViewModel, milestoneIndex)) {

if (transactionViewModel.getType() == TransactionViewModel.PREFILLED_SLOT) {
return null;
} else {
if (transactionViewModel.getCurrentIndex() == 0) {
boolean validBundle = false;

final List<List<TransactionViewModel>> bundleTransactions = BundleValidator.validate(
tangle, snapshotProvider.getInitialSnapshot(), transactionViewModel.getHash());

for (final List<TransactionViewModel> bundleTransactionViewModels : bundleTransactions) {

if (BundleValidator.isInconsistent(bundleTransactionViewModels)) {
break;
}

if (bundleTransactionViewModels.get(0).getHash().equals(transactionViewModel.getHash())) {
validBundle = true;

for (final TransactionViewModel bundleTransactionViewModel : bundleTransactionViewModels) {

if (bundleTransactionViewModel.value() != 0 && countedTx.add(bundleTransactionViewModel.getHash())) {

final Hash address = bundleTransactionViewModel.getAddressHash();
final Long value = state.get(address);
state.put(address, value == null ? bundleTransactionViewModel.value()
: Math.addExact(value, bundleTransactionViewModel.value()));
}
}

break;
}
}
if (!validBundle) {
return null;
}
}

nonAnalyzedTransactions.offer(transactionViewModel.getTrunkTransactionHash());
nonAnalyzedTransactions.offer(transactionViewModel.getBranchTransactionHash());
}
}
} catch (Exception e) {
throw new LedgerException("unexpected error while generating the balance diff", e);
}
}
}

return state;
}

/**
* This method generates the {@link com.iota.iri.model.StateDiff} that belongs to the given milestone in the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* This method generates the {@link com.iota.iri.model.StateDiff} that belongs to the given milestone in the
* Generates the {@link com.iota.iri.model.StateDiff} that belongs to the given milestone in the

* database and marks all transactions that have been approved by the milestone accordingly by setting their
* {@code snapshotIndex} value.<br />
* <br />
* It first checks if the {@code snapshotIndex} of the transaction belonging to the milestone was correctly set
* already (to determine if this milestone was processed already) and proceeds to generate the {@link
* com.iota.iri.model.StateDiff} if that is not the case. To do so, it calculates the balance changes, checks if
* they are consistent and only then writes them to the database.<br />
* <br />
* If inconsistencies in the {@code snapshotIndex} are found it issues a reset of the corresponding milestone to
* recover from this problem.<br />
*
* @param milestone the milestone that shall have its {@link com.iota.iri.model.StateDiff} generated
* @return {@code true} if the {@link com.iota.iri.model.StateDiff} could be generated and {@code false} otherwise
* @throws LedgerException if anything unexpected happens while generating the {@link com.iota.iri.model.StateDiff}
*/
private boolean generateStateDiff(MilestoneViewModel milestone) throws LedgerException {

try {
TransactionViewModel transactionViewModel = TransactionViewModel.fromHash(tangle, milestone.getHash());

if (!transactionViewModel.isSolid()) {
return false;
}

final int transactionSnapshotIndex = transactionViewModel.snapshotIndex();
boolean successfullyProcessed = transactionSnapshotIndex == milestone.index();
if (!successfullyProcessed) {
snapshotProvider.getLatestSnapshot().lockRead();
try {
Hash tail = transactionViewModel.getHash();
Map<Hash, Long> balanceChanges = generateBalanceDiff(new HashSet<>(), tail,
snapshotProvider.getLatestSnapshot().getIndex());
successfullyProcessed = balanceChanges != null;
if (successfullyProcessed) {
successfullyProcessed = snapshotProvider.getLatestSnapshot().patchedState(
new SnapshotStateDiffImpl(balanceChanges)).isConsistent();
if (successfullyProcessed) {
milestoneService.updateMilestoneIndexOfMilestoneTransactions(milestone.getHash(),
milestone.index());

if (!balanceChanges.isEmpty()) {
new StateDiffViewModel(balanceChanges, milestone.getHash()).store(tangle);
}
}
}
} finally {
snapshotProvider.getLatestSnapshot().unlockRead();
}
}

return successfullyProcessed;
} catch (Exception e) {
throw new LedgerException("unexpected error while generating the StateDiff for " + milestone, e);
}
}
}
Loading