diff --git a/src/main/java/io/horizen/lambo/CarRegistryAppModule.java b/src/main/java/io/horizen/lambo/CarRegistryAppModule.java index 2806f7b..4c7778c 100644 --- a/src/main/java/io/horizen/lambo/CarRegistryAppModule.java +++ b/src/main/java/io/horizen/lambo/CarRegistryAppModule.java @@ -1,7 +1,9 @@ package io.horizen.lambo; import com.google.inject.AbstractModule; +import com.google.inject.Provides; import com.google.inject.TypeLiteral; +import com.google.inject.name.Named; import com.google.inject.name.Names; import com.horizen.SidechainSettings; import com.horizen.api.http.ApplicationApiGroup; @@ -93,12 +95,6 @@ protected void configure() { SidechainTransactionsCompanion transactionsCompanion = new SidechainTransactionsCompanion( customTransactionSerializers, sidechainBoxesDataCompanion, sidechainProofsCompanion); - - // Define Application state and wallet logic: - ApplicationWallet defaultApplicationWallet = new CarRegistryApplicationWallet(); - ApplicationState defaultApplicationState = new CarRegistryApplicationState(); - - // Define the path to storages: String dataDirPath = sidechainSettings.scorexSettings().dataDir().getAbsolutePath(); File secretStore = new File( dataDirPath + "/secret"); @@ -108,18 +104,11 @@ protected void configure() { File stateStore = new File(dataDirPath + "/state"); File historyStore = new File(dataDirPath + "/history"); File consensusStore = new File(dataDirPath + "/consensusData"); - - - // Add car registry specific API endpoints: - // CarApi endpoints processing will be added to the API server. - List customApiGroups = new ArrayList<>(); - customApiGroups.add(new CarApi(transactionsCompanion)); - + File carInfoStore = new File(dataDirPath + "/cars"); // No core API endpoints to be disabled: List> rejectedApiPaths = new ArrayList<>(); - // Inject custom objects: // Names are equal to the ones specified in SidechainApp class constructor. bind(SidechainSettings.class) @@ -142,13 +131,13 @@ protected void configure() { .annotatedWith(Names.named("CustomTransactionSerializers")) .toInstance(customTransactionSerializers); + // Define Application state and wallet logic: bind(ApplicationWallet.class) .annotatedWith(Names.named("ApplicationWallet")) - .toInstance(defaultApplicationWallet); - + .to(CarRegistryApplicationWallet.class); bind(ApplicationState.class) .annotatedWith(Names.named("ApplicationState")) - .toInstance(defaultApplicationState); + .to(CarRegistryApplicationState.class); bind(Storage.class) .annotatedWith(Names.named("SecretStorage")) @@ -171,13 +160,26 @@ protected void configure() { bind(Storage.class) .annotatedWith(Names.named("ConsensusStorage")) .toInstance(IODBStorageUtil.getStorage(consensusStore)); - - bind(new TypeLiteral> () {}) - .annotatedWith(Names.named("CustomApiGroups")) - .toInstance(customApiGroups); + bind(Storage.class) + .annotatedWith(Names.named("CarInfoStorage")) + .toInstance(IODBStorageUtil.getStorage(carInfoStore)); bind(new TypeLiteral>> () {}) .annotatedWith(Names.named("RejectedApiPaths")) .toInstance(rejectedApiPaths); + + bind(SidechainTransactionsCompanion.class) + .annotatedWith(Names.named("SidechainTransactionsCompanion")) + .toInstance(transactionsCompanion); } + + // Add car registry specific API endpoints: + // CarApi endpoints processing will be added to the API server. + @Provides @Named("CustomApiGroups") + List getCustomApiGroups(CarApi carApi) { + List customApiGroups = new ArrayList<>(); + customApiGroups.add(carApi); + return customApiGroups; + } + } diff --git a/src/main/java/io/horizen/lambo/CarRegistryApplicationState.java b/src/main/java/io/horizen/lambo/CarRegistryApplicationState.java index 9fe5977..7d31cca 100644 --- a/src/main/java/io/horizen/lambo/CarRegistryApplicationState.java +++ b/src/main/java/io/horizen/lambo/CarRegistryApplicationState.java @@ -1,39 +1,99 @@ package io.horizen.lambo; +import com.google.inject.Inject; import com.horizen.block.SidechainBlock; import com.horizen.box.Box; import com.horizen.proposition.Proposition; import com.horizen.state.ApplicationState; import com.horizen.state.SidechainStateReader; import com.horizen.transaction.BoxTransaction; +import io.horizen.lambo.car.box.CarBox; +import io.horizen.lambo.car.box.CarSellOrderBox; +import io.horizen.lambo.car.services.CarInfoDBService; +import io.horizen.lambo.car.transaction.CarDeclarationTransaction; import scala.util.Success; import scala.util.Try; - +import java.util.HashSet; import java.util.List; +import java.util.Optional; +import java.util.Set; +import scala.collection.JavaConverters; -// There is no custom logic for Car registry State now. -// TODO: prevent the declaration of CarBoxes which car information already exists in the previously added CarBoxes or CarSellOrderBoxes. public class CarRegistryApplicationState implements ApplicationState { + + private CarInfoDBService carInfoDbService; + + @Inject + public CarRegistryApplicationState(CarInfoDBService carInfoDbService) { + this.carInfoDbService = carInfoDbService; + } + @Override public boolean validate(SidechainStateReader stateReader, SidechainBlock block) { + //We check that there are no multiple transactions declaring the same VIN inside the block + Set vinList = new HashSet<>(); + for (BoxTransaction> t : JavaConverters.seqAsJavaList(block.transactions())){ + if (CarDeclarationTransaction.class.isInstance(t)){ + for (String currentVin : carInfoDbService.extractVinFromBoxes(t.newBoxes())){ + if (vinList.contains(currentVin)){ + return false; + }else{ + vinList.add(currentVin); + } + } + } + } return true; - } + } @Override public boolean validate(SidechainStateReader stateReader, BoxTransaction> transaction) { - // TODO: here we expect to go though all CarDeclarationTransactions and verify that each CarBox reflects to unique Car. + // we go though all CarDeclarationTransactions and verify that each CarBox reflects to unique Car. + if (CarDeclarationTransaction.class.isInstance(transaction)){ + Set vinList = carInfoDbService.extractVinFromBoxes(transaction.newBoxes()); + for (String vin : vinList) { + if (! carInfoDbService.validateVin(vin, Optional.empty())){ + return false; + } + } + } return true; } @Override - public Try onApplyChanges(SidechainStateReader stateReader, byte[] version, List> newBoxes, List boxIdsToRemove) { - // TODO: here we expect to update Car info database. The data from it will be used during validation. + public Try onApplyChanges(SidechainStateReader stateReader, + byte[] version, + List> newBoxes, List boxIdsToRemove) { + //we update the Car info database. The data from it will be used during validation. + + //collect the vin to be added: the ones declared in new boxes + Set vinToAdd = carInfoDbService.extractVinFromBoxes(newBoxes); + //collect the vin to be removed: the ones contained in the removed boxes that are not present in the prevoius list + Set vinToRemove = new HashSet<>(); + for (byte[] boxId : boxIdsToRemove) { + stateReader.getClosedBox(boxId).ifPresent( box -> { + if (box instanceof CarBox){ + String vin = ((CarBox)box).getVin(); + if (!vinToAdd.contains(vin)){ + vinToRemove.add(vin); + } + } else if (box instanceof CarSellOrderBox){ + String vin = ((CarSellOrderBox)box).getVin(); + if (!vinToAdd.contains(vin)){ + vinToRemove.add(vin); + } + } + } + ); + } + carInfoDbService.updateVin(version, vinToAdd, vinToRemove); return new Success<>(this); } + @Override public Try onRollback(byte[] version) { - // TODO: rollback car info database to certain point. + carInfoDbService.rollback(version); return new Success<>(this); } } diff --git a/src/main/java/io/horizen/lambo/car/api/CarApi.java b/src/main/java/io/horizen/lambo/car/api/CarApi.java index 76d6f32..76c7fd1 100644 --- a/src/main/java/io/horizen/lambo/car/api/CarApi.java +++ b/src/main/java/io/horizen/lambo/car/api/CarApi.java @@ -2,6 +2,8 @@ import akka.http.javadsl.server.Route; import com.fasterxml.jackson.annotation.JsonView; +import com.google.inject.Inject; +import com.google.inject.name.Named; import com.horizen.api.http.ApiResponse; import com.horizen.api.http.ApplicationApiGroup; import com.horizen.api.http.ErrorResponse; @@ -30,6 +32,7 @@ import io.horizen.lambo.car.info.CarBuyOrderInfo; import io.horizen.lambo.car.info.CarSellOrderInfo; import io.horizen.lambo.car.proof.SellOrderSpendingProof; +import io.horizen.lambo.car.services.CarInfoDBService; import io.horizen.lambo.car.transaction.BuyCarTransaction; import io.horizen.lambo.car.transaction.CarDeclarationTransaction; import io.horizen.lambo.car.transaction.SellCarTransaction; @@ -54,9 +57,12 @@ public class CarApi extends ApplicationApiGroup { private final SidechainTransactionsCompanion sidechainTransactionsCompanion; + private CarInfoDBService carInfoDBService; - public CarApi(SidechainTransactionsCompanion sidechainTransactionsCompanion) { + @Inject + public CarApi(@Named("SidechainTransactionsCompanion") SidechainTransactionsCompanion sidechainTransactionsCompanion, CarInfoDBService carInfoDBService) { this.sidechainTransactionsCompanion = sidechainTransactionsCompanion; + this.carInfoDBService = carInfoDBService; } // Define the base path for API url, i.e. according current config we could access that Api Group by using address 127.0.0.1:9085/carApi @@ -92,6 +98,11 @@ private ApiResponse createCar(SidechainNodeView view, CreateCarBoxRequest ent) { PublicKey25519Proposition carOwnershipProposition = PublicKey25519PropositionSerializer.getSerializer() .parseBytes(BytesUtils.fromHexString(ent.proposition)); + //check that the vin is unique (both in local veichle store and in mempool) + if (! carInfoDBService.validateVin(ent.vin, Optional.of(view.getNodeMemoryPool()))){ + throw new IllegalStateException("Vehicle identification number already present in blockchain"); + } + CarBoxData carBoxData = new CarBoxData(carOwnershipProposition, ent.vin, ent.year, ent.model, ent.color); // Try to collect regular boxes to pay fee diff --git a/src/main/java/io/horizen/lambo/car/services/CarInfoDBService.java b/src/main/java/io/horizen/lambo/car/services/CarInfoDBService.java new file mode 100644 index 0000000..679a1a8 --- /dev/null +++ b/src/main/java/io/horizen/lambo/car/services/CarInfoDBService.java @@ -0,0 +1,105 @@ +package io.horizen.lambo.car.services; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import com.horizen.box.Box; +import com.horizen.node.NodeMemoryPool; +import com.horizen.proposition.Proposition; +import com.horizen.storage.Storage; +import com.horizen.transaction.BoxTransaction; +import com.horizen.utils.ByteArrayWrapper; +import com.horizen.utils.Pair; +import io.horizen.lambo.car.box.CarBox; +import io.horizen.lambo.car.box.CarSellOrderBox; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import scorex.crypto.hash.Blake2b256; +import java.util.*; + +/** + * This service manages a local db with the list of all veichle identification numbers (vin) declared on the chain. + * The vin could be present inside two type of boxes: CarBox and CarSellOrderBox. + */ +public class CarInfoDBService { + + private Storage carInfoStorage; + protected Logger log = LoggerFactory.getLogger(CarInfoDBService.class.getName()); + + @Inject + public CarInfoDBService(@Named("CarInfoStorage") Storage carInfoStorage){ + this.carInfoStorage = carInfoStorage; + } + + public void updateVin(byte[] version, Set vinToAdd, Set vinToRemove){ + log.debug("carInfoStorage updateVin"); + log.debug(" vinToAdd "+vinToAdd.size()); + log.debug(" vinToRemove "+vinToRemove.size()); + List> toUpdate = new ArrayList<>(vinToAdd.size()); + List toRemove = new ArrayList<>(vinToRemove.size()); + vinToAdd.forEach(ele -> { + toUpdate.add(buildDBElement(ele)); + }); + vinToRemove.forEach(ele -> { + toRemove.add(buildDBElement(ele).getKey()); + }); + carInfoStorage.update(new ByteArrayWrapper(version), toUpdate, toRemove); + log.debug("carInfoStorage now contains: "+carInfoStorage.getAll().size()+" elements"); + } + + + /** + * Validate the given vehicle identification number against the db list and (optionally) the mempool transactions. + * @param vin the vehicle identification number to check + * @param memoryPool if not null, the vin is checked also against the mempool transactions + * @return true if the vin is valid (not already declared) + */ + public boolean validateVin(String vin, Optional memoryPool){ + if (carInfoStorage.get(buildDBElement(vin).getKey()).isPresent()){ + return false; + } + //in the vin is not found, and the mempool was provided, we check also there + if (memoryPool.isPresent()) { + for (BoxTransaction> transaction : memoryPool.get().getTransactions()) { + Set vinInMempool = extractVinFromBoxes(transaction.newBoxes()); + if (vinInMempool.contains(vin)){ + return false; + } + } + } + //if we arrive here, the vin is valid + return true; + } + + public void rollback(byte[] version) { + carInfoStorage.rollback(new ByteArrayWrapper(version)); + } + + /** + * Extracts the list of vehicle identification numbers (vin) declared in the given box list. + * The vin could be present inside two type of boxes: CarBox and CarSellOrderBox + */ + public Set extractVinFromBoxes(List> boxes){ + Set vinList = new HashSet(); + for (Box currentBox : boxes) { + if (CarBox.class.isAssignableFrom(currentBox.getClass())){ + String vin = CarBox.parseBytes(currentBox.bytes()).getVin(); + vinList.add(vin); + } else if (CarSellOrderBox.class.isAssignableFrom(currentBox.getClass())){ + String vin = CarSellOrderBox.parseBytes(currentBox.bytes()).getVin(); + vinList.add(vin); + } + } + return vinList; + } + + + + private Pair buildDBElement(String vin){ + //we hash the vin to be sure the key has a fixed size of 32, which is the default of iohk.iodb used as underline storage + ByteArrayWrapper keyWrapper = new ByteArrayWrapper(Blake2b256.hash(vin)); + //the value is not important (we need just a key set, each key is a vin hash) + ByteArrayWrapper valueWrapper = new ByteArrayWrapper(new byte[1]); + return new Pair<>(keyWrapper, valueWrapper); + } + +} diff --git a/src/main/java/io/horizen/lambo/car/transaction/CarDeclarationTransaction.java b/src/main/java/io/horizen/lambo/car/transaction/CarDeclarationTransaction.java index c55dfda..d19239f 100644 --- a/src/main/java/io/horizen/lambo/car/transaction/CarDeclarationTransaction.java +++ b/src/main/java/io/horizen/lambo/car/transaction/CarDeclarationTransaction.java @@ -30,7 +30,6 @@ public final class CarDeclarationTransaction extends AbstractRegularTransaction { private final CarBoxData outputCarBoxData; - private List> newBoxes; public CarDeclarationTransaction(List inputRegularBoxIds,