diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterService.java index 86ad4b8..70c3ee5 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterService.java @@ -2,6 +2,7 @@ import com.bloxbean.cardano.yaci.core.protocol.localstate.api.Era; import com.bloxbean.cardano.yaci.core.util.OSUtil; +import com.bloxbean.cardano.yacicli.localcluster.config.CustomGenesisConfig; import com.bloxbean.cardano.yacicli.localcluster.model.RunStatus; import com.bloxbean.cardano.yacicli.localcluster.config.ApplicationConfig; import com.bloxbean.cardano.yacicli.localcluster.config.GenesisConfig; @@ -73,6 +74,9 @@ public class ClusterService { @Autowired private GenesisConfig genesisConfig; + @Autowired + private CustomGenesisConfig customGenesisConfig; + @Autowired private PrivNetService privNetService; @@ -304,6 +308,8 @@ private void updateGenesis(Path clusterFolder, String clusterName, ClusterInfo c Path destConwayGenesisFile = clusterFolder.resolve("node").resolve("genesis").resolve("conway-genesis.json"); GenesisConfig genesisConfigCopy = genesisConfig.copy(); + genesisConfigCopy.merge(customGenesisConfig.getMap()); + if (clusterInfo.getGenesisProfile() != null) genesisConfigCopy = GenesisProfile.applyGenesisProfile(clusterInfo.getGenesisProfile(), genesisConfigCopy); @@ -324,7 +330,7 @@ private void updateGenesis(Path clusterFolder, String clusterName, ClusterInfo c values.put("epochLength", String.valueOf(epochLength)); //Check if protocol version should be minimun 9 and it's conway era - if (era == Era.Conway && genesisConfig.getProtocolMajorVer() < 9) { + if (era == Era.Conway && genesisConfigCopy.getProtocolMajorVer() < 9) { values.put("protocolMajorVer", 10); values.put("protocolMinorVer", 2); } @@ -332,7 +338,7 @@ private void updateGenesis(Path clusterFolder, String clusterName, ClusterInfo c //Derive security param long securityParam = genesisConfigCopy.getSecurityParam(); - if (genesisConfig.getConwayHardForkAtEpoch() > 0 && genesisConfig.isShiftStartTimeBehind()) { + if (genesisConfigCopy.getConwayHardForkAtEpoch() > 0 && genesisConfigCopy.isShiftStartTimeBehind()) { //Workaround for https://github.com/bloxbean/yaci-devkit/issues/65 //Calculate required securityParam to jump directly to epoch = 1 long expectedStabilityWindow = Math.round(epochLength * 1.5); @@ -340,7 +346,7 @@ private void updateGenesis(Path clusterFolder, String clusterName, ClusterInfo c } else { if (securityParam == 0) { //For stabilityWindow = epochLength * stabilityWindowFactory (0-1) , k = (epochLength * coefficient) / (3 * 2) - securityParam = Math.round(((epochLength * activeSlotsCoeff) / 3) * genesisConfig.getStabilityWindowFactor()); + securityParam = Math.round(((epochLength * activeSlotsCoeff) / 3) * genesisConfigCopy.getStabilityWindowFactor()); } } @@ -416,9 +422,12 @@ private void updateConfiguration(Path clusterFolder, ClusterInfo clusterInfo, Co Path destConfigPath = clusterFolder.resolve("node").resolve("configuration.yaml"); boolean enableP2P = clusterInfo.isP2pEnabled(); - Map values = genesisConfig.getConfigMap(); + var genesisConfigCopy = genesisConfig.copy(); + genesisConfigCopy.merge(customGenesisConfig.getMap()); + + Map values = genesisConfigCopy.getConfigMap(); values.put("enableP2P", String.valueOf(enableP2P)); - values.put("peerSharing", genesisConfig.isPeerSharing()); + values.put("peerSharing", genesisConfigCopy.isPeerSharing()); values.put("prometheusPort", String.valueOf(clusterInfo.getPrometheusPort())); values.put("conway_era", clusterInfo.getEra() == Era.Conway ? Boolean.TRUE : Boolean.FALSE); diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterStartService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterStartService.java index d866e3c..b54dd7a 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterStartService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterStartService.java @@ -1,6 +1,7 @@ package com.bloxbean.cardano.yacicli.localcluster; import com.bloxbean.cardano.yaci.core.util.OSUtil; +import com.bloxbean.cardano.yacicli.localcluster.config.CustomGenesisConfig; import com.bloxbean.cardano.yacicli.localcluster.config.GenesisConfig; import com.bloxbean.cardano.yacicli.localcluster.model.RunStatus; import com.bloxbean.cardano.yacicli.util.PortUtil; @@ -39,6 +40,7 @@ public class ClusterStartService { private final ClusterPortInfoHelper clusterPortInfoHelper; private final ProcessUtil processUtil; private final GenesisConfig genesisConfig; + private final CustomGenesisConfig customGenesisConfig; private ObjectMapper objectMapper = new ObjectMapper(); private List processes = new ArrayList<>(); @@ -263,7 +265,10 @@ private boolean setupFirstRun(ClusterInfo clusterInfo, Path clusterFolder, Consu ObjectNode jsonNode = (ObjectNode) objectMapper.readTree(byronGenesis.toFile()); long byronStartTime = Instant.now().getEpochSecond(); - if (genesisConfig.getConwayHardForkAtEpoch() > 0 && genesisConfig.isShiftStartTimeBehind()) { + var genesisConfigCopy = genesisConfig.copy(); + genesisConfigCopy.merge(customGenesisConfig.getMap()); + + if (genesisConfigCopy.getConwayHardForkAtEpoch() > 0 && genesisConfigCopy.isShiftStartTimeBehind()) { long stabilityWindow = (long) Math.floor((3 * clusterInfo.getSecurityParam()) / clusterInfo.getActiveSlotsCoeff()); long maxBehindBySecond = stabilityWindow - 5; diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/api/ClusterAdminController.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/api/ClusterAdminController.java index 6d28512..e8e4e82 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/api/ClusterAdminController.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/api/ClusterAdminController.java @@ -3,8 +3,10 @@ import com.bloxbean.cardano.yacicli.localcluster.ClusterCommands; import com.bloxbean.cardano.yacicli.localcluster.ClusterInfo; import com.bloxbean.cardano.yacicli.localcluster.ClusterService; +import com.bloxbean.cardano.yacicli.localcluster.config.CustomGenesisConfig; import com.bloxbean.cardano.yacicli.localcluster.service.ClusterUtilService; import com.bloxbean.cardano.yacicli.common.CommandContext; +import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; @@ -21,6 +23,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -34,6 +37,7 @@ public class ClusterAdminController { private final ClusterService clusterService; private final ClusterUtilService clusterUtilService; private final ClusterCommands clusterCommands; + private final CustomGenesisConfig customGenesisConfig; @GetMapping("/devnet/download") public ResponseEntity downloadFiles() throws IOException { @@ -170,4 +174,38 @@ public ResponseEntity getGenesisHash() { else return ResponseEntity.notFound().build(); } + + @PostMapping("/devnet/create") + @Operation(summary = """ + Create and start the devnet by overriding the provided genesis properties. This method accepts a JSON object as input to + override the default genesis property values. All properties defined in `node.properties`, except for nested properties, are supported. + This is useful for dynamically changing the node configuration without modifying the `node.properties` file. + A value should always be set as string even if it's a number or boolean. + """) + public boolean create(@RequestBody Map requestData) { + CommandContext.INSTANCE.setProperty("cluster_name", DEFAULT_CLUSTER_NAME); + + int epochLength = 0; + try { + epochLength = Integer.parseInt(requestData.get("epochLength")); + } catch (Exception e) { + } + + if (epochLength == 0) + epochLength = 600; + + //Check if constitution script is there and it's value is empty + var constitutionScript = requestData.get("constitutionScript"); + if (constitutionScript == null || constitutionScript.isEmpty()) + requestData.remove(constitutionScript); + + customGenesisConfig.populate(requestData); + + try { + clusterCommands.createCluster(DEFAULT_CLUSTER_NAME, 3001, 8090, 1, 1, epochLength, true, true, "conway", null, false); + return true; + } catch (Exception e) { + return false; + } + } } diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/config/CustomGenesisConfig.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/config/CustomGenesisConfig.java new file mode 100644 index 0000000..2480989 --- /dev/null +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/config/CustomGenesisConfig.java @@ -0,0 +1,28 @@ +package com.bloxbean.cardano.yacicli.localcluster.config; + +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/* + * This class holds the custom overloaded values for the GenesisConfig. Ideally, the map in this class remains empty. + * However, if the GenesisConfig is updated through the admin create endpoint, these values will be merged with the default GenesisConfig values during creation of node. + */ +@Component +public class CustomGenesisConfig { + private Map map; + + public CustomGenesisConfig() { + this.map = new HashMap<>(); + } + + public void populate(Map updatedMap) { + this.map.clear(); + this.map.putAll(updatedMap); + } + + public Map getMap() { + return map; + } +} diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/config/GenesisConfig.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/config/GenesisConfig.java index e36434c..427670a 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/config/GenesisConfig.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/config/GenesisConfig.java @@ -335,7 +335,7 @@ public Map getConfigMap() { map.put("minFeeRefScriptCostPerByte", minFeeRefScriptCostPerByte); map.put("constitutionUrl", constitutionUrl); map.put("constitutionDataHash", constitutionDataHash); - if (constitutionScript != null) + if (constitutionScript != null && !constitutionScript.trim().isEmpty()) map.put("constitutionScript", constitutionScript); if (ccMembers != null && ccMembers.size() > 0) @@ -448,6 +448,116 @@ public GenesisConfig copy() { return genesisConfig; } + public void merge(Map updatedValues) { + if (updatedValues != null && !updatedValues.isEmpty()) { + if (updatedValues.get("protocolMagic") != null && !updatedValues.get("protocolMagic").isEmpty()) + protocolMagic = Long.parseLong(updatedValues.get("protocolMagic")); + if (updatedValues.get("maxKESEvolutions") != null && !updatedValues.get("maxKESEvolutions").isEmpty()) + maxKESEvolutions = Integer.parseInt(updatedValues.get("maxKESEvolutions")); + if (updatedValues.get("stabilityWindowFactor") != null && !updatedValues.get("stabilityWindowFactor").isEmpty()) + stabilityWindowFactor = Integer.parseInt(updatedValues.get("stabilityWindowFactor")); + if (updatedValues.get("securityParam") != null && !updatedValues.get("securityParam").isEmpty()) + securityParam = Integer.parseInt(updatedValues.get("securityParam")); + if (updatedValues.get("slotsPerKESPeriod") != null && !updatedValues.get("slotsPerKESPeriod").isEmpty()) + slotsPerKESPeriod = Integer.parseInt(updatedValues.get("slotsPerKESPeriod")); + if (updatedValues.get("updateQuorum") != null && !updatedValues.get("updateQuorum").isEmpty()) + updateQuorum = Integer.parseInt(updatedValues.get("updateQuorum")); + if (updatedValues.get("peerSharing") != null && !updatedValues.get("peerSharing").isEmpty()) + peerSharing = Boolean.parseBoolean(updatedValues.get("peerSharing")); + if (updatedValues.get("genesisUtxoSupply") != null && !updatedValues.get("genesisUtxoSupply").isEmpty()) + genesisUtxoSupply = updatedValues.get("genesisUtxoSupply"); + if (updatedValues.get("nGenesisKeys") != null && !updatedValues.get("nGenesisKeys").isEmpty()) + nGenesisKeys = Integer.parseInt(updatedValues.get("nGenesisKeys")); + if (updatedValues.get("nGenesisUtxoKeys") != null && !updatedValues.get("nGenesisUtxoKeys").isEmpty()) + nGenesisUtxoKeys = Integer.parseInt(updatedValues.get("nGenesisUtxoKeys")); + if (updatedValues.get("maxLovelaceSupply") != null && !updatedValues.get("maxLovelaceSupply").isEmpty()) + maxLovelaceSupply = updatedValues.get("maxLovelaceSupply"); + if (updatedValues.get("poolPledgeInfluence") != null && !updatedValues.get("poolPledgeInfluence").isEmpty()) + poolPledgeInfluence = Float.parseFloat(updatedValues.get("poolPledgeInfluence")); + if (updatedValues.get("decentralisationParam") != null && !updatedValues.get("decentralisationParam").isEmpty()) + decentralisationParam = new BigDecimal(updatedValues.get("decentralisationParam")); + if (updatedValues.get("eMax") != null && !updatedValues.get("eMax").isEmpty()) + eMax = Integer.parseInt(updatedValues.get("eMax")); + if (updatedValues.get("keyDeposit") != null && !updatedValues.get("keyDeposit").isEmpty()) + keyDeposit = new BigInteger(updatedValues.get("keyDeposit")); + if (updatedValues.get("maxBlockBodySize") != null && !updatedValues.get("maxBlockBodySize").isEmpty()) + maxBlockBodySize = Long.parseLong(updatedValues.get("maxBlockBodySize")); + if (updatedValues.get("maxBlockHeaderSize") != null && !updatedValues.get("maxBlockHeaderSize").isEmpty()) + maxBlockHeaderSize = Long.parseLong(updatedValues.get("maxBlockHeaderSize")); + if (updatedValues.get("maxTxSize") != null && !updatedValues.get("maxTxSize").isEmpty()) + maxTxSize = Long.parseLong(updatedValues.get("maxTxSize")); + if (updatedValues.get("minFeeA") != null && !updatedValues.get("minFeeA").isEmpty()) + minFeeA = Long.parseLong(updatedValues.get("minFeeA")); + if (updatedValues.get("minFeeB") != null && !updatedValues.get("minFeeB").isEmpty()) + minFeeB = Long.parseLong(updatedValues.get("minFeeB")); + if (updatedValues.get("minPoolCost") != null && !updatedValues.get("minPoolCost").isEmpty()) + minPoolCost = new BigInteger(updatedValues.get("minPoolCost")); + if (updatedValues.get("minUTxOValue") != null && !updatedValues.get("minUTxOValue").isEmpty()) + minUTxOValue = new BigInteger(updatedValues.get("minUTxOValue")); + if (updatedValues.get("nOpt") != null && !updatedValues.get("nOpt").isEmpty()) + nOpt = Integer.parseInt(updatedValues.get("nOpt")); + if (updatedValues.get("poolDeposit") != null && !updatedValues.get("poolDeposit").isEmpty()) + poolDeposit = new BigInteger(updatedValues.get("poolDeposit")); + if (updatedValues.get("protocolMajorVer") != null && !updatedValues.get("protocolMajorVer").isEmpty()) + protocolMajorVer = Integer.parseInt(updatedValues.get("protocolMajorVer")); + if (updatedValues.get("protocolMinorVer") != null && !updatedValues.get("protocolMinorVer").isEmpty()) + protocolMinorVer = Integer.parseInt(updatedValues.get("protocolMinorVer")); + if (updatedValues.get("monetaryExpansionRate") != null && !updatedValues.get("monetaryExpansionRate").isEmpty()) + monetaryExpansionRate = Float.parseFloat(updatedValues.get("monetaryExpansionRate")); + if (updatedValues.get("treasuryGrowthRate") != null && !updatedValues.get("treasuryGrowthRate").isEmpty()) + treasuryGrowthRate = Float.parseFloat(updatedValues.get("treasuryGrowthRate")); + if (updatedValues.get("collateralPercentage") != null && !updatedValues.get("collateralPercentage").isEmpty()) + collateralPercentage = Integer.parseInt(updatedValues.get("collateralPercentage")); + if (updatedValues.get("prMemNumerator") != null && !updatedValues.get("prMemNumerator").isEmpty()) + prMemNumerator = updatedValues.get("prMemNumerator"); + if (updatedValues.get("prMemDenominator") != null && !updatedValues.get("prMemDenominator").isEmpty()) + prMemDenominator = updatedValues.get("prMemDenominator"); + if (updatedValues.get("prStepsNumerator") != null && !updatedValues.get("prStepsNumerator").isEmpty()) + prStepsNumerator = updatedValues.get("prStepsNumerator"); + if (updatedValues.get("prStepsDenominator") != null && !updatedValues.get("prStepsDenominator").isEmpty()) + prStepsDenominator = updatedValues.get("prStepsDenominator"); + if (updatedValues.get("lovelacePerUTxOWord") != null && !updatedValues.get("lovelacePerUTxOWord").isEmpty()) + lovelacePerUTxOWord = Long.parseLong(updatedValues.get("lovelacePerUTxOWord")); + if (updatedValues.get("maxBlockExUnitsMem") != null && !updatedValues.get("maxBlockExUnitsMem").isEmpty()) + maxBlockExUnitsMem = Long.parseLong(updatedValues.get("maxBlockExUnitsMem")); + if (updatedValues.get("maxBlockExUnitsSteps") != null && !updatedValues.get("maxBlockExUnitsSteps").isEmpty()) + maxBlockExUnitsSteps = Long.parseLong(updatedValues.get("maxBlockExUnitsSteps")); + if (updatedValues.get("maxCollateralInputs") != null && !updatedValues.get("maxCollateralInputs").isEmpty()) + maxCollateralInputs = Integer.parseInt(updatedValues.get("maxCollateralInputs")); + if (updatedValues.get("maxTxExUnitsMem") != null && !updatedValues.get("maxTxExUnitsMem").isEmpty()) + maxTxExUnitsMem = Long.parseLong(updatedValues.get("maxTxExUnitsMem")); + if (updatedValues.get("maxTxExUnitsSteps") != null && !updatedValues.get("maxTxExUnitsSteps").isEmpty()) + maxTxExUnitsSteps = Long.parseLong(updatedValues.get("maxTxExUnitsSteps")); + if (updatedValues.get("maxValueSize") != null && !updatedValues.get("maxValueSize").isEmpty()) + maxValueSize = Integer.parseInt(updatedValues.get("maxValueSize")); + if (updatedValues.get("committeeMinSize") != null && !updatedValues.get("committeeMinSize").isEmpty()) + committeeMinSize = Integer.parseInt(updatedValues.get("committeeMinSize")); + if (updatedValues.get("committeeMaxTermLength") != null && !updatedValues.get("committeeMaxTermLength").isEmpty()) + committeeMaxTermLength = Integer.parseInt(updatedValues.get("committeeMaxTermLength")); + if (updatedValues.get("govActionLifetime") != null && !updatedValues.get("govActionLifetime").isEmpty()) + govActionLifetime = Integer.parseInt(updatedValues.get("govActionLifetime")); + if (updatedValues.get("govActionDeposit") != null && !updatedValues.get("govActionDeposit").isEmpty()) + govActionDeposit = new BigInteger(updatedValues.get("govActionDeposit")); + if (updatedValues.get("dRepDeposit") != null && !updatedValues.get("dRepDeposit").isEmpty()) + dRepDeposit = new BigInteger(updatedValues.get("dRepDeposit")); + if (updatedValues.get("dRepActivity") != null && !updatedValues.get("dRepActivity").isEmpty()) + dRepActivity = Integer.parseInt(updatedValues.get("dRepActivity")); + if (updatedValues.get("minFeeRefScriptCostPerByte") != null && !updatedValues.get("minFeeRefScriptCostPerByte").isEmpty()) + minFeeRefScriptCostPerByte = new BigDecimal(updatedValues.get("minFeeRefScriptCostPerByte")); + if (updatedValues.get("constitutionUrl") != null && !updatedValues.get("constitutionUrl").isEmpty()) + constitutionUrl = updatedValues.get("constitutionUrl"); + if (updatedValues.get("constitutionDataHash") != null && !updatedValues.get("constitutionDataHash").isEmpty()) + constitutionDataHash = updatedValues.get("constitutionDataHash"); + if (updatedValues.get("constitutionScript") != null && !updatedValues.get("constitutionScript").trim().isEmpty()) + constitutionScript = updatedValues.get("constitutionScript"); + + if (updatedValues.get("shiftStartTimeBehind") != null && !updatedValues.get("shiftStartTimeBehind").isEmpty()) + shiftStartTimeBehind = Boolean.parseBoolean(updatedValues.get("shiftStartTimeBehind")); + if (updatedValues.get("conwayHardForkAtEpoch") != null && !updatedValues.get("conwayHardForkAtEpoch").isEmpty()) + conwayHardForkAtEpoch = Integer.parseInt(updatedValues.get("conwayHardForkAtEpoch")); + } + } + private List> createListWithLastFlag(Map faucets) { List> faucetList = new ArrayList<>(); int i = 0; diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/privnet/PrivNetService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/privnet/PrivNetService.java index 179deb5..54c2f28 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/privnet/PrivNetService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/privnet/PrivNetService.java @@ -4,6 +4,7 @@ import com.bloxbean.cardano.client.util.HexUtil; import com.bloxbean.cardano.yacicli.localcluster.ClusterConfig; import com.bloxbean.cardano.yacicli.localcluster.ClusterInfo; +import com.bloxbean.cardano.yacicli.localcluster.config.CustomGenesisConfig; import com.bloxbean.cardano.yacicli.localcluster.config.GenesisConfig; import com.bloxbean.cardano.yacicli.localcluster.config.GenesisUtil; import com.bloxbean.cardano.yacicli.localcluster.peer.PoolConfig; @@ -44,6 +45,9 @@ public class PrivNetService { @Autowired private GenesisConfig genesisConfig; + @Autowired + private CustomGenesisConfig customGenesisConfig; + @Autowired private PoolKeyGeneratorService poolKeyGeneratorService; @@ -76,6 +80,7 @@ private void updateGenesisScript(Path destPath, String clusterName, ClusterInfo Path genCreateScript = destPath.resolve("genesis-scripts").resolve("genesis-create.sh"); GenesisConfig genesisConfigCopy = genesisConfig.copy(); + genesisConfigCopy.merge(customGenesisConfig.getMap()); if (clusterInfo.getGenesisProfile() != null) genesisConfigCopy = GenesisProfile.applyGenesisProfile(clusterInfo.getGenesisProfile(), genesisConfigCopy);