diff --git a/src/main/java/com/iota/iri/Iota.java b/src/main/java/com/iota/iri/Iota.java index e61ddb2b4b..d3b4774715 100644 --- a/src/main/java/com/iota/iri/Iota.java +++ b/src/main/java/com/iota/iri/Iota.java @@ -3,6 +3,10 @@ import java.util.List; import java.util.Map; +import com.iota.iri.storage.rocksDB.RocksDBPPPImpl; +import org.apache.commons.lang3.NotImplementedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.iota.iri.storage.Tangle; import com.iota.iri.storage.PersistenceProvider; import com.iota.iri.storage.LocalSnapshotsPersistenceProvider; @@ -10,10 +14,6 @@ import com.iota.iri.storage.Indexable; import com.iota.iri.storage.Persistable; -import org.apache.commons.lang3.NotImplementedException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.iota.iri.conf.IotaConfig; import com.iota.iri.controllers.TipsViewModel; import com.iota.iri.controllers.TransactionViewModel; @@ -287,6 +287,24 @@ private void initializeTangle() { if (configuration.isZmqEnabled()) { tangle.addMessageQueueProvider(new ZmqMessageQueueProvider(configuration)); } + + if(configuration.isSelectivePermaEnabled()){ + switch (configuration.getPermaMainDb()) { + case "rocksdb": { + RocksDBPPPImpl ppp = new RocksDBPPPImpl( + configuration.getPermaDbPath(), + configuration.getPermaDbLogPath(), + configuration.getPermaDbCacheSize()); + + tangle.addPersistenceProvider(ppp); + tangle.addPermanentPersistenceProvider(ppp); + break; + } + default: { + throw new NotImplementedException("No such database type."); + } + } + } } /** diff --git a/src/main/java/com/iota/iri/conf/BaseIotaConfig.java b/src/main/java/com/iota/iri/conf/BaseIotaConfig.java index b9ab7c517a..6dd9d6be04 100644 --- a/src/main/java/com/iota/iri/conf/BaseIotaConfig.java +++ b/src/main/java/com/iota/iri/conf/BaseIotaConfig.java @@ -68,6 +68,15 @@ public abstract class BaseIotaConfig implements IotaConfig { protected boolean revalidate = Defaults.REVALIDATE; protected boolean rescanDb = Defaults.RESCAN_DB; + //PERMADB + protected boolean permaDbEnabled = Defaults.PERMADB_ENABLED; + protected String permaDbPath = Defaults.PERMADB_PATH; + protected String permaDbLogPath = Defaults.PERMADB_LOG_PATH; + protected int permaDbCacheSize = Defaults.PERMADB_CACHE_SIZE; //KB + protected String permaMainDb = Defaults.PERMAMAIN_DB; + protected boolean permaRevalidate = Defaults.PERMAREVALIDATE; + protected boolean permaRescanDb = Defaults.PERMARESCAN_DB; + //Protocol protected double pSendMilestone = Defaults.P_SEND_MILESTONE; @@ -383,6 +392,83 @@ protected void setIxiDir(String ixiDir) { this.ixiDir = ixiDir; } + //------- PERMADB ------------- + @Override + public boolean isSelectivePermaEnabled() { return this.permaDbEnabled;} + + @JsonProperty + @Parameter(names = {"--permadb-enabled"}, description = PermaDBConfig.Descriptions.PERMADB_ENABLED) + protected void setPermaDbEnabled(boolean enabled) { + this.permaDbEnabled = enabled; + } + + @Override + public String getPermaDbPath() { + return permaDbPath; + } + + @JsonProperty + @Parameter(names = {"--perma-db-path"}, description = PermaDBConfig.Descriptions.PERMADB_PATH) + protected void setPermaDbPath(String permaDbPath) { + this.permaDbPath = permaDbPath; + } + + @Override + public String getPermaDbLogPath() { + return dbLogPath; + } + + @JsonProperty + @Parameter(names = {"--perma-db-log-path"}, description = PermaDBConfig.Descriptions.PERMADB_LOG_PATH) + protected void setPermaDbLogPath(String permaDbLogPath) { + this.permaDbLogPath = permaDbLogPath; + } + + @Override + public int getPermaDbCacheSize() { + return permaDbCacheSize; + } + + @JsonProperty + @Parameter(names = {"--perma-db-cache-size"}, description = PermaDBConfig.Descriptions.PERMADB_CACHE_SIZE) + protected void setPermaDbCacheSize(int permaDbCacheSize) { + this.permaDbCacheSize = permaDbCacheSize; + } + + @Override + public String getPermaMainDb() { + return permaMainDb; + } + + @JsonProperty + @Parameter(names = {"--perma-db"}, description = PermaDBConfig.Descriptions.PERMAMAIN_DB) + protected void setPermaMainDb(String permaMainDb) { + this.permaMainDb = permaMainDb; + } + + @Override + public boolean permaIsRevalidate() { + return permaRevalidate; + } + + @JsonProperty + @Parameter(names = {"--perma-revalidate"}, description = PermaDBConfig.Descriptions.PERMAREVALIDATE) + protected void setPermaRevalidate(boolean permaRevalidate) { + this.permaRevalidate = permaRevalidate; + } + + @Override + public boolean permaIsRescanDb() { + return this.permaRescanDb; + } + + @JsonProperty + @Parameter(names = {"--perma-rescan"}, description = PermaDBConfig.Descriptions.PERMARESCAN_DB) + protected void setPermaRescanDb(boolean permaRescanDb) { + this.permaRescanDb = permaRescanDb; + } + //----------------------------- + @Override public String getDbPath() { return dbPath; @@ -868,6 +954,15 @@ public interface Defaults { boolean REVALIDATE = false; boolean RESCAN_DB = false; + //PERMADB + boolean PERMADB_ENABLED = true; + String PERMADB_PATH = "mainnetpermadb"; + String PERMADB_LOG_PATH = "mainnetpermadb.log"; + int PERMADB_CACHE_SIZE = 100_000; + String PERMAMAIN_DB = "rocksdb"; + boolean PERMAREVALIDATE = false; + boolean PERMARESCAN_DB = false; + //Protocol double P_SEND_MILESTONE = 0.02d; int MWM = 14; diff --git a/src/main/java/com/iota/iri/conf/IotaConfig.java b/src/main/java/com/iota/iri/conf/IotaConfig.java index a6e0923f64..2045e46c41 100644 --- a/src/main/java/com/iota/iri/conf/IotaConfig.java +++ b/src/main/java/com/iota/iri/conf/IotaConfig.java @@ -10,7 +10,8 @@ * In charge of how we parse the configuration from given inputs. */ public interface IotaConfig extends APIConfig, NodeConfig, - IXIConfig, DbConfig, ConsensusConfig, ZMQConfig, TipSelConfig, PearlDiverConfig, SolidificationConfig { + IXIConfig, DbConfig, PermaDBConfig, + ConsensusConfig, ZMQConfig, TipSelConfig, PearlDiverConfig, SolidificationConfig { File CONFIG_FILE = new File("iota.ini"); /** diff --git a/src/main/java/com/iota/iri/conf/PermaDBConfig.java b/src/main/java/com/iota/iri/conf/PermaDBConfig.java new file mode 100644 index 0000000000..dea784c055 --- /dev/null +++ b/src/main/java/com/iota/iri/conf/PermaDBConfig.java @@ -0,0 +1,72 @@ +package com.iota.iri.conf; + +/** + * Configurations for tangle database. + */ +public interface PermaDBConfig extends Config { + + + /** + * Default Value: {@value BaseIotaConfig.Defaults#PERMADB_ENABLED} + * + * @return {@value PermaDBConfig.Descriptions#PERMADB_ENABLED} + */ + boolean isSelectivePermaEnabled(); + + /** + * Default Value: {@value BaseIotaConfig.Defaults#PERMADB_PATH} + * + * @return {@value PermaDBConfig.Descriptions#PERMADB_PATH} + */ + String getPermaDbPath(); + + /** + * Default Value: {@value BaseIotaConfig.Defaults#PERMADB_LOG_PATH} + * + * @return {@value PermaDBConfig.Descriptions#PERMADB_LOG_PATH} + */ + String getPermaDbLogPath(); + + /** + * Default Value: {@value BaseIotaConfig.Defaults#PERMADB_CACHE_SIZE} + * + * @return {@value PermaDBConfig.Descriptions#PERMADB_CACHE_SIZE} + */ + int getPermaDbCacheSize(); + + /** + * Default Value: {@value BaseIotaConfig.Defaults#PERMAMAIN_DB} + * + * @return {@value PermaDBConfig.Descriptions#PERMAMAIN_DB} + */ + String getPermaMainDb(); + + /** + * Default Value: {@value BaseIotaConfig.Defaults#PERMAREVALIDATE} + * + * @return {@value PermaDBConfig.Descriptions#PERMAREVALIDATE} + */ + boolean permaIsRevalidate(); + + /** + * Default Value: {@value BaseIotaConfig.Defaults#PERMARESCAN_DB} + * + * @return {@value PermaDBConfig.Descriptions#PERMARESCAN_DB} + */ + boolean permaIsRescanDb(); + + /** + * Field descriptions + */ + interface Descriptions { + + String PERMADB_PATH = "The folder where the DB saves its data."; + String PERMADB_LOG_PATH = "The folder where the DB logs info"; + String PERMADB_CACHE_SIZE = "The size of the DB cache in KB"; + String PERMAMAIN_DB = "The DB engine used to store the transactions. Currently only RocksDB is supported."; + String PERMAREVALIDATE = "Reload from the db data about confirmed transaction (milestones), state of the ledger, " + + "and transaction metadata."; + String PERMARESCAN_DB = "Rescan all transaction metadata (Approvees, Bundles, and Tags)"; + String PERMADB_ENABLED = "Enables secondary permanent storage"; + } +} diff --git a/src/main/java/com/iota/iri/model/persistables/Hashes.java b/src/main/java/com/iota/iri/model/persistables/Hashes.java index 30da9560bb..67c777f77b 100644 --- a/src/main/java/com/iota/iri/model/persistables/Hashes.java +++ b/src/main/java/com/iota/iri/model/persistables/Hashes.java @@ -62,7 +62,6 @@ public void readMetadata(byte[] bytes) { } - @Override public boolean canMerge() { return true; diff --git a/src/main/java/com/iota/iri/model/persistables/Milestone.java b/src/main/java/com/iota/iri/model/persistables/Milestone.java index 561aa8bf37..3de3ed92e2 100644 --- a/src/main/java/com/iota/iri/model/persistables/Milestone.java +++ b/src/main/java/com/iota/iri/model/persistables/Milestone.java @@ -55,7 +55,6 @@ public void readMetadata(byte[] bytes) { } - @Override public boolean canMerge() { return false; diff --git a/src/main/java/com/iota/iri/model/persistables/SpentAddress.java b/src/main/java/com/iota/iri/model/persistables/SpentAddress.java index f501e8f8a8..e6798f6fda 100644 --- a/src/main/java/com/iota/iri/model/persistables/SpentAddress.java +++ b/src/main/java/com/iota/iri/model/persistables/SpentAddress.java @@ -31,15 +31,14 @@ public boolean canMerge() { return false; } - @Override public Persistable mergeInto(Persistable source) throws OperationNotSupportedException { throw new OperationNotSupportedException("This object is not mergeable"); } - @Override public boolean exists() { return exists; } + } diff --git a/src/main/java/com/iota/iri/model/persistables/Transaction.java b/src/main/java/com/iota/iri/model/persistables/Transaction.java index a13e6a2e0a..b65e0bc01c 100644 --- a/src/main/java/com/iota/iri/model/persistables/Transaction.java +++ b/src/main/java/com/iota/iri/model/persistables/Transaction.java @@ -217,8 +217,6 @@ public void readMetadata(byte[] bytes) { parsed = true; } - - @Override public boolean canMerge() { return false; diff --git a/src/main/java/com/iota/iri/service/API.java b/src/main/java/com/iota/iri/service/API.java index 66990cd933..045c645687 100644 --- a/src/main/java/com/iota/iri/service/API.java +++ b/src/main/java/com/iota/iri/service/API.java @@ -196,6 +196,13 @@ public API(IotaConfig configuration, IXI ixi, TransactionRequester transactionRe commandRoute.put(ApiCommand.GET_MISSING_TRANSACTIONS, getMissingTransactions()); commandRoute.put(ApiCommand.CHECK_CONSISTENCY, checkConsistency()); commandRoute.put(ApiCommand.WERE_ADDRESSES_SPENT_FROM, wereAddressesSpentFrom()); + + commandRoute.put(ApiCommand.PIN_TRANSACTION_HASHES, pinTransactionHashes()); + commandRoute.put(ApiCommand.PIN_TRANSACTIONS_TRYTES, pinTransactionTrytes()); + commandRoute.put(ApiCommand.IS_PINNED_TRANSACTIONS_COUNT, isPinned()); + commandRoute.put(ApiCommand.UNPIN_TRANSACTIONS, unpinTransactionHashes()); + + } /** @@ -315,6 +322,70 @@ private AbstractResponse wereAddressesSpentFromStatement(List addresses) return WereAddressesSpentFrom.create(states); } + // ----------- permanent storage -------- + + /** + * Pins transactions based on transaction trytes. It will store the transaction trytes in permanent storage. + * @param trytes list of transaction trytes. + * @return list of booleans if it the pinning was successful or not. + * @throws Exception + */ + @Document(name="pinTransactionTrytes") + public AbstractResponse pinTransactionTrytesStatement(List trytes) throws Exception { + final List elements = convertTrytes(trytes); + boolean[] result = new boolean[elements.size()]; + for(int i = 0; i < elements.size(); i++){ + result[i] = tangle.pinTransaction(elements.get(i)); + } + return BooleanValuesResponse.create(result); + } + + /** + * Pins transactions that are already in the current database. It will move those transactions to permanent storage. + * @param transactionsList + * @return List of booleans that represent of the transaction transfer was successful. + * @throws Exception + */ + @Document(name="pinTransactionHashes") + private AbstractResponse pinTransactionHashesStatement(List transactionsList) throws Exception { + final List transactions = transactionsList.stream().map(HashFactory.TRANSACTION::create).collect(Collectors.toList()); + boolean[] result = new boolean[transactions.size()]; + for(int i = 0; i < transactions.size(); i++){ + result[i] = tangle.pinTransaction(transactions.get(i)); + } + return BooleanValuesResponse.create(result); + } + + /** + * Checks if transactions are pinned + * @param transactionsList list of transaction hashes to check. + * @return List of booleans, true equals it is pinned and false for not pinned. + * @throws Exception + */ + @Document(name="isPinned") + private AbstractResponse isPinnedStatement(List transactionsList) throws Exception { + final List transactions = transactionsList.stream().map(HashFactory.TRANSACTION::create).collect(Collectors.toList()); + return BooleanValuesResponse.create(tangle.isPinned(transactions)); + } + + /** + * Unpins a transaction, it will be removed from permanent storage. + * @param transactionsList List of transaction ID's to be unpinned. + * @return An empty response with the time it took to unpin. + * @throws Exception + */ + @Document(name="unpinTransactionHashes") + private AbstractResponse unpinTransactionHashesStatement(List transactionsList) throws Exception { + for(String txHash: transactionsList){ + tangle.unpinTransaction(HashFactory.TRANSACTION.create(txHash)); + } + return AbstractResponse.createEmptyResponse(); + } + + // -------------------------------------- + + + /** * Walks back from the hash until a tail transaction has been found or transaction aprovee is not found. * A tail transaction is the first transaction in a bundle, thus with index = 0 @@ -1731,4 +1802,53 @@ private List convertTrytes(List trytes) { return elements; } + // ------ permanode ---------- + + private Function, AbstractResponse> pinTransactionTrytes() { + return request -> { + List transactionTrytes = getParameterAsList(request,"trytes", TRYTES_SIZE); + try { + return pinTransactionTrytesStatement(transactionTrytes); + } catch (Exception e) { + throw new IllegalStateException(e); + } + }; + } + + private Function, AbstractResponse> pinTransactionHashes() { + return request -> { + List txids = getParameterAsList(request,"hashes", HASH_SIZE); + try { + return pinTransactionHashesStatement(txids); + } catch (Exception e) { + throw new IllegalStateException(e); + } + }; + } + + private Function, AbstractResponse> isPinned() { + return request -> { + List txids = getParameterAsList(request,"hashes", HASH_SIZE); + try { + return isPinnedStatement(txids); + } catch (Exception e) { + throw new IllegalStateException(e); + } + }; + } + + private Function, AbstractResponse> unpinTransactionHashes() { + return request -> { + List txids = getParameterAsList(request,"hashes", HASH_SIZE); + try { + return unpinTransactionHashesStatement(txids); + } catch (Exception e) { + throw new IllegalStateException(e); + } + }; + } + + + // --------------------------- + } diff --git a/src/main/java/com/iota/iri/service/ApiCommand.java b/src/main/java/com/iota/iri/service/ApiCommand.java index 280d42be8d..776dda7bbb 100644 --- a/src/main/java/com/iota/iri/service/ApiCommand.java +++ b/src/main/java/com/iota/iri/service/ApiCommand.java @@ -81,7 +81,7 @@ public enum ApiCommand { * Stop attaching to the tangle */ INTERRUPT_ATTACHING_TO_TANGLE("interruptAttachingToTangle"), - + /** * Temporary remove a neighbor from this node */ @@ -91,11 +91,34 @@ public enum ApiCommand { * Store a transaction on this node, without broadcasting */ STORE_TRANSACTIONS("storeTransactions"), - + /** * Check if an address has been spent from */ - WERE_ADDRESSES_SPENT_FROM("wereAddressesSpentFrom"); + WERE_ADDRESSES_SPENT_FROM("wereAddressesSpentFrom"), + + + /** + * Pins a transaction from normal storage identified by their hash + */ + PIN_TRANSACTION_HASHES("pinTransactionHashes"), + + /** + * Pins a transaction based on sent trytes + */ + PIN_TRANSACTIONS_TRYTES("pinTransactionsTrytes"), + + /** + * Unpins a transaction + */ + UNPIN_TRANSACTIONS("unpinTransactionHashes"), + + /** + * Checks if a transaction is pinned or not + */ + IS_PINNED_TRANSACTIONS_COUNT("isPinned"); + + private String name; diff --git a/src/main/java/com/iota/iri/service/Feature.java b/src/main/java/com/iota/iri/service/Feature.java index 0c78d2f7d1..f90a142090 100644 --- a/src/main/java/com/iota/iri/service/Feature.java +++ b/src/main/java/com/iota/iri/service/Feature.java @@ -39,7 +39,12 @@ public enum Feature { /** * This node has the zero message queue enabled for fetching/reading "activities" on the node */ - ZMQ("zeroMessageQueue"); + ZMQ("zeroMessageQueue"), + + /** + * This node can do transaction pinning + */ + TRANSACTION_PINNING("transactionPinning"); private String name; @@ -72,16 +77,26 @@ public static Feature[] calculateFeatures(IotaConfig configuration) { if (configuration.isZmqEnabled()) { features.add(ZMQ); } + if (configuration.isSelectivePermaEnabled()) { + features.add(TRANSACTION_PINNING); + } List apiFeatures = new ArrayList(Arrays.asList(new Feature[] { PROOF_OF_WORK })); - + for (String disabled : configuration.getRemoteLimitApi()) { + switch (disabled) { - case "attachToTangle": - apiFeatures.remove(PROOF_OF_WORK); - break; + case "attachToTangle": + apiFeatures.remove(PROOF_OF_WORK); + break; + case "pinTransactionHashes": + apiFeatures.remove(TRANSACTION_PINNING); + break; + case "pinTransactionsTrytes": + apiFeatures.remove(TRANSACTION_PINNING); + break; default: break; diff --git a/src/main/java/com/iota/iri/service/dto/BooleanValuesResponse.java b/src/main/java/com/iota/iri/service/dto/BooleanValuesResponse.java new file mode 100644 index 0000000000..c3f29956f3 --- /dev/null +++ b/src/main/java/com/iota/iri/service/dto/BooleanValuesResponse.java @@ -0,0 +1,32 @@ +package com.iota.iri.service.dto; + + +/** + * Genereric boolean list response + */ +public class BooleanValuesResponse extends AbstractResponse { + /** + * List of boleans to use as an API result + */ + private boolean[] result; + + + /** + * Creates a new {@link BooleanValuesResponse} + * @param values {@link #result} + * @return an {@link BooleanValuesResponse} filled with a list of booleans + */ + public static AbstractResponse create(boolean[] values) { + BooleanValuesResponse res = new BooleanValuesResponse(); + res.result = values; + return res; + } + + /** + * + * @return list of booleans + */ + public boolean[] getResult() { + return this.result; + } +} \ No newline at end of file diff --git a/src/main/java/com/iota/iri/storage/PermanentPersistenceProvider.java b/src/main/java/com/iota/iri/storage/PermanentPersistenceProvider.java new file mode 100644 index 0000000000..d6088a2a0b --- /dev/null +++ b/src/main/java/com/iota/iri/storage/PermanentPersistenceProvider.java @@ -0,0 +1,76 @@ +package com.iota.iri.storage; + +import com.iota.iri.controllers.TransactionViewModel; +import com.iota.iri.model.Hash; +import com.iota.iri.model.persistables.Hashes; + +import java.util.List; + +/** + * Abstracts the permanent storage for transaction pinning + */ +public interface PermanentPersistenceProvider { + + + /** + * Pins a {@code model} for permanent storage. + * @param model the transaction to store. + * @param index the Hash for indexing + * @return true of succesfully pinnend + * @throws Exception + */ + boolean pinTransaction(TransactionViewModel model, Hash index) throws Exception; + + /*** + * Unpins a transaction, removing it from permanent storage + * @param index the transaction ID + * @return true of succesfully unpinnend + * @throws Exception + */ + boolean unpinTransaction(Hash index) throws Exception; + + /*** + * Checks if transactions are pinnend or not + * @param indexes + * @return List of booleans in order of the list of hashes given. True is pinned, false is not pinned + * @throws Exception + */ + boolean[] isPinned(List indexes) throws Exception; + + /*** + * Retrieves the transactions from the permanent storage + * @param index the transaction ID to retrieve + * @return The transactions + * @throws Exception + */ + TransactionViewModel getTransaction(Hash index) throws Exception; + + /*** + * Finds a list of transaction ID's related to the address hash + * @param index address hash + * @return List of found transaction ID's + * @throws Exception + */ + Hashes findAddress(Hash index) throws Exception; + /*** + * Finds a list of transaction ID's related to the bundle hash + * @param index bundle hash + * @return List of found transaction ID's + * @throws Exception + */ + Hashes findBundle(Hash index) throws Exception; + /*** + * Finds a list of transaction ID's related to the tag + * @param index tag + * @return List of found transaction ID's + * @throws Exception + */ + Hashes findTag(Hash index) throws Exception; + /*** + * Finds a list of approvee's related to the transaction ID + * @param index transaction ID + * @return List of found approvee transaction ID's + * @throws Exception + */ + Hashes findApprovee(Hash index) throws Exception; +} diff --git a/src/main/java/com/iota/iri/storage/Tangle.java b/src/main/java/com/iota/iri/storage/Tangle.java index 94956cef30..e20ab6d2f2 100644 --- a/src/main/java/com/iota/iri/storage/Tangle.java +++ b/src/main/java/com/iota/iri/storage/Tangle.java @@ -1,5 +1,6 @@ package com.iota.iri.storage; +import com.iota.iri.controllers.TransactionViewModel; import com.iota.iri.model.Hash; import com.iota.iri.model.StateDiff; import com.iota.iri.model.persistables.Address; @@ -42,12 +43,25 @@ public class Tangle { new AbstractMap.SimpleImmutableEntry<>("transaction-metadata", Transaction.class); private final List persistenceProviders = new ArrayList<>(); + private final List permanentPersistenceProviders = new ArrayList<>(); private final List messageQueueProviders = new ArrayList<>(); + /** + * Adds a persistence provider + * @param provider + */ public void addPersistenceProvider(PersistenceProvider provider) { this.persistenceProviders.add(provider); } + /** + * Adds a perment persistence provider + * @param provider + */ + public void addPermanentPersistenceProvider(PermanentPersistenceProvider provider) { + this.permanentPersistenceProviders.add(provider); + } + /** * * @see PersistenceProvider#init() @@ -369,4 +383,60 @@ public void clearMetadata(Class column) throws Exception { provider.clearMetadata(column); } } + + // ---------- Permanent storage capabilities -------- + + + /** + * @see PermanentPersistenceProvider#pinTransaction(TransactionViewModel, Hash) + */ + public boolean pinTransaction(TransactionViewModel tvm) throws Exception { + boolean success = false; + for(PermanentPersistenceProvider provider: permanentPersistenceProviders) { + success = provider.pinTransaction(tvm, tvm.getHash()) || success ; + } + return success ; + } + + /** + * Retrieves transaction from persistence providers and then calls pinTransaction(TransactionViewModel, Hash) + */ + public boolean pinTransaction(Hash hash) throws Exception { + Transaction tx = (Transaction)load(Transaction.class, hash); + if(tx.exists()) { + TransactionViewModel tvm = new TransactionViewModel(tx, hash); + return pinTransaction(tvm); + } + return false; + } + /** + * @see PermanentPersistenceProvider#unpinTransaction(Hash) + */ + public boolean unpinTransaction(Hash hash) throws Exception { + boolean success = false; + for(PermanentPersistenceProvider provider: permanentPersistenceProviders) { + success = provider.unpinTransaction(hash) || success; + } + return success; + } + /** + * @see PermanentPersistenceProvider#isPinned(List) + */ + public boolean[] isPinned(List transactionHashes) throws Exception { + boolean[] mergedResult = new boolean[transactionHashes.size()]; + for(PermanentPersistenceProvider provider: permanentPersistenceProviders) { + boolean[] providerResult = provider.isPinned(transactionHashes); + for(int i = 0; i < mergedResult.length; i++){ + mergedResult[i] = (mergedResult[i] || providerResult[i]); + } + } + + return mergedResult; + } + + + + // ------------------------------------------------- + + } diff --git a/src/main/java/com/iota/iri/storage/rocksDB/RocksDBPPPImpl.java b/src/main/java/com/iota/iri/storage/rocksDB/RocksDBPPPImpl.java new file mode 100644 index 0000000000..4de8f0d05e --- /dev/null +++ b/src/main/java/com/iota/iri/storage/rocksDB/RocksDBPPPImpl.java @@ -0,0 +1,596 @@ +package com.iota.iri.storage.rocksDB; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.primitives.Bytes; +import com.iota.iri.controllers.TransactionViewModel; +import com.iota.iri.model.Hash; +import com.iota.iri.model.persistables.Transaction; +import com.iota.iri.model.persistables.Tag; +import com.iota.iri.model.persistables.Bundle; +import com.iota.iri.model.persistables.Address; +import com.iota.iri.model.persistables.Approvee; +import com.iota.iri.model.persistables.Hashes; +import com.iota.iri.storage.PermanentPersistenceProvider; +import com.iota.iri.storage.Indexable; +import com.iota.iri.storage.Persistable; +import com.iota.iri.storage.PersistenceProvider; +import com.iota.iri.utils.IotaIOUtils; +import com.iota.iri.utils.Pair; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.SystemUtils; + +import org.rocksdb.util.SizeUnit; +import org.rocksdb.ColumnFamilyHandle; +import org.rocksdb.RocksDB; +import org.rocksdb.Priority; +import org.rocksdb.DBOptions; +import org.rocksdb.BloomFilter; +import org.rocksdb.WriteBatch; +import org.rocksdb.WriteOptions; +import org.rocksdb.RocksEnv; +import org.rocksdb.BlockBasedTableConfig; +import org.rocksdb.StringAppendOperator; +import org.rocksdb.MergeOperator; +import org.rocksdb.ColumnFamilyOptions; +import org.rocksdb.ColumnFamilyDescriptor; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.nio.ByteBuffer; +import java.nio.file.Paths; +import java.util.Set; +import java.util.Map; +import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +/*** + * Implements the retrieval functions of a the normal persistence provider and the permanent persistence provider + */ +public class RocksDBPPPImpl implements PermanentPersistenceProvider, PersistenceProvider { + + private static final Logger log = LoggerFactory.getLogger(RocksDBPPPImpl.class); + private static final int BLOOM_FILTER_BITS_PER_KEY = 10; + /** A delimeter for separating hashes within a byte stream */ + private static final byte delimiter = ",".getBytes()[0]; + public static String TRANSACTION_COLUMN = "transaction"; + public static String ADDRESS_INDEX = "address-index"; + public static String BUNDLE_INDEX = "bundle-index"; + public static String TAG_INDEX = "tag-index"; + public static String APPROVEE_INDEX = "approvee-index"; + private final List columnFamilyHandles = new ArrayList<>(); + private final String dbPath; + private final String logPath; + private final int cacheSize; + @VisibleForTesting + Map columnMap = new HashMap<>(); + + private RocksDB db; + // DBOptions is only used in initDB(). However, it is closeable - so we keep a reference for shutdown. + private DBOptions options; + private BloomFilter bloomFilter; + private boolean available; + + /** + * Constructor + * @param dbPath database path + * @param logPath database log path + * @param cacheSize cache size + */ + public RocksDBPPPImpl(String dbPath, String logPath, int cacheSize) { + this.dbPath = dbPath; + this.logPath = logPath; + this.cacheSize = cacheSize; + } + + /** + * @see PersistenceProvider#init() + * @throws Exception + */ + @Override + public void init() throws Exception { + log.info("Initializing Database on " + dbPath); + initDB(dbPath, logPath); + available = true; + log.info("RocksDB permanent persistence provider initialized."); + } + + /** + * @see PersistenceProvider#isAvailable() + * @return + */ + @Override + public boolean isAvailable() { + return this.available; + } + + /** + * @see PersistenceProvider#shutdown() + */ + @Override + public void shutdown() { + for (final ColumnFamilyHandle columnFamilyHandle : columnFamilyHandles) { + IotaIOUtils.closeQuietly(columnFamilyHandle); + } + IotaIOUtils.closeQuietly(db, options, bloomFilter); + } + + // ----------- PersistenceProvider only overrides --------------- + /** + * @see PersistenceProvider#save(Persistable, Indexable) + */ + @Override + public boolean save(Persistable model, Indexable index) throws Exception { + return false; + } + + /** + * @see PersistenceProvider#delete(Class, Indexable) + */ + @Override + public void delete(Class model, Indexable index) throws Exception { + return; + } + + /** + * @see PersistenceProvider#update(Persistable, Indexable, String) + */ + @Override + public boolean update(Persistable model, Indexable index, String item) throws Exception { + return false; + } + + /** + * @see PersistenceProvider#exists(Class, Indexable) () + */ + @Override + public boolean exists(Class model, Indexable key) throws Exception { + return false; + } + + /** + * @see PersistenceProvider#latest(Class, Class) + */ + @Override + public Pair latest(Class model, Class indexModel) throws Exception { + return null; + } + + /** + * @see PersistenceProvider#keysWithMissingReferences(Class, Class) + */ + @Override + public Set keysWithMissingReferences(Class modelClass, Class otherClass) throws Exception { + return null; + } + + /** + * @see PersistenceProvider#get(Class, Indexable) + */ + @Override + public Persistable get(Class model, Indexable index) throws Exception { + Persistable object = (Persistable) model.newInstance(); + + if (object instanceof Transaction) { + + TransactionViewModel tvm = getTransaction((Hash) index); + if (tvm == null) { + return object; + } + // These 'manually' fill the metadata from the transaction data. + tvm.getAddressHash(); + tvm.getBundleHash(); + tvm.getBranchTransactionHash(); + tvm.getTrunkTransactionHash(); + tvm.getObsoleteTagValue(); + tvm.getObsoleteTagValue(); + tvm.getTagValue(); + tvm.setAttachmentData(); + tvm.setMetadata(); + + return tvm.getTransaction(); + } + + if (object instanceof Bundle) { + Bundle toReturn = new Bundle(); + toReturn.set = findBundle((Hash) index).set; + return toReturn; + } + if (object instanceof Address) { + Address toReturn = new Address(); + toReturn.set = findAddress((Hash) index).set; + return toReturn; + } + if (object instanceof Tag) { + Tag toReturn = new Tag(); + toReturn.set = findTag((Hash) index).set; + return toReturn; + } + if (object instanceof Approvee) { + Approvee toReturn = new Approvee(); + toReturn.set = findApprovee((Hash) index).set; + return toReturn; + } + return object; + } + + /** + * @see PersistenceProvider#mayExist(Class, Indexable) + */ + @Override + public boolean mayExist(Class model, Indexable index) throws Exception { + return false; + } + + /** + * @see PersistenceProvider#count(Class) + */ + @Override + public long count(Class model) throws Exception { + return 0; + } + + /** + * @see PersistenceProvider#keysStartingWith(Class, byte[]) + */ + @Override + public Set keysStartingWith(Class modelClass, byte[] value) { + return null; + } + + /** + * @see PersistenceProvider#seek(Class, byte[]) + */ + @Override + public Persistable seek(Class model, byte[] key) throws Exception { + return null; + } + + /** + * @see PersistenceProvider#next(Class, Indexable) + */ + @Override + public Pair next(Class model, Indexable index) throws Exception { + return null; + } + + /** + * @see PersistenceProvider#previous(Class, Indexable) + */ + @Override + public Pair previous(Class model, Indexable index) throws Exception { + return null; + } + + /** + * @see PersistenceProvider#first(Class, Class) + */ + @Override + public Pair first(Class model, Class indexModel) throws Exception { + return null; + } + + /** + * @see PersistenceProvider#saveBatch(List) + */ + @Override + public boolean saveBatch(List> models) throws Exception { + return false; + } + + /** + * @see PersistenceProvider#deleteBatch(Collection) + */ + @Override + public void deleteBatch(Collection>> models) + throws Exception { + return; + } + + /** + * @see PersistenceProvider#clear(Class) + */ + @Override + public void clear(Class column) throws Exception { + return; + } + + /** + * @see PersistenceProvider#clearMetadata(Class) + */ + @Override + public void clearMetadata(Class column) throws Exception { + return; + } + + /** + * @see PersistenceProvider#loadAllKeysFromTable(Class) + */ + @Override + public List loadAllKeysFromTable(Class model) { + return null; + } + // END----------- PersistenceProvider only overrides --------------- + + /** + * @see PermanentPersistenceProvider#pinTransaction(TransactionViewModel, Hash) + */ + @Override + public boolean pinTransaction(TransactionViewModel model, Hash index) throws Exception { + boolean exists = isPinned(Collections.singletonList(index))[0]; + if (!exists) { + try (WriteBatch writeBatch = new WriteBatch(); WriteOptions writeOptions = new WriteOptions()) { + byte[] txBytes = model.getBytes(); + + writeBatch.put(columnMap.get(TRANSACTION_COLUMN), index.bytes(), txBytes); + + addToIndex(writeBatch, columnMap.get(ADDRESS_INDEX), model.getAddressHash(), index); + addToIndex(writeBatch, columnMap.get(TAG_INDEX), model.getTagValue(), index); + addToIndex(writeBatch, columnMap.get(BUNDLE_INDEX), model.getBundleHash(), index); + addToIndex(writeBatch, columnMap.get(APPROVEE_INDEX), model.getTrunkTransactionHash(), index); + addToIndex(writeBatch, columnMap.get(APPROVEE_INDEX), model.getBranchTransactionHash(), index); + db.write(writeOptions, writeBatch); + return true; + } + } else { + return true; // Already pinned, is fine to return true + } + + } + + /** + * @see PermanentPersistenceProvider#unpinTransaction(Hash) + */ + @Override + public boolean unpinTransaction(Hash index) throws Exception { + try (WriteBatch writeBatch = new WriteBatch(); WriteOptions writeOptions = new WriteOptions()) { + safeDeleteTransaction(writeBatch, index); + db.write(writeOptions, writeBatch); + return true; + }catch (Exception e) { + log.error(e.getLocalizedMessage()); + return false; + } + } + + /** + * @see PermanentPersistenceProvider#isPinned(List) + */ + @Override + public boolean[] isPinned(List indexes) throws Exception { + boolean[] result = new boolean[indexes.size()]; + ColumnFamilyHandle handle = columnMap.get(TRANSACTION_COLUMN); + for (int i = 0; i < result.length; i++) { + byte[] keyBytes = indexes.get(i).bytes(); + if(db.keyMayExist(handle, keyBytes, new StringBuilder())){ + result[i] = db.get(handle, keyBytes) != null; + }else{ + result[i] = false; + } + + } + return result; + } + + /** + * Atomic deletion of a transaction and their relevant indexes + * + * @param writeBatch rocksDB atomic write batch + * @param key transaction ID to delete + * @throws Exception + */ + @VisibleForTesting + void safeDeleteTransaction(WriteBatch writeBatch, Hash key) throws Exception { + byte[] keyBytes = key.bytes(); + TransactionViewModel tx = getTransaction(key); + if(tx != null) { + writeBatch.delete(columnMap.get(TRANSACTION_COLUMN), keyBytes); + + removeFromIndex(writeBatch, columnMap.get(ADDRESS_INDEX), tx.getAddressHash(), key); + removeFromIndex(writeBatch, columnMap.get(TAG_INDEX), tx.getTagValue(), key); + removeFromIndex(writeBatch, columnMap.get(BUNDLE_INDEX), tx.getBundleHash(), key); + removeFromIndex(writeBatch, columnMap.get(APPROVEE_INDEX), tx.getTrunkTransactionHash(), key); + removeFromIndex(writeBatch, columnMap.get(APPROVEE_INDEX), tx.getBranchTransactionHash(), key); + } + } + + /** + * Removes an index from a set and deletes the index when the set is empty. + * With an atomic write batch + * @param writeBatch rocksDB atomic operation + * @param column index column + * @param key index key + * @param indexValue index value + * @throws Exception + */ + @VisibleForTesting + void removeFromIndex(WriteBatch writeBatch, ColumnFamilyHandle column, Indexable key, Indexable indexValue) + throws Exception { + byte[] indexBytes = indexValue.bytes(); + byte[] result = db.get(column, key.bytes()); + if (result != null) { + if (result.length <= indexBytes.length) { + // when it is the last one to delete just delete the entire index key space. + writeBatch.delete(column, key.bytes()); + } else { + + int keyLoc = Bytes.indexOf(result, indexBytes); + if (keyLoc >= 0) { // Does exists + ByteBuffer buffer = ByteBuffer.allocate(result.length - indexBytes.length - 1); + byte[] subarray1 = ArrayUtils.subarray(result, 0, keyLoc - 1); + buffer.put(subarray1); + byte[] subarray2 = ArrayUtils.subarray(result, keyLoc + indexBytes.length + 1, result.length); + if (subarray1.length > 0 && subarray2.length > 0) { + buffer.put(delimiter); + buffer.put(subarray2); + } + writeBatch.put(column, key.bytes(), buffer.array()); + } + } + } + } + + /** + * Adds a key to an index through an atomic write batch + * @param writeBatch rocksDB atomic operation + * @param column index column + * @param key index key + * @param indexValue index value + * @throws Exception + */ + @VisibleForTesting + void addToIndex(WriteBatch writeBatch, ColumnFamilyHandle column, Indexable key, Indexable indexValue) + throws Exception { + byte[] indexBytes = indexValue.bytes(); + + byte[] dbResult = db.get(column, key.bytes()); + + if (dbResult != null && Bytes.indexOf(dbResult, indexBytes) != -1) {// not found + + ByteBuffer buffer = ByteBuffer + .allocate(dbResult.length + indexBytes.length + (dbResult.length > 0 ? 1 : 0));// +1 delimiter + // length + buffer.put(dbResult); + if (dbResult.length > 0) { + // Means we add on the end of the current stream. + // To be compatible with Hashes.read(byte[]) + buffer.put(delimiter); + } + buffer.put(indexBytes); + writeBatch.put(column, key.bytes(), buffer.array()); + } + } + + /** + * @see PermanentPersistenceProvider#getTransaction(Hash) + */ + @Override + public TransactionViewModel getTransaction(Hash index) throws Exception { + if (index == null) { + return null; + } + byte[] dbResult = db.get(columnMap.get(TRANSACTION_COLUMN), index.bytes()); + if (dbResult != null && dbResult.length > 0) { + return new TransactionViewModel(TransactionViewModel.trits(dbResult), Hash.NULL_HASH); + } + return null; + } + /** + * @see PermanentPersistenceProvider#findAddress(Hash) + */ + @Override + public Hashes findAddress(Hash index) throws Exception { + return getIndex(columnMap.get(ADDRESS_INDEX), index); + } + /** + * @see PermanentPersistenceProvider#findBundle(Hash) + */ + @Override + public Hashes findBundle(Hash index) throws Exception { + return getIndex(columnMap.get(BUNDLE_INDEX), index); + } + /** + * @see PermanentPersistenceProvider#findTag(Hash) + */ + @Override + public Hashes findTag(Hash index) throws Exception { + return getIndex(columnMap.get(TAG_INDEX), index); + } + /** + * @see PermanentPersistenceProvider#findApprovee(Hash) + */ + @Override + public Hashes findApprovee(Hash index) throws Exception { + return getIndex(columnMap.get(APPROVEE_INDEX), index); + } + + private Hashes getIndex(ColumnFamilyHandle column, Indexable index) throws Exception { + if (index == null) { + return new Hashes(); + } + Hashes found = new Hashes(); + byte[] result = db.get(column, index.bytes()); + if (result != null) { + found.read(result); + } + return found; + } + + private void initDB(String path, String logPath) throws Exception { + try { + try { + RocksDB.loadLibrary(); + } catch (Exception e) { + if (SystemUtils.IS_OS_WINDOWS) { + log.error("Error loading RocksDB library. Please ensure that " + + "Microsoft Visual C++ 2015 Redistributable Update 3 " + "is installed and updated"); + } + throw e; + } + + File pathToLogDir = Paths.get(logPath).toFile(); + if (!pathToLogDir.exists() || !pathToLogDir.isDirectory()) { + boolean success = pathToLogDir.mkdir(); + if (!success) { + log.warn("Unable to make directory: {}", pathToLogDir); + } + } + + int numThreads = Math.max(1, Runtime.getRuntime().availableProcessors() / 2); + RocksEnv.getDefault().setBackgroundThreads(numThreads, Priority.HIGH).setBackgroundThreads(numThreads, + Priority.LOW); + + options = new DBOptions().setCreateIfMissing(true).setCreateMissingColumnFamilies(true).setDbLogDir(logPath) + .setMaxLogFileSize(SizeUnit.MB).setMaxManifestFileSize(SizeUnit.MB).setMaxOpenFiles(10000) + .setMaxBackgroundCompactions(1); + + options.setMaxSubcompactions(Runtime.getRuntime().availableProcessors()); + + bloomFilter = new BloomFilter(BLOOM_FILTER_BITS_PER_KEY); + + BlockBasedTableConfig blockBasedTableConfig = new BlockBasedTableConfig().setFilter(bloomFilter); + blockBasedTableConfig.setFilter(bloomFilter).setCacheNumShardBits(2).setBlockSizeDeviation(10) + .setBlockRestartInterval(16).setBlockCacheSize(cacheSize * SizeUnit.KB) + .setBlockCacheCompressedNumShardBits(10).setBlockCacheCompressedSize(32 * SizeUnit.KB); + + options.setAllowConcurrentMemtableWrite(true); + + MergeOperator mergeOperator = new StringAppendOperator(); + ColumnFamilyOptions columnFamilyOptions = new ColumnFamilyOptions().setMergeOperator(mergeOperator) + .setTableFormatConfig(blockBasedTableConfig).setMaxWriteBufferNumber(2) + .setWriteBufferSize(2 * SizeUnit.MB); + + List columnFamilyDescriptors = new ArrayList<>(); + // Add default column family. Main motivation is to not change legacy code + columnFamilyDescriptors.add(new ColumnFamilyDescriptor(RocksDB.DEFAULT_COLUMN_FAMILY, columnFamilyOptions)); + + columnFamilyDescriptors + .add(new ColumnFamilyDescriptor(TRANSACTION_COLUMN.getBytes("UTF-8"), columnFamilyOptions)); + // columnFamilyDescriptors.add(new ColumnFamilyDescriptor(COUNTER_INDEX.getBytes("UTF-8"), + // columnFamilyOptions)); + columnFamilyDescriptors + .add(new ColumnFamilyDescriptor(BUNDLE_INDEX.getBytes("UTF-8"), columnFamilyOptions)); + columnFamilyDescriptors.add(new ColumnFamilyDescriptor(TAG_INDEX.getBytes("UTF-8"), columnFamilyOptions)); + columnFamilyDescriptors + .add(new ColumnFamilyDescriptor(ADDRESS_INDEX.getBytes("UTF-8"), columnFamilyOptions)); + columnFamilyDescriptors + .add(new ColumnFamilyDescriptor(APPROVEE_INDEX.getBytes("UTF-8"), columnFamilyOptions)); + + db = RocksDB.open(options, path, columnFamilyDescriptors, columnFamilyHandles); + db.enableFileDeletions(true); + + for (ColumnFamilyHandle columnHandler : columnFamilyHandles) { + columnMap.put(new String(columnHandler.getName(), "UTF-8"), columnHandler); + } + + } catch (Exception e) { + IotaIOUtils.closeQuietly(db); + throw e; + } + } + +} diff --git a/src/test/java/com/iota/iri/storage/rocksDB/PermaRocksDBPersentenceProviderTest.java b/src/test/java/com/iota/iri/storage/rocksDB/PermaRocksDBPersentenceProviderTest.java new file mode 100644 index 0000000000..e4688f45e4 --- /dev/null +++ b/src/test/java/com/iota/iri/storage/rocksDB/PermaRocksDBPersentenceProviderTest.java @@ -0,0 +1,89 @@ +package com.iota.iri.storage.rocksDB; + +import com.iota.iri.controllers.TransactionViewModel; +import com.iota.iri.model.Hash; + +import org.apache.commons.io.FileUtils; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import static com.iota.iri.TransactionTestUtils.getTransactionHash; +import static com.iota.iri.TransactionTestUtils.getTransactionTrits; +import static com.iota.iri.TransactionTestUtils.getTransactionTritsWithTrunkAndBranch; + +public class PermaRocksDBPersentenceProviderTest { + private static RocksDBPPPImpl rocksDBPersistenceProvider; + private static String dbPath = "tmpdb", dbLogPath = "tmplogs"; + + @BeforeClass + public static void setUpDb() throws Exception { + rocksDBPersistenceProvider = new RocksDBPPPImpl( + dbPath, dbLogPath,1000); + rocksDBPersistenceProvider.init(); + } + + @AfterClass + public static void destroyDb() { + rocksDBPersistenceProvider.shutdown(); + FileUtils.deleteQuietly(new File(dbPath)); + FileUtils.deleteQuietly(new File(dbLogPath)); + rocksDBPersistenceProvider = null; + } + + + @Test + public void testSelectiveTransactionModel() throws Exception { + + TransactionViewModel transaction, transaction1, transaction2, transaction3, transaction4; + transaction = new TransactionViewModel(getTransactionTrits(), Hash.NULL_HASH); + transaction1 = new TransactionViewModel(getTransactionTritsWithTrunkAndBranch(transaction.getHash(), + transaction.getHash()), getTransactionHash()); + transaction2 = new TransactionViewModel(getTransactionTritsWithTrunkAndBranch(transaction1.getHash(), + transaction1.getHash()), getTransactionHash()); + transaction3 = new TransactionViewModel(getTransactionTritsWithTrunkAndBranch(transaction2.getHash(), + transaction1.getHash()), getTransactionHash()); + transaction4 = new TransactionViewModel(getTransactionTritsWithTrunkAndBranch(transaction2.getHash(), + transaction3.getHash()), getTransactionHash()); + + List mm = new LinkedList<>(); + mm.add(transaction); + mm.add(transaction1); + mm.add(transaction2); + mm.add(transaction3); + mm.add(transaction4); + + for(TransactionViewModel v: mm){ + //System.out.println(v.getHash() + " \t " + v.getAddressHash()); + rocksDBPersistenceProvider.pinTransaction(v, v.getHash()); + } + //rocksDBPersistenceProvider.incrementTransactions(new Indexable[]{transaction.getHash()}); + boolean isPinned = rocksDBPersistenceProvider.isPinned(Collections.singletonList(transaction.getHash()))[0]; + Assert.assertTrue(isPinned); + + //See if all indexes are updated. + Assert.assertTrue(rocksDBPersistenceProvider.findAddress(transaction2.getAddressHash()).set.contains(transaction2.getHash())); + Assert.assertTrue(rocksDBPersistenceProvider.findTag(transaction2.getTagValue()).set.contains(transaction2.getHash())); + Assert.assertTrue(rocksDBPersistenceProvider.findBundle(transaction2.getBundleHash()).set.contains(transaction2.getHash())); + Assert.assertTrue(rocksDBPersistenceProvider.findApprovee(transaction2.getTrunkTransactionHash()).set.contains(transaction2.getHash())); + Assert.assertTrue(rocksDBPersistenceProvider.findApprovee(transaction2.getBranchTransactionHash()).set.contains(transaction2.getHash())); + + //Test decremeant delete; + rocksDBPersistenceProvider.unpinTransaction(transaction3.getHash()); + + //Test if indexes are updated after delete + Assert.assertFalse(rocksDBPersistenceProvider.findAddress(transaction3.getAddressHash()).set.contains(transaction3.getHash())); + Assert.assertFalse(rocksDBPersistenceProvider.findTag(transaction3.getTagValue()).set.contains(transaction3.getHash())); + + boolean isPinnedTwo = rocksDBPersistenceProvider.isPinned(Collections.singletonList(transaction3.getHash()))[0]; + Assert.assertFalse(isPinnedTwo); + Assert.assertNull(rocksDBPersistenceProvider.getTransaction(transaction3.getHash())); + + } +}