From 674048a12088ff8ac5c9485c9cea0b0ec7a4e2d2 Mon Sep 17 00:00:00 2001 From: Satya Date: Sun, 1 Mar 2026 13:51:39 +0800 Subject: [PATCH 01/17] chore: upgrade Spring Boot 3.5.11, Spring AI 1.1.2, Yaci 0.4.0, cardano-client-lib 0.7.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Upgrade Spring Boot 3.2.5 → 3.5.11, Spring Shell 3.2.4 → 3.4.1 - Upgrade Spring AI 1.0.3 → 1.1.2 with STATELESS MCP protocol - Upgrade Yaci 0.3.3 → 0.4.0, cardano-client-lib 0.6.2 → 0.7.1 - Update LocalProtocolParamSupplier for Yaci 0.4.0 Rational type changes - Remove deprecated legacy_testnet network from BlockStreamerService - Update MCP endpoint URL from /sse to /mcp (streamable HTTP transport) - Suppress noisy MCP and Spring AI warnings from CLI console output - Handle NoResourceFoundException at DEBUG level to avoid ERROR stack traces - Fix deprecated logback syntax to remove config parsing warnings - Remove duplicate MCP properties from Docker application.properties - Update Sonatype repository URL to new central endpoint --- README.md | 2 +- applications/cli/build.gradle | 20 ++++++++--------- .../cli/docker/application.properties | 5 ----- .../controller/GlobalExceptionHandler.java | 15 +++++++++++++ .../commands/tail/BlockStreamerService.java | 17 -------------- .../service/LocalProtocolParamSupplier.java | 12 +++++----- .../cli/src/main/resources/application.yml | 4 ++++ .../cli/src/main/resources/logback-spring.xml | 22 ++++++++++++++++--- mcp.json.example | 2 +- 9 files changed, 55 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index a9cce593..1395002c 100644 --- a/README.md +++ b/README.md @@ -246,7 +246,7 @@ Copy [`mcp.json.example`](./mcp.json.example) to `.mcp.json` in your project roo { "mcpServers": { "yaci-devkit": { - "url": "http://localhost:10000/sse" + "url": "http://localhost:10000/mcp" } } } diff --git a/applications/cli/build.gradle b/applications/cli/build.gradle index d188d61a..511a74e4 100644 --- a/applications/cli/build.gradle +++ b/applications/cli/build.gradle @@ -1,6 +1,6 @@ plugins { - id 'org.springframework.boot' version '3.2.5' - id 'io.spring.dependency-management' version '1.1.4' + id 'org.springframework.boot' version '3.5.11' + id 'io.spring.dependency-management' version '1.1.7' id 'org.graalvm.buildtools.native' version '0.10.2' id 'java' id 'signing' @@ -18,17 +18,16 @@ java { } repositories { - //maven { url 'https://repo.spring.io/release' } mavenCentral() mavenLocal() maven { - url "https://oss.sonatype.org/content/repositories/snapshots" + url "https://central.sonatype.com/repository/maven-snapshots/" } } ext { - set('springShellVersion', "3.2.4") - set('springAiVersion', "1.0.3") + set('springShellVersion', "3.4.1") + set('springAiVersion', "1.1.2") } dependencies { @@ -39,13 +38,12 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-logging' implementation 'org.springframework.boot:spring-boot-starter-mustache' - implementation('com.bloxbean.cardano:yaci:0.3.3') { + implementation('com.bloxbean.cardano:yaci:0.4.0') { exclude group: 'com.bloxbean.cardano', module: 'cardano-client-core' } - implementation 'com.bloxbean.cardano:cardano-client-lib:0.6.2' - implementation 'com.bloxbean.cardano:cardano-client-backend:0.6.2' - implementation 'com.bloxbean.cardano:cardano-client-backend-blockfrost:0.6.2' - //implementation 'com.bloxbean.cardano:cardano-client-supplier-local:0.5.1' + implementation 'com.bloxbean.cardano:cardano-client-lib:0.7.1' + implementation 'com.bloxbean.cardano:cardano-client-backend:0.7.1' + implementation 'com.bloxbean.cardano:cardano-client-backend-blockfrost:0.7.1' implementation 'org.apache.commons:commons-compress:1.23.0' diff --git a/applications/cli/docker/application.properties b/applications/cli/docker/application.properties index 81cc1311..9e80581f 100644 --- a/applications/cli/docker/application.properties +++ b/applications/cli/docker/application.properties @@ -9,8 +9,3 @@ pool.keys.home=/clusters/pool-keys spring.shell.config.location=/clusters server.port=10000 -# MCP Server -spring.ai.mcp.server.enabled=true -spring.ai.mcp.server.name=yaci-devkit -spring.ai.mcp.server.version=0.1.0 - diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/cip30/controller/GlobalExceptionHandler.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/cip30/controller/GlobalExceptionHandler.java index d8e729fd..81e332b6 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/cip30/controller/GlobalExceptionHandler.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/cip30/controller/GlobalExceptionHandler.java @@ -7,6 +7,7 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.resource.NoResourceFoundException; import java.util.HashMap; import java.util.Map; @@ -56,6 +57,20 @@ public ResponseEntity> handleIllegalArgumentException( return ResponseEntity.badRequest().body(response); } + /** + * Handle missing static resource requests (e.g., MCP client probing legacy endpoints) + */ + @ExceptionHandler(NoResourceFoundException.class) + public ResponseEntity> handleNoResourceFoundException( + NoResourceFoundException ex) { + Map response = new HashMap<>(); + response.put("code", HttpStatus.NOT_FOUND.value()); + response.put("message", "Resource not found"); + + log.debug("Resource not found: {}", ex.getMessage()); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); + } + /** * Handle generic exceptions */ diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/commands/tail/BlockStreamerService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/commands/tail/BlockStreamerService.java index 6014f25c..fc0333d6 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/commands/tail/BlockStreamerService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/commands/tail/BlockStreamerService.java @@ -221,23 +221,6 @@ private CliConnection getConnectionInfo(String host, int port, String network, l wellKnownPoint = new Point(slot, blockHash); else wellKnownPoint = Constants.WELL_KNOWN_MAINNET_POINT; - } else if ("legacy_testnet".equals(network)) { - if (!StringUtils.hasLength(host)) { - host = Constants.TESTNET_IOHK_RELAY_ADDR; - } - - if (port == 0) { - port = Constants.TESTNET_IOHK_RELAY_PORT; - } - - if (protocolMagic == 0) { - protocolMagic = Constants.LEGACY_TESTNET_PROTOCOL_MAGIC; - } - - if (slot != 0 && StringUtils.hasLength(blockHash)) - wellKnownPoint = new Point(slot, blockHash); - else - wellKnownPoint = Constants.WELL_KNOWN_TESTNET_POINT; } else if ("preprod".equals(network)) { if (!StringUtils.hasLength(host)) { host = Constants.PREPROD_IOHK_RELAY_ADDR; diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/LocalProtocolParamSupplier.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/LocalProtocolParamSupplier.java index c93b42ac..2fe79572 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/LocalProtocolParamSupplier.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/LocalProtocolParamSupplier.java @@ -62,10 +62,10 @@ public ProtocolParams getProtocolParams() { protocolParams.setPoolDeposit(String.valueOf(protocolParamUpdate.getPoolDeposit())); protocolParams.setEMax(protocolParamUpdate.getMaxEpoch()); protocolParams.setNOpt(protocolParamUpdate.getNOpt()); - protocolParams.setA0(protocolParamUpdate.getPoolPledgeInfluence()); - protocolParams.setRho(protocolParamUpdate.getExpansionRate()); - protocolParams.setTau(protocolParamUpdate.getTreasuryGrowthRate()); - protocolParams.setDecentralisationParam(protocolParamUpdate.getDecentralisationParam()); //Deprecated. Not there + protocolParams.setA0(protocolParamUpdate.getPoolPledgeInfluence() != null? protocolParamUpdate.getPoolPledgeInfluence().safeRatio(): null); + protocolParams.setRho(protocolParamUpdate.getExpansionRate() != null? protocolParamUpdate.getExpansionRate().safeRatio(): null); + protocolParams.setTau(protocolParamUpdate.getTreasuryGrowthRate() != null? protocolParamUpdate.getTreasuryGrowthRate().safeRatio(): null); + protocolParams.setDecentralisationParam(protocolParamUpdate.getDecentralisationParam() != null? protocolParamUpdate.getDecentralisationParam().safeRatio(): null); //Deprecated. Not there //protocolParams.setExtraEntropy(protocolParamUpdate.getExtraEntropy()); //TODO protocolParams.setProtocolMajorVer(protocolParamUpdate.getProtocolMajorVer()); protocolParams.setProtocolMinorVer(protocolParamUpdate.getProtocolMinorVer()); @@ -90,8 +90,8 @@ public ProtocolParams getProtocolParams() { protocolParams.setCostModels(costModels); - protocolParams.setPriceMem(protocolParamUpdate.getPriceMem()); - protocolParams.setPriceStep(protocolParamUpdate.getPriceStep()); + protocolParams.setPriceMem(protocolParamUpdate.getPriceMem() != null? protocolParamUpdate.getPriceMem().safeRatio(): null); + protocolParams.setPriceStep(protocolParamUpdate.getPriceStep() != null? protocolParamUpdate.getPriceStep().safeRatio(): null); protocolParams.setMaxTxExMem(String.valueOf(protocolParamUpdate.getMaxTxExMem())); protocolParams.setMaxTxExSteps(String.valueOf(protocolParamUpdate.getMaxTxExSteps())); protocolParams.setMaxBlockExMem(String.valueOf(protocolParamUpdate.getMaxBlockExMem())); diff --git a/applications/cli/src/main/resources/application.yml b/applications/cli/src/main/resources/application.yml index 1a5864dc..cafbb323 100644 --- a/applications/cli/src/main/resources/application.yml +++ b/applications/cli/src/main/resources/application.yml @@ -12,11 +12,15 @@ spring: location: classpath:/banner.txt devtools: add-properties:logging: false + shell: + interactive: + enabled: true ai: mcp: server: name: yaci-devkit version: 0.1.0 + protocol: STATELESS logging: level: diff --git a/applications/cli/src/main/resources/logback-spring.xml b/applications/cli/src/main/resources/logback-spring.xml index 48237cbd..c0baa8a7 100644 --- a/applications/cli/src/main/resources/logback-spring.xml +++ b/applications/cli/src/main/resources/logback-spring.xml @@ -8,9 +8,9 @@ ${LOG_FILE} - - - + ${LOG_FILE}.%i + 1 + 20 10MB @@ -21,6 +21,22 @@ %clr(%d){faint} %clr(${PID:- }){magenta} %clr([%15.15t]){magenta} %clr(%-5p) %clr(%-40c{40}){cyan} - %m%n + + + + + + + + + + + + + + + + diff --git a/mcp.json.example b/mcp.json.example index fc429501..d868beb7 100644 --- a/mcp.json.example +++ b/mcp.json.example @@ -1,7 +1,7 @@ { "mcpServers": { "yaci-devkit": { - "url": "http://localhost:10000/sse" + "url": "http://localhost:10000/mcp" } } } From 6b36cb813a381ecbea2b5bdf814c56b1fdea4ea9 Mon Sep 17 00:00:00 2001 From: Satya Date: Mon, 6 Apr 2026 21:39:34 +0800 Subject: [PATCH 02/17] chore: default protocol version to 10, auto-select Plutus cost models, fix springdoc compatibility - Change default protocol version from 11 to 10 for Conway era devnets - Add protocolMajorVer to ClusterInfo for persisting resolved version - Auto-select plutus-costmodels-v10.json or plutus-costmodels-v11.json based on protocolMajorVer - Upgrade springdoc-openapi from 2.5.0 to 2.8.6 to fix Swagger UI with Spring Boot 3.5.x - Update Earthfile, docker-compose, and docker config for versioned cost model files --- applications/cli/.dockerignore | 2 +- applications/cli/Earthfile | 21 +- applications/cli/build.gradle | 6 +- .../cli/config/application.properties | 3 + applications/cli/config/download.properties | 10 +- applications/cli/config/node.properties | 4 +- .../cli/config/plutus-costmodels-v10.json | 646 ++++++++++++++++++ .../cli/config/plutus-costmodels-v11.json | 73 ++ .../cli/docker/application.properties | 2 +- applications/cli/docker/download-amd64.sh | 4 +- applications/cli/docker/download-arm64.sh | 4 +- applications/cli/docker/download-ogmios.sh | 2 +- .../commands/common/DownloadService.java | 14 +- .../yacicli/localcluster/ClusterInfo.java | 2 + .../yacicli/localcluster/ClusterService.java | 15 +- .../localcluster/config/GenesisConfig.java | 28 + .../localcluster/service/AccountService.java | 49 +- .../service/LocalNodeService.java | 213 ++++++ .../service/LocalProtocolParamSupplier.java | 13 +- .../genesis-templates/alonzo-genesis.json | 13 +- .../genesis-templates/conway-genesis.json | 48 +- .../devnet/genesis-templates/spec/config.json | 1 + .../templates/devnet/submit-api-config.yaml | 106 +-- .../devnet/templates/configuration.json | 1 + .../devnet/templates/configuration.yaml | 1 + config/version | 2 +- scripts/docker-compose.yml | 2 + 27 files changed, 1101 insertions(+), 184 deletions(-) create mode 100644 applications/cli/config/plutus-costmodels-v10.json create mode 100644 applications/cli/config/plutus-costmodels-v11.json diff --git a/applications/cli/.dockerignore b/applications/cli/.dockerignore index 0312f06b..a70be4c4 100644 --- a/applications/cli/.dockerignore +++ b/applications/cli/.dockerignore @@ -3,4 +3,4 @@ build/libs/yaci-cli-*-plain.jar yaci-store-*-sources.jar yaci-store-*-plain.jar yaci-store-*-javadoc.jar - +wallet-ui/node_modules/ diff --git a/applications/cli/Earthfile b/applications/cli/Earthfile index fc57a66a..6efa85f1 100644 --- a/applications/cli/Earthfile +++ b/applications/cli/Earthfile @@ -1,5 +1,14 @@ VERSION 0.8 +wallet-ui-build: + FROM node:20-slim + WORKDIR /app + COPY wallet-ui/package.json wallet-ui/package-lock.json ./ + RUN npm ci + COPY wallet-ui/ ./ + RUN npx vite build --outDir dist + SAVE ARTIFACT dist /wallet + cli-java: ARG EARTHLY_TARGET_NAME ARG EARTHLY_GIT_SHORT_HASH @@ -7,6 +16,7 @@ cli-java: FROM eclipse-temurin:21 COPY . . + COPY +wallet-ui-build/wallet src/main/resources/static/wallet RUN echo git.commit.id.abbrev=${EARTHLY_GIT_SHORT_HASH} > src/main/resources/git.properties RUN cat src/main/resources/git.properties @@ -22,6 +32,7 @@ cli-native: FROM ghcr.io/graalvm/graalvm-community:21 COPY . . + COPY +wallet-ui-build/wallet src/main/resources/static/wallet RUN echo git.commit.id.abbrev=${EARTHLY_GIT_SHORT_HASH} > src/main/resources/git.properties RUN cat src/main/resources/git.properties @@ -49,8 +60,8 @@ java-setup: docker-build: FROM ubuntu:22.04 ENV JAVA_HOME=/opt/java/openjdk - ENV STORE_VERSION=0.1.0 - ENV STORE_NATIVE_BRANCH=release/2.0.0-beta3 + ENV STORE_VERSION=2.0.0 + ENV STORE_NATIVE_BRANCH=release/2.0.x ARG TARGETOS ARG TARGETARCH @@ -94,7 +105,9 @@ docker-build: RUN mkdir -p /app/store/config COPY docker/store-application.properties /app/store/config/application.properties - RUN wget https://github.com/bloxbean/yaci-store/releases/download/v${STORE_VERSION}/yaci-store-all-${STORE_VERSION}.jar -O /app/store/yaci-store.jar + RUN wget https://github.com/bloxbean/yaci-store/releases/download/v${STORE_VERSION}/yaci-store-${STORE_VERSION}.zip -O /app/store/yaci-store.zip + RUN unzip /app/store/yaci-store.zip -d /app/store/ + RUN cp /app/store/yaci-store-${STORE_VERSION}/yaci-store.jar /app/store/yaci-store.jar RUN echo ${APP_VERSION} > /app/version @@ -116,6 +129,8 @@ docker-build: RUN mkdir -p /app/config COPY docker/application.properties /app/config/ + COPY docker/plutus-costmodels-v10.json /app/config/ + COPY docker/plutus-costmodels-v11.json /app/config/ ENV PATH="$PATH:/app/cardano-bin" ENV CARDANO_NODE_SOCKET_PATH=/clusters/nodes/default/node/node.sock diff --git a/applications/cli/build.gradle b/applications/cli/build.gradle index 511a74e4..e665dd9c 100644 --- a/applications/cli/build.gradle +++ b/applications/cli/build.gradle @@ -34,7 +34,7 @@ dependencies { implementation 'org.springframework.shell:spring-shell-starter' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc' - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.6' implementation 'org.springframework.boot:spring-boot-starter-logging' implementation 'org.springframework.boot:spring-boot-starter-mustache' @@ -102,6 +102,7 @@ tasks.register('walletUiInstall', Exec) { commandLine 'bash', '-c', 'npm install' inputs.file('wallet-ui/package.json') outputs.dir('wallet-ui/node_modules') + onlyIf { !file('src/main/resources/static/wallet/index.html').exists() } } tasks.register('walletUiBuild', Exec) { @@ -114,6 +115,7 @@ tasks.register('walletUiBuild', Exec) { inputs.file('wallet-ui/vite.config.ts') inputs.file('wallet-ui/tailwind.config.cjs') outputs.dir('src/main/resources/static/wallet') + onlyIf { !file('src/main/resources/static/wallet/index.html').exists() } } processResources { @@ -137,7 +139,7 @@ task cliZip(type: Zip) { from('config') { into(configDir) - include '**/*.properties' + include '**/*.properties', '**/*.json' } include 'yaci-cli*' diff --git a/applications/cli/config/application.properties b/applications/cli/config/application.properties index b56712ac..07a0d6e4 100644 --- a/applications/cli/config/application.properties +++ b/applications/cli/config/application.properties @@ -7,6 +7,9 @@ server.port=10000 #Default is the user_home/.yaci-cli #yaci.cli.home=/Users/satya/yacicli +#Path to directory containing Plutus cost models JSON files (plutus-costmodels-v10.json, plutus-costmodels-v11.json) +yaci.cli.plutus-costmodels-path=./config + ogmios.enabled=false kupo.enabled=false yaci.store.enabled=false diff --git a/applications/cli/config/download.properties b/applications/cli/config/download.properties index 63830bd9..ba46dda2 100644 --- a/applications/cli/config/download.properties +++ b/applications/cli/config/download.properties @@ -1,11 +1,11 @@ #Please specify either the version or the full url for the following components -node.version=10.5.0 -ogmios.version=6.13.0 +node.version=10.6.2 +ogmios.version=6.14.0 kupo.version=2.11.0 -yaci.store.tag=rel-native-2.0.0-beta3 -yaci.store.version=2.0.0-beta3 -yaci.store.jar.version=2.0.0-beta3 +yaci.store.tag=rel-native-2.0.0 +yaci.store.version=2.0.0 +yaci.store.jar.version=2.0.0 #node.url= #ogmios.url= diff --git a/applications/cli/config/node.properties b/applications/cli/config/node.properties index d5a3d7c4..02f65ae1 100644 --- a/applications/cli/config/node.properties +++ b/applications/cli/config/node.properties @@ -65,7 +65,9 @@ #dvtPPGovGroup=0.51f #dvtTreasuryWithdrawal=0.51f -#committeeMinSize=0 +committeeMinSize=0 +ccThresholdNumerator=0 +ccThresholdDenominator=1 #committeeMaxTermLength=200 #govActionLifetime=10 #govActionDeposit=1000000000 diff --git a/applications/cli/config/plutus-costmodels-v10.json b/applications/cli/config/plutus-costmodels-v10.json new file mode 100644 index 00000000..f07d0a17 --- /dev/null +++ b/applications/cli/config/plutus-costmodels-v10.json @@ -0,0 +1,646 @@ +{ + "PlutusV1": [ + 100788, + 420, + 1, + 1, + 1000, + 173, + 0, + 1, + 1000, + 59957, + 4, + 1, + 11183, + 32, + 201305, + 8356, + 4, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 100, + 100, + 16000, + 100, + 94375, + 32, + 132994, + 32, + 61462, + 4, + 72010, + 178, + 0, + 1, + 22151, + 32, + 91189, + 769, + 4, + 2, + 85848, + 228465, + 122, + 0, + 1, + 1, + 1000, + 42921, + 4, + 2, + 24548, + 29498, + 38, + 1, + 898148, + 27279, + 1, + 51775, + 558, + 1, + 39184, + 1000, + 60594, + 1, + 141895, + 32, + 83150, + 32, + 15299, + 32, + 76049, + 1, + 13169, + 4, + 22100, + 10, + 28999, + 74, + 1, + 28999, + 74, + 1, + 43285, + 552, + 1, + 44749, + 541, + 1, + 33852, + 32, + 68246, + 32, + 72362, + 32, + 7243, + 32, + 7391, + 32, + 11546, + 32, + 85848, + 228465, + 122, + 0, + 1, + 1, + 90434, + 519, + 0, + 1, + 74433, + 32, + 85848, + 228465, + 122, + 0, + 1, + 1, + 85848, + 228465, + 122, + 0, + 1, + 1, + 270652, + 22588, + 4, + 1457325, + 64566, + 4, + 20467, + 1, + 4, + 0, + 141992, + 32, + 100788, + 420, + 1, + 1, + 81663, + 32, + 59498, + 32, + 20142, + 32, + 24588, + 32, + 20744, + 32, + 25933, + 32, + 24623, + 32, + 53384111, + 14333, + 10 + ], + "PlutusV2": [ + 100788, + 420, + 1, + 1, + 1000, + 173, + 0, + 1, + 1000, + 59957, + 4, + 1, + 11183, + 32, + 201305, + 8356, + 4, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 100, + 100, + 16000, + 100, + 94375, + 32, + 132994, + 32, + 61462, + 4, + 72010, + 178, + 0, + 1, + 22151, + 32, + 91189, + 769, + 4, + 2, + 85848, + 228465, + 122, + 0, + 1, + 1, + 1000, + 42921, + 4, + 2, + 24548, + 29498, + 38, + 1, + 898148, + 27279, + 1, + 51775, + 558, + 1, + 39184, + 1000, + 60594, + 1, + 141895, + 32, + 83150, + 32, + 15299, + 32, + 76049, + 1, + 13169, + 4, + 22100, + 10, + 28999, + 74, + 1, + 28999, + 74, + 1, + 43285, + 552, + 1, + 44749, + 541, + 1, + 33852, + 32, + 68246, + 32, + 72362, + 32, + 7243, + 32, + 7391, + 32, + 11546, + 32, + 85848, + 228465, + 122, + 0, + 1, + 1, + 90434, + 519, + 0, + 1, + 74433, + 32, + 85848, + 228465, + 122, + 0, + 1, + 1, + 85848, + 228465, + 122, + 0, + 1, + 1, + 955506, + 213312, + 0, + 2, + 270652, + 22588, + 4, + 1457325, + 64566, + 4, + 20467, + 1, + 4, + 0, + 141992, + 32, + 100788, + 420, + 1, + 1, + 81663, + 32, + 59498, + 32, + 20142, + 32, + 24588, + 32, + 20744, + 32, + 25933, + 32, + 24623, + 32, + 43053543, + 10, + 53384111, + 14333, + 10, + 43574283, + 26308, + 10 + ], + "PlutusV3": [ + 100788, + 420, + 1, + 1, + 1000, + 173, + 0, + 1, + 1000, + 59957, + 4, + 1, + 11183, + 32, + 201305, + 8356, + 4, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 100, + 100, + 16000, + 100, + 94375, + 32, + 132994, + 32, + 61462, + 4, + 72010, + 178, + 0, + 1, + 22151, + 32, + 91189, + 769, + 4, + 2, + 85848, + 123203, + 7305, + -900, + 1716, + 549, + 57, + 85848, + 0, + 1, + 1, + 1000, + 42921, + 4, + 2, + 24548, + 29498, + 38, + 1, + 898148, + 27279, + 1, + 51775, + 558, + 1, + 39184, + 1000, + 60594, + 1, + 141895, + 32, + 83150, + 32, + 15299, + 32, + 76049, + 1, + 13169, + 4, + 22100, + 10, + 28999, + 74, + 1, + 28999, + 74, + 1, + 43285, + 552, + 1, + 44749, + 541, + 1, + 33852, + 32, + 68246, + 32, + 72362, + 32, + 7243, + 32, + 7391, + 32, + 11546, + 32, + 85848, + 123203, + 7305, + -900, + 1716, + 549, + 57, + 85848, + 0, + 1, + 90434, + 519, + 0, + 1, + 74433, + 32, + 85848, + 123203, + 7305, + -900, + 1716, + 549, + 57, + 85848, + 0, + 1, + 1, + 85848, + 123203, + 7305, + -900, + 1716, + 549, + 57, + 85848, + 0, + 1, + 955506, + 213312, + 0, + 2, + 270652, + 22588, + 4, + 1457325, + 64566, + 4, + 20467, + 1, + 4, + 0, + 141992, + 32, + 100788, + 420, + 1, + 1, + 81663, + 32, + 59498, + 32, + 20142, + 32, + 24588, + 32, + 20744, + 32, + 25933, + 32, + 24623, + 32, + 43053543, + 10, + 53384111, + 14333, + 10, + 43574283, + 26308, + 10, + 16000, + 100, + 16000, + 100, + 962335, + 18, + 2780678, + 6, + 442008, + 1, + 52538055, + 3756, + 18, + 267929, + 18, + 76433006, + 8868, + 18, + 52948122, + 18, + 1995836, + 36, + 3227919, + 12, + 901022, + 1, + 166917843, + 4307, + 36, + 284546, + 36, + 158221314, + 26549, + 36, + 74698472, + 36, + 333849714, + 1, + 254006273, + 72, + 2174038, + 72, + 2261318, + 64571, + 4, + 207616, + 8310, + 4, + 1293828, + 28716, + 63, + 0, + 1, + 1006041, + 43623, + 251, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 107878, + 680, + 0, + 1, + 95336, + 1, + 281145, + 18848, + 0, + 1, + 180194, + 159, + 1, + 1, + 158519, + 8942, + 0, + 1, + 159378, + 8813, + 0, + 1, + 107490, + 3298, + 1, + 106057, + 655, + 1, + 1964219, + 24520, + 3 + ] +} diff --git a/applications/cli/config/plutus-costmodels-v11.json b/applications/cli/config/plutus-costmodels-v11.json new file mode 100644 index 00000000..cc725ecd --- /dev/null +++ b/applications/cli/config/plutus-costmodels-v11.json @@ -0,0 +1,73 @@ +{ + "PlutusV1": [ + 100788, 420, 1, 1, 1000, 173, 0, 1, 1000, 59957, + 4, 1, 11183, 32, 201305, 8356, 4, 16000, 100, 16000, + 100, 16000, 100, 16000, 100, 16000, 100, 16000, 100, 100, + 100, 16000, 100, 94375, 32, 132994, 32, 61462, 4, 72010, + 178, 0, 1, 22151, 32, 91189, 769, 4, 2, 85848, + 228465, 122, 0, 1, 1, 1000, 42921, 4, 2, 24548, + 29498, 38, 1, 898148, 27279, 1, 51775, 558, 1, 39184, + 1000, 60594, 1, 141895, 32, 83150, 32, 15299, 32, 76049, + 1, 13169, 4, 22100, 10, 28999, 74, 1, 28999, 74, + 1, 43285, 552, 1, 44749, 541, 1, 33852, 32, 68246, + 32, 72362, 32, 7243, 32, 7391, 32, 11546, 32, 85848, + 228465, 122, 0, 1, 1, 90434, 519, 0, 1, 74433, + 32, 85848, 228465, 122, 0, 1, 1, 85848, 228465, 122, + 0, 1, 1, 270652, 22588, 4, 1457325, 64566, 4, 20467, + 1, 4, 0, 141992, 32, 100788, 420, 1, 1, 81663, + 32, 59498, 32, 20142, 32, 24588, 32, 20744, 32, 25933, + 32, 24623, 32, 53384111, 14333, 10 + ], + "PlutusV2": [ + 100788, 420, 1, 1, 1000, 173, 0, 1, 1000, 59957, + 4, 1, 11183, 32, 201305, 8356, 4, 16000, 100, 16000, + 100, 16000, 100, 16000, 100, 16000, 100, 16000, 100, 100, + 100, 16000, 100, 94375, 32, 132994, 32, 61462, 4, 72010, + 178, 0, 1, 22151, 32, 91189, 769, 4, 2, 85848, + 228465, 122, 0, 1, 1, 1000, 42921, 4, 2, 24548, + 29498, 38, 1, 898148, 27279, 1, 51775, 558, 1, 39184, + 1000, 60594, 1, 141895, 32, 83150, 32, 15299, 32, 76049, + 1, 13169, 4, 22100, 10, 28999, 74, 1, 28999, 74, + 1, 43285, 552, 1, 44749, 541, 1, 33852, 32, 68246, + 32, 72362, 32, 7243, 32, 7391, 32, 11546, 32, 85848, + 228465, 122, 0, 1, 1, 90434, 519, 0, 1, 74433, + 32, 85848, 228465, 122, 0, 1, 1, 85848, 228465, 122, + 0, 1, 1, 955506, 213312, 0, 2, 270652, 22588, 4, + 1457325, 64566, 4, 20467, 1, 4, 0, 141992, 32, 100788, + 420, 1, 1, 81663, 32, 59498, 32, 20142, 32, 24588, + 32, 20744, 32, 25933, 32, 24623, 32, 43053543, 10, 53384111, + 14333, 10, 43574283, 26308, 10 + ], + "PlutusV3": [ + 100788, 420, 1, 1, 1000, 173, 0, 1, 1000, 59957, + 4, 1, 11183, 32, 201305, 8356, 4, 16000, 100, 16000, + 100, 16000, 100, 16000, 100, 16000, 100, 16000, 100, 100, + 100, 16000, 100, 94375, 32, 132994, 32, 61462, 4, 72010, + 178, 0, 1, 22151, 32, 91189, 769, 4, 2, 85848, + 123203, 7305, -900, 1716, 549, 57, 85848, 0, 1, 1, + 1000, 42921, 4, 2, 24548, 29498, 38, 1, 898148, 27279, + 1, 51775, 558, 1, 39184, 1000, 60594, 1, 141895, 32, + 83150, 32, 15299, 32, 76049, 1, 13169, 4, 22100, 10, + 28999, 74, 1, 28999, 74, 1, 43285, 552, 1, 44749, + 541, 1, 33852, 32, 68246, 32, 72362, 32, 7243, 32, + 7391, 32, 11546, 32, 85848, 123203, 7305, -900, 1716, 549, + 57, 85848, 0, 1, 90434, 519, 0, 1, 74433, 32, + 85848, 123203, 7305, -900, 1716, 549, 57, 85848, 0, 1, + 1, 85848, 123203, 7305, -900, 1716, 549, 57, 85848, 0, + 1, 955506, 213312, 0, 2, 270652, 22588, 4, 1457325, 64566, + 4, 20467, 1, 4, 0, 141992, 32, 100788, 420, 1, + 1, 81663, 32, 59498, 32, 20142, 32, 24588, 32, 20744, + 32, 25933, 32, 24623, 32, 43053543, 10, 53384111, 14333, 10, + 43574283, 26308, 10, 16000, 100, 16000, 100, 962335, 18, 2780678, + 6, 442008, 1, 52538055, 3756, 18, 267929, 18, 76433006, 8868, + 18, 52948122, 18, 1995836, 36, 3227919, 12, 901022, 1, 166917843, + 4307, 36, 284546, 36, 158221314, 26549, 36, 74698472, 36, 333849714, + 1, 254006273, 72, 2174038, 72, 2261318, 64571, 4, 207616, 8310, + 4, 1293828, 28716, 63, 0, 1, 1006041, 43623, 251, 0, + 1, 100181, 726, 719, 0, 1, 100181, 726, 719, 0, + 1, 100181, 726, 719, 0, 1, 107878, 680, 0, 1, + 95336, 1, 281145, 18848, 0, 1, 180194, 159, 1, 1, + 158519, 8942, 0, 1, 159378, 8813, 0, 1, 107490, 3298, + 1, 106057, 655, 1, 1964219, 24520, 3 + ] +} diff --git a/applications/cli/docker/application.properties b/applications/cli/docker/application.properties index 9e80581f..9de797f0 100644 --- a/applications/cli/docker/application.properties +++ b/applications/cli/docker/application.properties @@ -8,4 +8,4 @@ local.cluster.home=/clusters/nodes pool.keys.home=/clusters/pool-keys spring.shell.config.location=/clusters server.port=10000 - +yaci.cli.plutus-costmodels-path=/app/config diff --git a/applications/cli/docker/download-amd64.sh b/applications/cli/docker/download-amd64.sh index cdbdb738..05921d9d 100644 --- a/applications/cli/docker/download-amd64.sh +++ b/applications/cli/docker/download-amd64.sh @@ -1,5 +1,5 @@ -file=cardano-node-10.5.0-linux.tar.gz -wget https://github.com/IntersectMBO/cardano-node/releases/download/10.5.0/cardano-node-10.5.0-linux.tar.gz +file=cardano-node-10.6.2-linux-amd64.tar.gz +wget https://github.com/IntersectMBO/cardano-node/releases/download/10.6.2/cardano-node-10.6.2-linux-amd64.tar.gz mkdir /app/cardano-bin diff --git a/applications/cli/docker/download-arm64.sh b/applications/cli/docker/download-arm64.sh index 0d81adef..86866201 100755 --- a/applications/cli/docker/download-arm64.sh +++ b/applications/cli/docker/download-arm64.sh @@ -1,5 +1,5 @@ -file=cardano-10_5_0-aarch64-static-musl-ghc_9101.tar.zst -dir=cardano-10_5_0-aarch64-static-musl-ghc_9101 +file=cardano-10_6_2-aarch64-static-musl-ghc_9122.tar.zst +dir=cardano-10_5_0-aarch64-static-musl-ghc_9122 wget https://github.com/armada-alliance/cardano-node-binaries/raw/main/static-binaries/$file?raw=true -O - | tar -I zstd -xv #unzip $file diff --git a/applications/cli/docker/download-ogmios.sh b/applications/cli/docker/download-ogmios.sh index 53c80546..93f34bd9 100644 --- a/applications/cli/docker/download-ogmios.sh +++ b/applications/cli/docker/download-ogmios.sh @@ -15,7 +15,7 @@ case $1 in esac -version=v6.13.0 +version=v6.14.0 file=ogmios-${version}-${SUFFIX}-linux.zip wget https://github.com/CardanoSolutions/ogmios/releases/download/${version}/$file diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/commands/common/DownloadService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/commands/common/DownloadService.java index 1c13f7ad..28d210d8 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/commands/common/DownloadService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/commands/common/DownloadService.java @@ -483,10 +483,22 @@ private String resolveNodeDownloadPath() { writeLn(error("Unsupported OS : " + System.getProperty("os.name"))); } + String arch = System.getProperty("os.arch"); + String cpuArch = null; + if (arch.startsWith("aarch") || arch.startsWith("arm")) { + cpuArch = "arm64"; + } else{ + cpuArch = "amd64"; + } + + //Just a workaround, as 10.6.2 macos arm binary is not working. + if (osPrefix.equals("macos")) + cpuArch = "amd64"; + if (osPrefix == null) return null; - String url = NODE_DOWNLOAD_URL + "/" + nodeVersion + "/cardano-node-" + nodeVersion + "-" + osPrefix + ".tar.gz"; + String url = NODE_DOWNLOAD_URL + "/" + nodeVersion + "/cardano-node-" + nodeVersion + "-" + osPrefix + "-" + cpuArch + ".tar.gz"; return url; } diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterInfo.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterInfo.java index 8f1cc14c..7ced1e90 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterInfo.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterInfo.java @@ -43,6 +43,8 @@ public class ClusterInfo { @Builder.Default private int prometheusPort=12798; + private int protocolMajorVer; + private boolean localMultiNodeEnabled; private int localMultiNodeStakeRatioFactor; } 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 5b93fccf..93733695 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 @@ -343,10 +343,13 @@ private void updateGenesis(Path clusterFolder, String clusterName, ClusterInfo c srcConwayGenesisFile = clusterFolder.resolve("genesis-templates").resolve("conway-genesis.json"); } + Path srcDijkstraGenesisFile = clusterFolder.resolve("genesis-templates").resolve("dijkstra-genesis.json"); + Path destByronGenesisFile = clusterFolder.resolve("node").resolve("genesis").resolve("byron-genesis.json"); Path destShelleyGenesisFile = clusterFolder.resolve("node").resolve("genesis").resolve("shelley-genesis.json"); Path destAlonzoGenesisFile = clusterFolder.resolve("node").resolve("genesis").resolve("alonzo-genesis.json"); Path destConwayGenesisFile = clusterFolder.resolve("node").resolve("genesis").resolve("conway-genesis.json"); + Path destDijkstraGenesisFile = clusterFolder.resolve("node").resolve("genesis").resolve("dijkstra-genesis.json"); GenesisConfig genesisConfigCopy = genesisConfig.copy(); genesisConfigCopy.merge(customGenesisConfig.getMap()); @@ -375,11 +378,14 @@ private void updateGenesis(Path clusterFolder, String clusterName, ClusterInfo c values.put("activeSlotsCoeff", String.valueOf(activeSlotsCoeff)); values.put("epochLength", String.valueOf(epochLength)); - //Check if protocol version should be minimun 9 and it's conway era - if (era == Era.Conway && genesisConfigCopy.getProtocolMajorVer() < 9) { - values.put("protocolMajorVer", 10); - values.put("protocolMinorVer", 2); + //Check if protocol version should be minimum 9 and it's conway era + int resolvedProtocolMajorVer = genesisConfigCopy.getProtocolMajorVer(); + if (era == Era.Conway && resolvedProtocolMajorVer < 9) { + resolvedProtocolMajorVer = 10; + values.put("protocolMajorVer", resolvedProtocolMajorVer); + values.put("protocolMinorVer", 0); } + clusterInfo.setProtocolMajorVer(resolvedProtocolMajorVer); //Derive security param long securityParam = genesisConfigCopy.getSecurityParam(); @@ -407,6 +413,7 @@ private void updateGenesis(Path clusterFolder, String clusterName, ClusterInfo c templateEngineHelper.replaceValues(srcShelleyGenesisFile, destShelleyGenesisFile, values); templateEngineHelper.replaceValues(srcAlonzoGenesisFile, destAlonzoGenesisFile, values); templateEngineHelper.replaceValues(srcConwayGenesisFile, destConwayGenesisFile, values); + templateEngineHelper.replaceValues(srcDijkstraGenesisFile, destDijkstraGenesisFile, values); } catch (Exception e) { throw new IOException(e); } 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 4d20b6fd..c736563c 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 @@ -247,6 +247,12 @@ public class GenesisConfig { ); + //Dijkstra + private long maxRefScriptSizePerBlock = 1048576; + private long maxRefScriptSizePerTx = 204800; + private long refScriptCostStride = 25600; + private double refScriptCostMultiplier = 1.2; + //Introduced for the issue https://github.com/bloxbean/yaci-devkit/issues/65 private int conwayHardForkAtEpoch = 0; private boolean shiftStartTimeBehind = false; @@ -437,6 +443,12 @@ public Map getConfigMap() { map.put("conwayHardForkAtEpoch", conwayHardForkAtEpoch); map.put("shiftStartTimeBehind", shiftStartTimeBehind); + //Dijkstra + map.put("maxRefScriptSizePerBlock", maxRefScriptSizePerBlock); + map.put("maxRefScriptSizePerTx", maxRefScriptSizePerTx); + map.put("refScriptCostStride", refScriptCostStride); + map.put("refScriptCostMultiplier", refScriptCostMultiplier); + return map; } @@ -529,6 +541,12 @@ public GenesisConfig copy() { genesisConfig.setConwayHardForkAtEpoch(conwayHardForkAtEpoch); genesisConfig.setShiftStartTimeBehind(shiftStartTimeBehind); + //Dijkstra + genesisConfig.setMaxRefScriptSizePerBlock(maxRefScriptSizePerBlock); + genesisConfig.setMaxRefScriptSizePerTx(maxRefScriptSizePerTx); + genesisConfig.setRefScriptCostStride(refScriptCostStride); + genesisConfig.setRefScriptCostMultiplier(refScriptCostMultiplier); + return genesisConfig; } @@ -639,6 +657,16 @@ public void merge(Map updatedValues) { shiftStartTimeBehind = Boolean.parseBoolean(updatedValues.get("shiftStartTimeBehind")); if (updatedValues.get("conwayHardForkAtEpoch") != null && !updatedValues.get("conwayHardForkAtEpoch").isEmpty()) conwayHardForkAtEpoch = Integer.parseInt(updatedValues.get("conwayHardForkAtEpoch")); + + //Dijkstra + if (updatedValues.get("maxRefScriptSizePerBlock") != null && !updatedValues.get("maxRefScriptSizePerBlock").isEmpty()) + maxRefScriptSizePerBlock = Long.parseLong(updatedValues.get("maxRefScriptSizePerBlock")); + if (updatedValues.get("maxRefScriptSizePerTx") != null && !updatedValues.get("maxRefScriptSizePerTx").isEmpty()) + maxRefScriptSizePerTx = Long.parseLong(updatedValues.get("maxRefScriptSizePerTx")); + if (updatedValues.get("refScriptCostStride") != null && !updatedValues.get("refScriptCostStride").isEmpty()) + refScriptCostStride = Long.parseLong(updatedValues.get("refScriptCostStride")); + if (updatedValues.get("refScriptCostMultiplier") != null && !updatedValues.get("refScriptCostMultiplier").isEmpty()) + refScriptCostMultiplier = Double.parseDouble(updatedValues.get("refScriptCostMultiplier")); } } diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/AccountService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/AccountService.java index 6a340d47..f2d1da13 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/AccountService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/AccountService.java @@ -6,12 +6,14 @@ import com.bloxbean.cardano.yacicli.commands.common.RootLogService; import com.bloxbean.cardano.yacicli.localcluster.ClusterService; import com.bloxbean.cardano.yacicli.localcluster.common.LocalClientProviderHelper; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import java.io.IOException; import java.math.BigInteger; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collections; import java.util.List; import java.util.Map; @@ -21,12 +23,31 @@ import static com.bloxbean.cardano.yacicli.util.ConsoleWriter.writeLn; @Component -@RequiredArgsConstructor @Slf4j public class AccountService { private final ClusterService clusterService; private final LocalClientProviderHelper localQueryClientUtil; private final RootLogService rootLogService; + private final Path plutusCostModelsBasePath; + + public AccountService(ClusterService clusterService, + LocalClientProviderHelper localQueryClientUtil, + RootLogService rootLogService, + @Value("${yaci.cli.plutus-costmodels-path:./config}") String plutusCostModelsBasePath) { + this.clusterService = clusterService; + this.localQueryClientUtil = localQueryClientUtil; + this.rootLogService = rootLogService; + this.plutusCostModelsBasePath = Paths.get(plutusCostModelsBasePath); + } + + private Path resolveCostModelsFile(String clusterName) throws IOException { + var clusterInfo = clusterService.getClusterInfo(clusterName); + int protocolMajorVer = clusterInfo.getProtocolMajorVer(); + String fileName = protocolMajorVer >= 11 + ? "plutus-costmodels-v11.json" + : "plutus-costmodels-v10.json"; + return plutusCostModelsBasePath.resolve(fileName); + } public boolean topup(String clusterName, Era era, String address, double adaValue, Consumer writer) { Level orgLevel = rootLogService.getLogLevel(); @@ -74,6 +95,30 @@ public boolean mint(String clusterName, Era era, String assetName, BigInteger qu } } + public boolean updateCostModels(String clusterName, Era era, Consumer writer) { + Level orgLevel = rootLogService.getLogLevel(); + if (!rootLogService.isDebugLevel()) + rootLogService.setLogLevel(Level.OFF); + + LocalNodeService localNodeService = null; + try { + Path clusterFolder = clusterService.getClusterFolder(clusterName); + localNodeService = new LocalNodeService(clusterFolder, era, localQueryClientUtil, writer); + + Path costModelsFile = resolveCostModelsFile(clusterName); + writer.accept("Using Plutus cost models file: " + costModelsFile.getFileName()); + return localNodeService.updateCostModels(costModelsFile, msg -> writeLn(msg)); + } catch (Exception e) { + log.error("Error", e); + writer.accept(error("Plutus cost models update error: " + e.getMessage())); + return false; + } finally { + rootLogService.setLogLevel(orgLevel); + if (localNodeService != null) + localNodeService.shutdown(); + } + } + public Map> getUtxosAtDefaultAccounts(String clusterName, Era era, Consumer writer) { Level orgLevel = rootLogService.getLogLevel(); if (!rootLogService.isDebugLevel()) diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/LocalNodeService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/LocalNodeService.java index cac8b407..c3a257d6 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/LocalNodeService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/LocalNodeService.java @@ -2,9 +2,11 @@ import com.bloxbean.cardano.client.address.Address; import com.bloxbean.cardano.client.address.AddressProvider; +import com.bloxbean.cardano.client.address.Credential; import com.bloxbean.cardano.client.api.ProtocolParamsSupplier; import com.bloxbean.cardano.client.api.UtxoSupplier; import com.bloxbean.cardano.client.api.model.Amount; +import com.bloxbean.cardano.client.api.model.Result; import com.bloxbean.cardano.client.api.model.Utxo; import com.bloxbean.cardano.client.cip.cip20.MessageMetadata; import com.bloxbean.cardano.client.common.model.Networks; @@ -19,14 +21,30 @@ import com.bloxbean.cardano.client.function.helper.AuxDataProviders; import com.bloxbean.cardano.client.function.helper.InputBuilders; import com.bloxbean.cardano.client.function.helper.SignerProviders; +import com.bloxbean.cardano.client.api.impl.StaticTransactionEvaluator; +import com.bloxbean.cardano.client.plutus.spec.CostMdls; +import com.bloxbean.cardano.client.plutus.spec.CostModel; +import com.bloxbean.cardano.client.plutus.spec.ExUnits; +import com.bloxbean.cardano.client.plutus.spec.Language; import com.bloxbean.cardano.client.plutus.spec.PlutusData; +import com.bloxbean.cardano.client.plutus.spec.PlutusV3Script; import com.bloxbean.cardano.client.quicktx.QuickTxBuilder; +import com.bloxbean.cardano.client.quicktx.ScriptTx; import com.bloxbean.cardano.client.quicktx.Tx; import com.bloxbean.cardano.client.transaction.spec.Asset; +import com.bloxbean.cardano.client.transaction.spec.ProtocolParamUpdate; import com.bloxbean.cardano.client.transaction.spec.Transaction; +import com.bloxbean.cardano.client.transaction.spec.governance.Anchor; +import com.bloxbean.cardano.client.transaction.spec.governance.DRep; +import com.bloxbean.cardano.client.transaction.spec.governance.Vote; +import com.bloxbean.cardano.client.transaction.spec.governance.Voter; +import com.bloxbean.cardano.client.transaction.spec.governance.VoterType; +import com.bloxbean.cardano.client.transaction.spec.governance.actions.GovActionId; +import com.bloxbean.cardano.client.transaction.spec.governance.actions.ParameterChangeAction; import com.bloxbean.cardano.client.transaction.spec.script.ScriptPubkey; import com.bloxbean.cardano.client.transaction.util.TransactionUtil; import com.bloxbean.cardano.client.util.HexUtil; +import com.fasterxml.jackson.core.type.TypeReference; import com.bloxbean.cardano.yaci.core.common.TxBodyType; import com.bloxbean.cardano.yaci.core.protocol.chainsync.messages.Point; import com.bloxbean.cardano.yaci.core.protocol.localstate.api.Era; @@ -264,6 +282,201 @@ public boolean mint(String assetName, BigInteger quntity, String receiver, Consu } } + public boolean updateCostModels(Path costModelsFile, Consumer writer) { + try { + // 1. Find a funded UTXO key (same pattern as topUp) + BigInteger minBalance = BigInteger.valueOf(2_000_000_000L); // 2000 ADA to cover deposit + fees + String senderAddress = null; + SecretKey senderSkey = null; + + int i = 0; + for (Map.Entry> entry : getFundsAtGenesisKeys().entrySet()) { + String address = entry.getKey(); + Optional amountOptional = entry.getValue().stream() + .flatMap(utxo -> utxo.getAmount().stream()) + .filter(amt -> LOVELACE.equals(amt.getUnit()) && amt.getQuantity().compareTo(minBalance) > 0) + .findAny(); + if (amountOptional.isPresent()) { + senderAddress = address; + senderSkey = utxoKeys.get(i)._2; + break; + } + i++; + } + + if (senderAddress == null) { + writer.accept(error("No funded UTXO key found for governance proposal")); + return false; + } + + // 2. Load cost models from external config file + Map costModelMap = loadCostModels(costModelsFile); + + // 3. Build the ProtocolParamUpdate with all available cost models + CostMdls costMdls = new CostMdls(); + Map languageMap = Map.of( + "PlutusV1", Language.PLUTUS_V1, + "PlutusV2", Language.PLUTUS_V2, + "PlutusV3", Language.PLUTUS_V3 + ); + for (Map.Entry entry2 : costModelMap.entrySet()) { + Language language = languageMap.get(entry2.getKey()); + if (language != null) { + costMdls.add(new CostModel(language, entry2.getValue())); + } else { + writer.accept(error("Unknown Plutus version in cost models file: " + entry2.getKey())); + } + } + + ProtocolParamUpdate protocolParamUpdate = ProtocolParamUpdate.builder() + .costModels(costMdls) + .build(); + + // 4. Build the always-true PlutusV3 guardrail script + // CBOR hex: 46450101002499 -> script hash: 186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4 + PlutusV3Script guardrailScript = PlutusV3Script.builder() + .type("PlutusScriptV3") + .cborHex("46450101002499") + .build(); + + // 5. Build the ParameterChangeAction + ParameterChangeAction paramChangeAction = ParameterChangeAction.builder() + .prevGovActionId(null) + .protocolParamUpdate(protocolParamUpdate) + .policyHash(guardrailScript.getScriptHash()) + .build(); + + // 6. Build dummy anchor + Anchor anchor = Anchor.builder() + .anchorUrl("https://devkit.yaci.xyz/plutus-costmodel-update.json") + .anchorDataHash(new byte[32]) // zeroed hash for devnet + .build(); + + // 7. Build a reward address from the sender's verification key for deposit return + var senderVkey = KeyGenUtil.getPublicKeyFromPrivateKey(senderSkey); + HdPublicKey hdPublicKey = new HdPublicKey(); + hdPublicKey.setKeyData(senderVkey.getBytes()); + Address rewardAddr = AddressProvider.getRewardAddress(hdPublicKey, Networks.testnet()); + String rewardAccount = rewardAddr.toBech32(); + + // 7a. Register stake credential for the reward address + writer.accept("Registering stake credential for governance proposal..."); + var regProcessor = new LocalTransactionProcessor(localClientProvider.getTxSubmissionClient(), TxBodyType.CONWAY); + Tx regTx = new Tx() + .registerStakeAddress(rewardAddr) + .from(senderAddress); + Result regResult = new QuickTxBuilder(utxoSupplier, protocolParamsSupplier, regProcessor) + .compose(regTx) + .withSigner(SignerProviders.signerFrom(senderSkey)) + .complete(); + + if (!regResult.isSuccessful()) { + writer.accept(error("Stake credential registration failed: " + regResult.getResponse())); + return false; + } + writer.accept(success("Stake credential registered. Tx# : " + regResult.getValue())); + waitForTx(senderAddress, regResult.getValue(), writer); + + // 7b. Register DRep and delegate voting power for governance voting + writer.accept("Registering DRep and delegating voting power..."); + byte[] credHash = rewardAddr.getDelegationCredentialHash() + .orElseThrow(() -> new RuntimeException("Failed to get delegation credential hash")); + String credHashHex = HexUtil.encodeHexString(credHash); + Credential drepCredential = Credential.fromKey(credHashHex); + + var drepProcessor = new LocalTransactionProcessor(localClientProvider.getTxSubmissionClient(), TxBodyType.CONWAY); + Tx drepTx = new Tx() + .registerDRep(drepCredential) + .delegateVotingPowerTo(rewardAddr, DRep.addrKeyHash(credHashHex)) + .from(senderAddress); + Result drepResult = new QuickTxBuilder(utxoSupplier, protocolParamsSupplier, drepProcessor) + .compose(drepTx) + .withSigner(SignerProviders.signerFrom(senderSkey)) + .complete(); + + if (!drepResult.isSuccessful()) { + writer.accept(error("DRep registration failed: " + drepResult.getResponse())); + return false; + } + writer.accept(success("DRep registered and voting power delegated. Tx# : " + drepResult.getValue())); + waitForTx(senderAddress, drepResult.getValue(), writer); + + // 8. Build and submit the governance proposal using ScriptTx + var transactionProcessor = new LocalTransactionProcessor(localClientProvider.getTxSubmissionClient(), TxBodyType.CONWAY); + + ScriptTx scriptTx = new ScriptTx() + .createProposal(paramChangeAction, rewardAccount, anchor, PlutusData.unit()) + .attachProposingValidator(guardrailScript); + + // Use StaticTransactionEvaluator with fixed ex units for the always-true guardrail script + // since LocalTransactionProcessor doesn't support evaluateTx + var staticEvaluator = new StaticTransactionEvaluator( + List.of(ExUnits.builder() + .mem(BigInteger.valueOf(500000)) + .steps(BigInteger.valueOf(200000000)) + .build())); + + Result result = new QuickTxBuilder(utxoSupplier, protocolParamsSupplier, transactionProcessor) + .compose(scriptTx) + .feePayer(senderAddress) + .withSigner(SignerProviders.signerFrom(senderSkey)) + .withTxEvaluator(staticEvaluator) + .complete(); + + if (!result.isSuccessful()) { + writer.accept(error("Plutus cost models governance proposal failed: " + result.getResponse())); + return false; + } + + writer.accept(success("Plutus cost models governance proposal submitted. Tx# : " + result.getValue())); + + // 9. Vote YES on the proposal as DRep + writer.accept("Voting YES on Plutus cost models proposal..."); + waitForTx(senderAddress, result.getValue(), writer); + + Voter voter = Voter.builder() + .type(VoterType.DREP_KEY_HASH) + .credential(drepCredential) + .build(); + GovActionId govActionId = GovActionId.builder() + .transactionId(result.getValue()) + .govActionIndex(0) + .build(); + + var voteProcessor = new LocalTransactionProcessor(localClientProvider.getTxSubmissionClient(), TxBodyType.CONWAY); + Tx voteTx = new Tx() + .createVote(voter, govActionId, Vote.YES) + .from(senderAddress); + Result voteResult = new QuickTxBuilder(utxoSupplier, protocolParamsSupplier, voteProcessor) + .compose(voteTx) + .withSigner(SignerProviders.signerFrom(senderSkey)) + .complete(); + + if (!voteResult.isSuccessful()) { + writer.accept(error("DRep vote on proposal failed: " + voteResult.getResponse())); + return false; + } + writer.accept(success("DRep voted YES on Plutus cost models proposal. Tx# : " + voteResult.getValue())); + writer.accept(info("Plutus cost models will be enacted at the next epoch boundary.")); + return true; + + } catch (Exception e) { + log.error("Failed to submit Plutus cost models governance proposal", e); + writer.accept(error("Plutus cost models update failed: " + e.getMessage())); + return false; + } + } + + private Map loadCostModels(Path costModelsFile) throws IOException { + Map> raw = objectMapper.readValue(costModelsFile.toFile(), + new TypeReference>>() {}); + Map result = new HashMap<>(); + for (Map.Entry> entry : raw.entrySet()) { + result.put(entry.getKey(), entry.getValue().stream().mapToLong(Long::longValue).toArray()); + } + return result; + } + private boolean waitForTx(String receiver, String txHash, Consumer writer) { int count = 0; while (true) { diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/LocalProtocolParamSupplier.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/LocalProtocolParamSupplier.java index 2fe79572..f1c8a052 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/LocalProtocolParamSupplier.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/LocalProtocolParamSupplier.java @@ -78,7 +78,7 @@ public ProtocolParams getProtocolParams() { LinkedHashMap plutusV2CostModel = cborToCostModel(protocolParamUpdate.getCostModels().get(1), PlutusOps.getOperations(2)); LinkedHashMap plutusV3CostModel = null; - if (era == Era.Conway) { + if (era.getValue() >= Era.Conway.getValue()) { plutusV3CostModel = cborToCostModel(protocolParamUpdate.getCostModels().get(2), PlutusOps.getOperations(3)); } @@ -100,6 +100,17 @@ public ProtocolParams getProtocolParams() { protocolParams.setCollateralPercent(BigDecimal.valueOf(protocolParamUpdate.getCollateralPercent())); protocolParams.setMaxCollateralInputs(protocolParamUpdate.getMaxCollateralInputs()); protocolParams.setCoinsPerUtxoSize(String.valueOf(protocolParamUpdate.getAdaPerUtxoByte())); + + // Conway governance parameters + if (era.getValue() >= Era.Conway.getValue()) { + protocolParams.setGovActionDeposit(protocolParamUpdate.getGovActionDeposit()); + protocolParams.setDrepDeposit(protocolParamUpdate.getDrepDeposit()); + protocolParams.setDrepActivity(protocolParamUpdate.getDrepActivity()); + protocolParams.setCommitteeMinSize(protocolParamUpdate.getCommitteeMinSize()); + protocolParams.setCommitteeMaxTermLength(protocolParamUpdate.getCommitteeMaxTermLength()); + protocolParams.setGovActionLifetime(protocolParamUpdate.getGovActionLifetime()); + } + return protocolParams; } diff --git a/applications/cli/src/main/resources/localcluster/templates/devnet/genesis-templates/alonzo-genesis.json b/applications/cli/src/main/resources/localcluster/templates/devnet/genesis-templates/alonzo-genesis.json index 65771d96..ae7ee4c6 100644 --- a/applications/cli/src/main/resources/localcluster/templates/devnet/genesis-templates/alonzo-genesis.json +++ b/applications/cli/src/main/resources/localcluster/templates/devnet/genesis-templates/alonzo-genesis.json @@ -344,18 +344,7 @@ 10, 43574283, 26308, - 10{{^shiftStartTimeBehind}}, - 9999999999999, - 9999999999999, - 9999999999999, - 9999999999999, - 9999999999999, - 9999999999999, - 9999999999999, - 9999999999999, - 9999999999999, - 9999999999999 - {{/shiftStartTimeBehind}} + 10 ] }, diff --git a/applications/cli/src/main/resources/localcluster/templates/devnet/genesis-templates/conway-genesis.json b/applications/cli/src/main/resources/localcluster/templates/devnet/genesis-templates/conway-genesis.json index 4c069fab..d1208417 100644 --- a/applications/cli/src/main/resources/localcluster/templates/devnet/genesis-templates/conway-genesis.json +++ b/applications/cli/src/main/resources/localcluster/templates/devnet/genesis-templates/conway-genesis.json @@ -276,53 +276,7 @@ 43623, 251, 0, - 1, - 100181, - 726, - 719, - 0, - 1, - 100181, - 726, - 719, - 0, - 1, - 100181, - 726, - 719, - 0, - 1, - 107878, - 680, - 0, - 1, - 95336, - 1, - 281145, - 18848, - 0, - 1, - 180194, - 159, - 1, - 1, - 158519, - 8942, - 0, - 1, - 159378, - 8813, - 0, - 1, - 107490, - 3298, - 1, - 106057, - 655, - 1, - 1964219, - 24520, - 3 + 1 ], "constitution": { "anchor": { diff --git a/applications/cli/src/main/resources/localcluster/templates/devnet/genesis-templates/spec/config.json b/applications/cli/src/main/resources/localcluster/templates/devnet/genesis-templates/spec/config.json index be0ae270..70ec386b 100644 --- a/applications/cli/src/main/resources/localcluster/templates/devnet/genesis-templates/spec/config.json +++ b/applications/cli/src/main/resources/localcluster/templates/devnet/genesis-templates/spec/config.json @@ -3,6 +3,7 @@ "ShelleyGenesisFile": "shelley-genesis.json", "AlonzoGenesisFile": "alonzo-genesis.json", "ConwayGenesisFile": "conway-genesis.json", + "DijkstraGenesisFile": "dijkstra-genesis.json", "ApplicationName": "cardano-sl", "ApplicationVersion": 0, "LastKnownBlockVersion-Alt": 0, diff --git a/applications/cli/src/main/resources/localcluster/templates/devnet/submit-api-config.yaml b/applications/cli/src/main/resources/localcluster/templates/devnet/submit-api-config.yaml index 83529bf0..aa700b66 100644 --- a/applications/cli/src/main/resources/localcluster/templates/devnet/submit-api-config.yaml +++ b/applications/cli/src/main/resources/localcluster/templates/devnet/submit-api-config.yaml @@ -1,100 +1,10 @@ # Tx Submission Server Configuration -EnableLogMetrics: False -EnableLogging: True - -# ------------------------------------------------------------------------------ -# Logging configuration follows. - -# global filter; messages must have at least this severity to pass: -minSeverity: Info - -# global file rotation settings: -rotation: - rpLogLimitBytes: 5000000 - rpKeepFilesNum: 10 - rpMaxAgeHours: 24 - -# these backends are initialized: -setupBackends: - - AggregationBK - - KatipBK - # - EditorBK - # - EKGViewBK - -# if not indicated otherwise, then messages are passed to these backends: -defaultBackends: - - KatipBK - -# if wanted, the GUI is listening on this port: -# hasGUI: 12787 - -# if wanted, the EKG interface is listening on this port: -# hasEKG: 12788 - -# here we set up outputs of logging in 'katip': -setupScribes: - - scKind: StdoutSK - scName: stdout - scFormat: ScText - scRotation: null - -# if not indicated otherwise, then log output is directed to this: -defaultScribes: - - - StdoutSK - - stdout - -# more options which can be passed as key-value pairs: -options: - cfokey: - value: "Release-1.0.0" - mapSubtrace: - benchmark: - contents: - - GhcRtsStats - - MonotonicClock - subtrace: ObservableTrace - '#ekgview': - contents: - - - tag: Contains - contents: 'cardano.epoch-validation.benchmark' - - - tag: Contains - contents: .monoclock.basic. - - - tag: Contains - contents: 'cardano.epoch-validation.benchmark' - - - tag: Contains - contents: diff.RTS.cpuNs.timed. - - - tag: StartsWith - contents: '#ekgview.#aggregation.cardano.epoch-validation.benchmark' - - - tag: Contains - contents: diff.RTS.gcNum.timed. - subtrace: FilterTrace - 'cardano.epoch-validation.utxo-stats': - # Change the `subtrace` value to `Neutral` in order to log - # `UTxO`-related messages during epoch validation. - subtrace: NoTrace - '#messagecounters.aggregation': - subtrace: NoTrace - '#messagecounters.ekgview': - subtrace: NoTrace - '#messagecounters.switchboard': - subtrace: NoTrace - '#messagecounters.katip': - subtrace: NoTrace - '#messagecounters.monitoring': - subtrace: NoTrace - 'cardano.#messagecounters.aggregation': - subtrace: NoTrace - 'cardano.#messagecounters.ekgview': - subtrace: NoTrace - 'cardano.#messagecounters.switchboard': - subtrace: NoTrace - 'cardano.#messagecounters.katip': - subtrace: NoTrace - 'cardano.#messagecounters.monitoring': - subtrace: NoTrace - mapBackends: - cardano.epoch-validation.benchmark: - - AggregationBK - '#aggregation.cardano.epoch-validation.benchmark': - - EKGViewBK +UseTraceDispatcher: True + +TraceOptions: + "": + severity: Notice + detail: DNormal + backends: + - Stdout HumanFormatColoured diff --git a/applications/cli/src/main/resources/localcluster/templates/devnet/templates/configuration.json b/applications/cli/src/main/resources/localcluster/templates/devnet/templates/configuration.json index 750fa1e5..15be04c1 100644 --- a/applications/cli/src/main/resources/localcluster/templates/devnet/templates/configuration.json +++ b/applications/cli/src/main/resources/localcluster/templates/devnet/templates/configuration.json @@ -2,6 +2,7 @@ "AlonzoGenesisFile": "./genesis/alonzo-genesis.json", "ByronGenesisFile": "./genesis/byron-genesis.json", "ConwayGenesisFile": "./genesis/conway-genesis.json", + "DijkstraGenesisFile": "./genesis/dijkstra-genesis.json", "EnableP2P": {{enableP2P}}, "LastKnownBlockVersion-Alt": 0, "LastKnownBlockVersion-Major": {{#mainnet}}2{{/mainnet}}{{^mainnet}}2{{/mainnet}}, diff --git a/applications/cli/src/main/resources/localcluster/templates/devnet/templates/configuration.yaml b/applications/cli/src/main/resources/localcluster/templates/devnet/templates/configuration.yaml index 11dd5dac..2b03942c 100644 --- a/applications/cli/src/main/resources/localcluster/templates/devnet/templates/configuration.yaml +++ b/applications/cli/src/main/resources/localcluster/templates/devnet/templates/configuration.yaml @@ -9,6 +9,7 @@ ByronGenesisFile: ./genesis/byron-genesis.json ShelleyGenesisFile: ./genesis/shelley-genesis.json AlonzoGenesisFile: ./genesis/alonzo-genesis.json ConwayGenesisFile: ./genesis/conway-genesis.json +DijkstraGenesisFile: ./genesis/dijkstra-genesis.json SocketPath: db/node.socket EnableP2P: {{enableP2P}} diff --git a/config/version b/config/version index f0644cc5..aae83034 100644 --- a/config/version +++ b/config/version @@ -1,2 +1,2 @@ -tag=0.11.0-beta1 +tag=0.11.0-beta2-dev revision= diff --git a/scripts/docker-compose.yml b/scripts/docker-compose.yml index f63b7f99..14d28d34 100644 --- a/scripts/docker-compose.yml +++ b/scripts/docker-compose.yml @@ -13,6 +13,8 @@ services: volumes: - cluster-data:/clusters - ../config/node.properties:/app/config/node.properties + - ../config/plutus-costmodels-v10.json:/app/config/plutus-costmodels-v10.json + - ../config/plutus-costmodels-v11.json:/app/config/plutus-costmodels-v11.json env_file: - ../config/env - ../config/node.properties From 6659d65a4fd5c8f82927d0282b024ea2fb42d6a9 Mon Sep 17 00:00:00 2001 From: Satya Date: Wed, 8 Apr 2026 19:49:57 +0800 Subject: [PATCH 03/17] - Introduced node modes : companion, haskell-only, yano-only --- .gitignore | 1 + .../cli/config/application.properties | 3 + applications/cli/config/download.properties | 6 +- .../cli/docker/plutus-costmodels-v10.json | 646 ++++++++++++++++++ .../cli/docker/plutus-costmodels-v11.json | 73 ++ .../yacicli/localcluster/ClusterCommands.java | 1 + .../yacicli/localcluster/ClusterConfig.java | 10 + .../yacicli/localcluster/ClusterInfo.java | 6 + .../localcluster/ClusterPortInfoHelper.java | 2 + .../localcluster/ClusterStartService.java | 91 ++- .../localcluster/common/GenesisUtil.java | 55 ++ .../localcluster/config/GenesisConfig.java | 7 + .../FirstRunPlutusV3CostModelUpdate.java | 65 ++ .../localcluster/service/AccountService.java | 7 +- .../service/LocalNodeService.java | 21 +- .../yano/YanoBootstrapService.java | 193 ++++++ .../yano/YanoCompanionService.java | 260 +++++++ .../localcluster/yano/YanoConfigBuilder.java | 91 +++ .../yano/YanoGovernanceService.java | 305 +++++++++ .../localcluster/yano/YanoService.java | 372 ++++++++++ .../genesis-templates/dijkstra-genesis.json | 6 + 21 files changed, 2194 insertions(+), 27 deletions(-) create mode 100644 applications/cli/docker/plutus-costmodels-v10.json create mode 100644 applications/cli/docker/plutus-costmodels-v11.json create mode 100644 applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/common/GenesisUtil.java create mode 100644 applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/events/listeners/FirstRunPlutusV3CostModelUpdate.java create mode 100644 applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoBootstrapService.java create mode 100644 applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoCompanionService.java create mode 100644 applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoConfigBuilder.java create mode 100644 applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoGovernanceService.java create mode 100644 applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoService.java create mode 100644 applications/cli/src/main/resources/localcluster/templates/devnet/genesis-templates/dijkstra-genesis.json diff --git a/.gitignore b/.gitignore index 03a4fb14..3694443b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ applications/store-build/tmp /examples/wallet-demo/test-results/ applications/cli/wallet-ui/node_modules/ applications/cli/src/main/resources/static/wallet/ +/applications/cli/chainstate/ diff --git a/applications/cli/config/application.properties b/applications/cli/config/application.properties index 07a0d6e4..7cd0066b 100644 --- a/applications/cli/config/application.properties +++ b/applications/cli/config/application.properties @@ -13,6 +13,7 @@ yaci.cli.plutus-costmodels-path=./config ogmios.enabled=false kupo.enabled=false yaci.store.enabled=false +yano.enabled=false yaci.store.mode=native @@ -24,6 +25,8 @@ bp.create.enabled=true #yaci.store.port=8080 #socat.port=3333 #prometheus.port=12798 +#yano.server.port=14447 +#yano.http.port=6666 ###################################################### diff --git a/applications/cli/config/download.properties b/applications/cli/config/download.properties index ba46dda2..1059baaa 100644 --- a/applications/cli/config/download.properties +++ b/applications/cli/config/download.properties @@ -1,5 +1,5 @@ #Please specify either the version or the full url for the following components -node.version=10.6.2 +node.version=10.6.3 ogmios.version=6.14.0 kupo.version=2.11.0 @@ -7,8 +7,12 @@ yaci.store.tag=rel-native-2.0.0 yaci.store.version=2.0.0 yaci.store.jar.version=2.0.0 +yano.tag=v0.1.0-pre1 +yano.version=0.1.0-pre1 + #node.url= #ogmios.url= #kupo.url= #yaci.store.url= #yaci.store.jar.url= +#yano.url= diff --git a/applications/cli/docker/plutus-costmodels-v10.json b/applications/cli/docker/plutus-costmodels-v10.json new file mode 100644 index 00000000..f07d0a17 --- /dev/null +++ b/applications/cli/docker/plutus-costmodels-v10.json @@ -0,0 +1,646 @@ +{ + "PlutusV1": [ + 100788, + 420, + 1, + 1, + 1000, + 173, + 0, + 1, + 1000, + 59957, + 4, + 1, + 11183, + 32, + 201305, + 8356, + 4, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 100, + 100, + 16000, + 100, + 94375, + 32, + 132994, + 32, + 61462, + 4, + 72010, + 178, + 0, + 1, + 22151, + 32, + 91189, + 769, + 4, + 2, + 85848, + 228465, + 122, + 0, + 1, + 1, + 1000, + 42921, + 4, + 2, + 24548, + 29498, + 38, + 1, + 898148, + 27279, + 1, + 51775, + 558, + 1, + 39184, + 1000, + 60594, + 1, + 141895, + 32, + 83150, + 32, + 15299, + 32, + 76049, + 1, + 13169, + 4, + 22100, + 10, + 28999, + 74, + 1, + 28999, + 74, + 1, + 43285, + 552, + 1, + 44749, + 541, + 1, + 33852, + 32, + 68246, + 32, + 72362, + 32, + 7243, + 32, + 7391, + 32, + 11546, + 32, + 85848, + 228465, + 122, + 0, + 1, + 1, + 90434, + 519, + 0, + 1, + 74433, + 32, + 85848, + 228465, + 122, + 0, + 1, + 1, + 85848, + 228465, + 122, + 0, + 1, + 1, + 270652, + 22588, + 4, + 1457325, + 64566, + 4, + 20467, + 1, + 4, + 0, + 141992, + 32, + 100788, + 420, + 1, + 1, + 81663, + 32, + 59498, + 32, + 20142, + 32, + 24588, + 32, + 20744, + 32, + 25933, + 32, + 24623, + 32, + 53384111, + 14333, + 10 + ], + "PlutusV2": [ + 100788, + 420, + 1, + 1, + 1000, + 173, + 0, + 1, + 1000, + 59957, + 4, + 1, + 11183, + 32, + 201305, + 8356, + 4, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 100, + 100, + 16000, + 100, + 94375, + 32, + 132994, + 32, + 61462, + 4, + 72010, + 178, + 0, + 1, + 22151, + 32, + 91189, + 769, + 4, + 2, + 85848, + 228465, + 122, + 0, + 1, + 1, + 1000, + 42921, + 4, + 2, + 24548, + 29498, + 38, + 1, + 898148, + 27279, + 1, + 51775, + 558, + 1, + 39184, + 1000, + 60594, + 1, + 141895, + 32, + 83150, + 32, + 15299, + 32, + 76049, + 1, + 13169, + 4, + 22100, + 10, + 28999, + 74, + 1, + 28999, + 74, + 1, + 43285, + 552, + 1, + 44749, + 541, + 1, + 33852, + 32, + 68246, + 32, + 72362, + 32, + 7243, + 32, + 7391, + 32, + 11546, + 32, + 85848, + 228465, + 122, + 0, + 1, + 1, + 90434, + 519, + 0, + 1, + 74433, + 32, + 85848, + 228465, + 122, + 0, + 1, + 1, + 85848, + 228465, + 122, + 0, + 1, + 1, + 955506, + 213312, + 0, + 2, + 270652, + 22588, + 4, + 1457325, + 64566, + 4, + 20467, + 1, + 4, + 0, + 141992, + 32, + 100788, + 420, + 1, + 1, + 81663, + 32, + 59498, + 32, + 20142, + 32, + 24588, + 32, + 20744, + 32, + 25933, + 32, + 24623, + 32, + 43053543, + 10, + 53384111, + 14333, + 10, + 43574283, + 26308, + 10 + ], + "PlutusV3": [ + 100788, + 420, + 1, + 1, + 1000, + 173, + 0, + 1, + 1000, + 59957, + 4, + 1, + 11183, + 32, + 201305, + 8356, + 4, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 100, + 100, + 16000, + 100, + 94375, + 32, + 132994, + 32, + 61462, + 4, + 72010, + 178, + 0, + 1, + 22151, + 32, + 91189, + 769, + 4, + 2, + 85848, + 123203, + 7305, + -900, + 1716, + 549, + 57, + 85848, + 0, + 1, + 1, + 1000, + 42921, + 4, + 2, + 24548, + 29498, + 38, + 1, + 898148, + 27279, + 1, + 51775, + 558, + 1, + 39184, + 1000, + 60594, + 1, + 141895, + 32, + 83150, + 32, + 15299, + 32, + 76049, + 1, + 13169, + 4, + 22100, + 10, + 28999, + 74, + 1, + 28999, + 74, + 1, + 43285, + 552, + 1, + 44749, + 541, + 1, + 33852, + 32, + 68246, + 32, + 72362, + 32, + 7243, + 32, + 7391, + 32, + 11546, + 32, + 85848, + 123203, + 7305, + -900, + 1716, + 549, + 57, + 85848, + 0, + 1, + 90434, + 519, + 0, + 1, + 74433, + 32, + 85848, + 123203, + 7305, + -900, + 1716, + 549, + 57, + 85848, + 0, + 1, + 1, + 85848, + 123203, + 7305, + -900, + 1716, + 549, + 57, + 85848, + 0, + 1, + 955506, + 213312, + 0, + 2, + 270652, + 22588, + 4, + 1457325, + 64566, + 4, + 20467, + 1, + 4, + 0, + 141992, + 32, + 100788, + 420, + 1, + 1, + 81663, + 32, + 59498, + 32, + 20142, + 32, + 24588, + 32, + 20744, + 32, + 25933, + 32, + 24623, + 32, + 43053543, + 10, + 53384111, + 14333, + 10, + 43574283, + 26308, + 10, + 16000, + 100, + 16000, + 100, + 962335, + 18, + 2780678, + 6, + 442008, + 1, + 52538055, + 3756, + 18, + 267929, + 18, + 76433006, + 8868, + 18, + 52948122, + 18, + 1995836, + 36, + 3227919, + 12, + 901022, + 1, + 166917843, + 4307, + 36, + 284546, + 36, + 158221314, + 26549, + 36, + 74698472, + 36, + 333849714, + 1, + 254006273, + 72, + 2174038, + 72, + 2261318, + 64571, + 4, + 207616, + 8310, + 4, + 1293828, + 28716, + 63, + 0, + 1, + 1006041, + 43623, + 251, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 107878, + 680, + 0, + 1, + 95336, + 1, + 281145, + 18848, + 0, + 1, + 180194, + 159, + 1, + 1, + 158519, + 8942, + 0, + 1, + 159378, + 8813, + 0, + 1, + 107490, + 3298, + 1, + 106057, + 655, + 1, + 1964219, + 24520, + 3 + ] +} diff --git a/applications/cli/docker/plutus-costmodels-v11.json b/applications/cli/docker/plutus-costmodels-v11.json new file mode 100644 index 00000000..cc725ecd --- /dev/null +++ b/applications/cli/docker/plutus-costmodels-v11.json @@ -0,0 +1,73 @@ +{ + "PlutusV1": [ + 100788, 420, 1, 1, 1000, 173, 0, 1, 1000, 59957, + 4, 1, 11183, 32, 201305, 8356, 4, 16000, 100, 16000, + 100, 16000, 100, 16000, 100, 16000, 100, 16000, 100, 100, + 100, 16000, 100, 94375, 32, 132994, 32, 61462, 4, 72010, + 178, 0, 1, 22151, 32, 91189, 769, 4, 2, 85848, + 228465, 122, 0, 1, 1, 1000, 42921, 4, 2, 24548, + 29498, 38, 1, 898148, 27279, 1, 51775, 558, 1, 39184, + 1000, 60594, 1, 141895, 32, 83150, 32, 15299, 32, 76049, + 1, 13169, 4, 22100, 10, 28999, 74, 1, 28999, 74, + 1, 43285, 552, 1, 44749, 541, 1, 33852, 32, 68246, + 32, 72362, 32, 7243, 32, 7391, 32, 11546, 32, 85848, + 228465, 122, 0, 1, 1, 90434, 519, 0, 1, 74433, + 32, 85848, 228465, 122, 0, 1, 1, 85848, 228465, 122, + 0, 1, 1, 270652, 22588, 4, 1457325, 64566, 4, 20467, + 1, 4, 0, 141992, 32, 100788, 420, 1, 1, 81663, + 32, 59498, 32, 20142, 32, 24588, 32, 20744, 32, 25933, + 32, 24623, 32, 53384111, 14333, 10 + ], + "PlutusV2": [ + 100788, 420, 1, 1, 1000, 173, 0, 1, 1000, 59957, + 4, 1, 11183, 32, 201305, 8356, 4, 16000, 100, 16000, + 100, 16000, 100, 16000, 100, 16000, 100, 16000, 100, 100, + 100, 16000, 100, 94375, 32, 132994, 32, 61462, 4, 72010, + 178, 0, 1, 22151, 32, 91189, 769, 4, 2, 85848, + 228465, 122, 0, 1, 1, 1000, 42921, 4, 2, 24548, + 29498, 38, 1, 898148, 27279, 1, 51775, 558, 1, 39184, + 1000, 60594, 1, 141895, 32, 83150, 32, 15299, 32, 76049, + 1, 13169, 4, 22100, 10, 28999, 74, 1, 28999, 74, + 1, 43285, 552, 1, 44749, 541, 1, 33852, 32, 68246, + 32, 72362, 32, 7243, 32, 7391, 32, 11546, 32, 85848, + 228465, 122, 0, 1, 1, 90434, 519, 0, 1, 74433, + 32, 85848, 228465, 122, 0, 1, 1, 85848, 228465, 122, + 0, 1, 1, 955506, 213312, 0, 2, 270652, 22588, 4, + 1457325, 64566, 4, 20467, 1, 4, 0, 141992, 32, 100788, + 420, 1, 1, 81663, 32, 59498, 32, 20142, 32, 24588, + 32, 20744, 32, 25933, 32, 24623, 32, 43053543, 10, 53384111, + 14333, 10, 43574283, 26308, 10 + ], + "PlutusV3": [ + 100788, 420, 1, 1, 1000, 173, 0, 1, 1000, 59957, + 4, 1, 11183, 32, 201305, 8356, 4, 16000, 100, 16000, + 100, 16000, 100, 16000, 100, 16000, 100, 16000, 100, 100, + 100, 16000, 100, 94375, 32, 132994, 32, 61462, 4, 72010, + 178, 0, 1, 22151, 32, 91189, 769, 4, 2, 85848, + 123203, 7305, -900, 1716, 549, 57, 85848, 0, 1, 1, + 1000, 42921, 4, 2, 24548, 29498, 38, 1, 898148, 27279, + 1, 51775, 558, 1, 39184, 1000, 60594, 1, 141895, 32, + 83150, 32, 15299, 32, 76049, 1, 13169, 4, 22100, 10, + 28999, 74, 1, 28999, 74, 1, 43285, 552, 1, 44749, + 541, 1, 33852, 32, 68246, 32, 72362, 32, 7243, 32, + 7391, 32, 11546, 32, 85848, 123203, 7305, -900, 1716, 549, + 57, 85848, 0, 1, 90434, 519, 0, 1, 74433, 32, + 85848, 123203, 7305, -900, 1716, 549, 57, 85848, 0, 1, + 1, 85848, 123203, 7305, -900, 1716, 549, 57, 85848, 0, + 1, 955506, 213312, 0, 2, 270652, 22588, 4, 1457325, 64566, + 4, 20467, 1, 4, 0, 141992, 32, 100788, 420, 1, + 1, 81663, 32, 59498, 32, 20142, 32, 24588, 32, 20744, + 32, 25933, 32, 24623, 32, 43053543, 10, 53384111, 14333, 10, + 43574283, 26308, 10, 16000, 100, 16000, 100, 962335, 18, 2780678, + 6, 442008, 1, 52538055, 3756, 18, 267929, 18, 76433006, 8868, + 18, 52948122, 18, 1995836, 36, 3227919, 12, 901022, 1, 166917843, + 4307, 36, 284546, 36, 158221314, 26549, 36, 74698472, 36, 333849714, + 1, 254006273, 72, 2174038, 72, 2261318, 64571, 4, 207616, 8310, + 4, 1293828, 28716, 63, 0, 1, 1006041, 43623, 251, 0, + 1, 100181, 726, 719, 0, 1, 100181, 726, 719, 0, + 1, 100181, 726, 719, 0, 1, 107878, 680, 0, 1, + 95336, 1, 281145, 18848, 0, 1, 180194, 159, 1, 1, + 158519, 8942, 0, 1, 159378, 8813, 0, 1, 107490, 3298, + 1, 106057, 655, 1, 1964219, 24520, 3 + ] +} diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterCommands.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterCommands.java index 63ce2613..fe03beac 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterCommands.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterCommands.java @@ -168,6 +168,7 @@ else if (era.equalsIgnoreCase("conway")) .yaciStorePort(yaciStorePort) .socatPort(socatPort) .prometheusPort(prometheusPort) + .nodeMode(genesisConfig.getNodeMode()) .localMultiNodeEnabled(enableMultiNode) .localMultiNodeStakeRatioFactor(stakeRatioFactor) .build(); diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterConfig.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterConfig.java index a522f1ba..4438f3da 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterConfig.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterConfig.java @@ -51,6 +51,9 @@ public class ClusterConfig { @Value("${kupo.folder:#{null}}") private String kupoFolder; + @Value("${yano.folder:#{null}}") + private String yanoFolder; + @Value("${jre.folder:#{null}}") private String jreFolder; @@ -135,6 +138,13 @@ public Path getGenesisKeysFolder(String clusterName) { return Path.of(getGenesisKeysHome(), clusterName); } + public String getYanoHome() { + if (yanoFolder == null || !StringUtils.hasLength(yanoFolder.trim())) + return Path.of(getYaciCliHome(), COMPONENTS, "yano").toAbsolutePath().toString(); + else + return Path.of(yanoFolder).toAbsolutePath().toString(); + } + public String getJreHome() { if (jreFolder == null || !StringUtils.hasLength(jreFolder.trim())) return Path.of(getYaciCliHome(), COMPONENTS, "jre").toAbsolutePath().toString(); diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterInfo.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterInfo.java index 7ced1e90..808c6f6c 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterInfo.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterInfo.java @@ -44,6 +44,12 @@ public class ClusterInfo { private int prometheusPort=12798; private int protocolMajorVer; + private String nodeMode; + + @Builder.Default + private int yanoServerPort = 14447; + @Builder.Default + private int yanoHttpPort = 6666; private boolean localMultiNodeEnabled; private int localMultiNodeStakeRatioFactor; diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterPortInfoHelper.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterPortInfoHelper.java index 4ba5997f..9df1c7fc 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterPortInfoHelper.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterPortInfoHelper.java @@ -38,6 +38,8 @@ public void printUrls(String clusteName, ClusterInfo clusterInfo) { writeLn(infoLabel("Epoch Length", String.valueOf(clusterInfo.getEpochLength()))); writeLn(infoLabel("Security Param", String.valueOf(clusterInfo.getSecurityParam()))); writeLn(infoLabel("SlotsPerKESPeriod", String.valueOf(clusterInfo.getSlotsPerKESPeriod()))); + String nodeMode = clusterInfo.getNodeMode() != null ? clusterInfo.getNodeMode() : "haskell-only"; + writeLn(infoLabel("Node Mode", nodeMode)); if (clusteName == null || !"default".equals(clusteName)) return; 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 ef9763fc..baa0f25d 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 @@ -8,6 +8,7 @@ import com.bloxbean.cardano.yacicli.localcluster.events.FirstRunDone; import com.bloxbean.cardano.yacicli.localcluster.model.RunStatus; import com.bloxbean.cardano.yacicli.localcluster.peer.LocalPeerService; +import com.bloxbean.cardano.yacicli.localcluster.yano.YanoCompanionService; import com.bloxbean.cardano.yacicli.util.PortUtil; import com.bloxbean.cardano.yacicli.util.ProcessUtil; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; @@ -47,6 +48,7 @@ public class ClusterStartService { private final GenesisConfig genesisConfig; private final CustomGenesisConfig customGenesisConfig; private final LocalPeerService localPeerService; + private final YanoCompanionService yanoCompanionService; private ObjectMapper objectMapper = new ObjectMapper(); private List processes = new ArrayList<>(); @@ -69,9 +71,27 @@ public RunStatus startCluster(ClusterInfo clusterInfo, Path clusterFolder, Consu try { boolean firstRun = checkIfFirstRun(clusterFolder); + boolean companionMode = "companion".equals(clusterInfo.getNodeMode()); + boolean yanoOnlyMode = "yano-only".equals(clusterInfo.getNodeMode()); + boolean companionBootstrapDone = false; + if (clusterInfo.isMasterNode() && firstRun) setupFirstRun(clusterInfo, clusterFolder, writer); + // Companion mode: run Yano bootstrap before Haskell node + if (companionMode && firstRun) { + companionBootstrapDone = yanoCompanionService.bootstrap(clusterInfo, clusterFolder, writer); + if (!companionBootstrapDone) { + writer.accept(warn("Yano bootstrap failed. Continuing with haskell-only mode.")); + } + } + + if (yanoOnlyMode) { + // Yano-only: start Yano without past-time-travel, skip Haskell node entirely + // TODO: Full yano-only mode implementation (start Yano, fund, update cost models) + writer.accept(warn("yano-only mode is not yet fully implemented. Falling back to haskell-only.")); + } + if (clusterInfo.isLocalMultiNodeEnabled()) { String clusterName = CommandContext.INSTANCE.getProperty(ClusterConfig.CLUSTER_NAME); if (firstRun) { @@ -81,7 +101,10 @@ public RunStatus startCluster(ClusterInfo clusterInfo, Path clusterFolder, Consu localPeerService.handleClusterStarted(new ClusterStarted(clusterName)); } - Process nodeProcess = startNode(clusterFolder, clusterInfo, writer); + // In companion mode, start as relay first (no forging) to sync Yano's chain cleanly. + // A block producer would forge its own block at slot 1 (activeSlotsCoeff=1.0), + // creating a divergent chain that prevents chain-sync from Yano. + Process nodeProcess = startNode(clusterFolder, clusterInfo, companionBootstrapDone, writer); if (nodeProcess == null) { writer.accept(error("Node process could not be started.")); return new RunStatus(false, firstRun); @@ -97,6 +120,36 @@ public RunStatus startCluster(ClusterInfo clusterInfo, Path clusterFolder, Consu if (submitApiProcess != null) processes.add(submitApiProcess); + // Companion mode handover: sync as relay, stop Yano, restart as block producer + if (companionBootstrapDone) { + yanoCompanionService.performHandover(clusterInfo, clusterFolder, writer); + + // Stop relay node, restart as block producer (picks up synced chain from db) + writer.accept(info("Restarting Haskell node as block producer...")); + if (nodeProcess.isAlive()) { + nodeProcess.descendants().forEach(ProcessHandle::destroyForcibly); + nodeProcess.destroyForcibly(); + nodeProcess.waitFor(15, TimeUnit.SECONDS); + } + processes.remove(nodeProcess); + + // Remove stale socket so the BP instance can create a fresh one + Path socketPath = clusterFolder.resolve(ClusterConfig.NODE_FOLDER_PREFIX).resolve("node.sock"); + Files.deleteIfExists(socketPath); + + nodeProcess = startNode(clusterFolder, clusterInfo, false, writer); + if (nodeProcess != null) { + processes.add(nodeProcess); + writer.accept(success("Haskell node restarted as block producer.")); + } else { + writer.accept(error("Failed to restart node as block producer.")); + } + } + + // In companion mode, still report firstRun=true so FirstRunDone fires + // (topup + cost model governance proposals run against the Haskell node). + // The Haskell node syncs Yano's epoch history, so it starts at a later epoch + // and governance enactment happens faster. return new RunStatus(true, firstRun); } catch (IOException e) { throw new RuntimeException(e); @@ -204,10 +257,17 @@ public void showSubmitApiLogs(Consumer consumer) { } } - private Process startNode(Path clusterFolder, ClusterInfo clusterInfo, Consumer writer) throws IOException, InterruptedException, ExecutionException, TimeoutException { + private Process startNode(Path clusterFolder, ClusterInfo clusterInfo, boolean asRelay, Consumer writer) throws IOException, InterruptedException, ExecutionException, TimeoutException { String clusterFolderPath = clusterFolder.toAbsolutePath().toString(); String startScript = null; - if (clusterInfo.isMasterNode()) { + if (asRelay) { + // Companion mode: start as relay to sync from Yano, not as block producer + // Create relay script on the fly (node.sh minus pool key args) + Path nodeDir = clusterFolder.resolve(ClusterConfig.NODE_FOLDER_PREFIX); + createRelayScript(nodeDir); + startScript = ClusterConfig.NODE_RELAY_SCRIPT; + writer.accept(info("Starting Haskell node as relay (syncing from Yano)...")); + } else if (clusterInfo.isMasterNode()) { startScript = ClusterConfig.NODE_FOLDER_PREFIX; } else if (clusterInfo.isBlockProducer()) { startScript = ClusterConfig.NODE_BP_SCRIPT; @@ -322,6 +382,31 @@ public void saveClusterInfo(Path clusterFolder, ClusterInfo clusterInfo) throws objectMapper.writer(new DefaultPrettyPrinter()).writeValue(new File(clusterInfoPath), clusterInfo); } + /** + * Create a relay startup script (no block producer keys) from the existing node.sh. + * This removes --shelley-kes-key, --shelley-vrf-key, --shelley-operational-certificate, + * --byron-delegation-certificate, and --byron-signing-key flags. + */ + private void createRelayScript(Path nodeDir) throws IOException { + Path nodeScript = nodeDir.resolve("node.sh"); + Path relayScript = nodeDir.resolve(ClusterConfig.NODE_RELAY_SCRIPT + ".sh"); + + if (!nodeScript.toFile().exists()) return; + + String content = Files.readString(nodeScript); + // Remove block producer key arguments + content = content.replaceAll("--shelley-kes-key\\s+[^\\\\\\n]+\\\\?\\s*\n?", ""); + content = content.replaceAll("--shelley-vrf-key\\s+[^\\\\\\n]+\\\\?\\s*\n?", ""); + content = content.replaceAll("--shelley-operational-certificate\\s+[^\\\\\\n]+\\\\?\\s*\n?", ""); + content = content.replaceAll("--byron-delegation-certificate\\s+[^\\\\\\n]+\\\\?\\s*\n?", ""); + content = content.replaceAll("--byron-signing-key\\s+[^\\\\\\n]+\\\\?\\s*\n?", ""); + // Clean up any double blank lines + content = content.replaceAll("\n{3,}", "\n"); + + Files.writeString(relayScript, content); + relayScript.toFile().setExecutable(true); + } + public boolean checkIfFirstRun(Path clusterFolder) { String node1Folder = ClusterConfig.NODE_FOLDER_PREFIX; Path db = clusterFolder.resolve(node1Folder).resolve("db"); diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/common/GenesisUtil.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/common/GenesisUtil.java new file mode 100644 index 00000000..2516a10e --- /dev/null +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/common/GenesisUtil.java @@ -0,0 +1,55 @@ +package com.bloxbean.cardano.yacicli.localcluster.common; + +import com.bloxbean.cardano.client.crypto.SecretKey; +import com.bloxbean.cardano.client.crypto.VerificationKey; +import com.bloxbean.cardano.yacicli.common.Tuple; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Shared utility methods for genesis UTXO keys and cost model loading, + * used by both LocalNodeService and YanoGovernanceService. + */ +public class GenesisUtil { + private static final String UTXO_KEYS_FOLDER = "utxo-keys"; + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public static List> loadUtxoKeys(Path clusterFolder) throws IOException { + List> keys = new ArrayList<>(); + Path utxoFolder = clusterFolder.resolve(UTXO_KEYS_FOLDER); + + for (int i = 1; i <= 3; i++) { + Path skeyPath = utxoFolder.resolve("utxo" + i + ".skey"); + SecretKey skey = objectMapper.readValue(skeyPath.toFile(), SecretKey.class); + + Path vkeyPath = utxoFolder.resolve("utxo" + i + ".vkey"); + VerificationKey vkey = objectMapper.readValue(vkeyPath.toFile(), VerificationKey.class); + keys.add(new Tuple<>(vkey, skey)); + } + return keys; + } + + public static Map loadCostModels(Path costModelsFile) throws IOException { + Map> raw = objectMapper.readValue(costModelsFile.toFile(), + new TypeReference>>() {}); + Map result = new HashMap<>(); + for (Map.Entry> entry : raw.entrySet()) { + result.put(entry.getKey(), entry.getValue().stream().mapToLong(Long::longValue).toArray()); + } + return result; + } + + public static Path resolveCostModelsFile(Path basePath, int protocolMajorVer) { + String fileName = protocolMajorVer >= 11 + ? "plutus-costmodels-v11.json" + : "plutus-costmodels-v10.json"; + return basePath.resolve(fileName); + } +} 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 c736563c..96babf4f 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 @@ -253,6 +253,10 @@ public class GenesisConfig { private long refScriptCostStride = 25600; private double refScriptCostMultiplier = 1.2; + // Node mode: companion (Yano bootstraps + Haskell takes over), + // yano-only (Yano only, fastest), haskell-only (legacy, Haskell node only) + private String nodeMode = "haskell-only"; + //Introduced for the issue https://github.com/bloxbean/yaci-devkit/issues/65 private int conwayHardForkAtEpoch = 0; private boolean shiftStartTimeBehind = false; @@ -538,6 +542,7 @@ public GenesisConfig copy() { genesisConfig.setGenesisDelegs(new ArrayList<>(genesisDelegs)); genesisConfig.setNonAvvmBalances(new ArrayList<>(nonAvvmBalances)); + genesisConfig.setNodeMode(nodeMode); genesisConfig.setConwayHardForkAtEpoch(conwayHardForkAtEpoch); genesisConfig.setShiftStartTimeBehind(shiftStartTimeBehind); @@ -653,6 +658,8 @@ public void merge(Map updatedValues) { if (updatedValues.get("constitutionScript") != null && !updatedValues.get("constitutionScript").trim().isEmpty()) constitutionScript = updatedValues.get("constitutionScript"); + if (updatedValues.get("nodeMode") != null && !updatedValues.get("nodeMode").isEmpty()) + nodeMode = updatedValues.get("nodeMode"); if (updatedValues.get("shiftStartTimeBehind") != null && !updatedValues.get("shiftStartTimeBehind").isEmpty()) shiftStartTimeBehind = Boolean.parseBoolean(updatedValues.get("shiftStartTimeBehind")); if (updatedValues.get("conwayHardForkAtEpoch") != null && !updatedValues.get("conwayHardForkAtEpoch").isEmpty()) diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/events/listeners/FirstRunPlutusV3CostModelUpdate.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/events/listeners/FirstRunPlutusV3CostModelUpdate.java new file mode 100644 index 00000000..898f7dad --- /dev/null +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/events/listeners/FirstRunPlutusV3CostModelUpdate.java @@ -0,0 +1,65 @@ +package com.bloxbean.cardano.yacicli.localcluster.events.listeners; + +import com.bloxbean.cardano.yaci.core.protocol.localstate.api.Era; +import com.bloxbean.cardano.yacicli.localcluster.ClusterService; +import com.bloxbean.cardano.yacicli.localcluster.events.FirstRunDone; +import com.bloxbean.cardano.yacicli.localcluster.service.AccountService; +import com.bloxbean.cardano.yacicli.localcluster.service.ClusterUtilService; +import com.bloxbean.cardano.yacicli.common.AnsiColors; +import com.bloxbean.cardano.yacicli.common.CommandContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.function.Consumer; + +import static com.bloxbean.cardano.yacicli.util.ConsoleWriter.*; + +@Component +@RequiredArgsConstructor +@Slf4j +public class FirstRunPlutusV3CostModelUpdate { + private final ClusterService localClusterService; + private final ClusterUtilService clusterUtilService; + private final AccountService accountService; + + @EventListener + @Order(100) // Run after FirstRunTopupAccounts + public void updatePlutusV3CostModel(FirstRunDone firstRunDone) { + Consumer writer = msg -> writeLn(msg); + try { + String clusterName = firstRunDone.getCluster(); + var clusterInfo = localClusterService.getClusterInfo(clusterName); + if (clusterInfo != null && !clusterInfo.isMasterNode()) { + return; + } + + if (clusterInfo != null && "companion".equals(clusterInfo.getNodeMode())) { + writeLn(info("Skipping Plutus cost models update - already submitted via Yano companion bootstrap")); + return; + } + + Era era = CommandContext.INSTANCE.getEra(); + if (era != Era.Conway) { + log.debug("Skipping Plutus cost models update - not Conway era"); + return; + } + + writeLn(header(AnsiColors.CYAN_BOLD, "Submitting Plutus cost models governance proposal...")); + clusterUtilService.waitForNextBlocks(1, writer); + + boolean success = accountService.updateCostModels(clusterName, era, writer); + if (success) { + writeLn(success("Plutus cost models will be enacted from epoch 1 onward")); + } else { + writeLn(error("Plutus cost models governance proposal failed. Extended builtins may not be available.")); + } + + } catch (Exception e) { + log.error("Plutus cost models update failed", e); + writeLn(error("Plutus cost models update failed: " + e.getMessage())); + } + } +} diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/AccountService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/AccountService.java index f2d1da13..1574be82 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/AccountService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/AccountService.java @@ -5,6 +5,7 @@ import com.bloxbean.cardano.yaci.core.protocol.localstate.api.Era; import com.bloxbean.cardano.yacicli.commands.common.RootLogService; import com.bloxbean.cardano.yacicli.localcluster.ClusterService; +import com.bloxbean.cardano.yacicli.localcluster.common.GenesisUtil; import com.bloxbean.cardano.yacicli.localcluster.common.LocalClientProviderHelper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -42,11 +43,7 @@ public AccountService(ClusterService clusterService, private Path resolveCostModelsFile(String clusterName) throws IOException { var clusterInfo = clusterService.getClusterInfo(clusterName); - int protocolMajorVer = clusterInfo.getProtocolMajorVer(); - String fileName = protocolMajorVer >= 11 - ? "plutus-costmodels-v11.json" - : "plutus-costmodels-v10.json"; - return plutusCostModelsBasePath.resolve(fileName); + return GenesisUtil.resolveCostModelsFile(plutusCostModelsBasePath, clusterInfo.getProtocolMajorVer()); } public boolean topup(String clusterName, Era era, String address, double adaValue, Consumer writer) { diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/LocalNodeService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/LocalNodeService.java index c3a257d6..18936fa5 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/LocalNodeService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/LocalNodeService.java @@ -54,6 +54,7 @@ import com.bloxbean.cardano.yaci.core.protocol.localtx.messages.MsgRejectTx; import com.bloxbean.cardano.yaci.core.protocol.localtx.model.TxSubmissionRequest; import com.bloxbean.cardano.yaci.helper.LocalClientProvider; +import com.bloxbean.cardano.yacicli.localcluster.common.GenesisUtil; import com.bloxbean.cardano.yacicli.localcluster.common.LocalClientProviderHelper; import com.bloxbean.cardano.yacicli.common.Tuple; import com.fasterxml.jackson.databind.ObjectMapper; @@ -113,17 +114,7 @@ public void txRejected(TxSubmissionRequest txSubmissionRequest, MsgRejectTx msgR private void loadUtxoKeys(Path clusterFolder) throws IOException { utxoKeys.clear(); - Path utxoFolder = clusterFolder.resolve(UTXO_KEYS_FOLDER); - - for (int i = 1; i <= 3; i++) { - Path skeyPath = utxoFolder.resolve("utxo" + i + ".skey"); - SecretKey skey = objectMapper.readValue(skeyPath.toFile(), SecretKey.class); - - Path vkeyPath = utxoFolder.resolve("utxo" + i + ".vkey"); - VerificationKey vkey = objectMapper.readValue(vkeyPath.toFile(), VerificationKey.class); - utxoKeys.add(new Tuple<>(vkey, skey)); - } - + utxoKeys.addAll(GenesisUtil.loadUtxoKeys(clusterFolder)); } public Map> getFundsAtGenesisKeys() { @@ -468,13 +459,7 @@ public boolean updateCostModels(Path costModelsFile, Consumer writer) { } private Map loadCostModels(Path costModelsFile) throws IOException { - Map> raw = objectMapper.readValue(costModelsFile.toFile(), - new TypeReference>>() {}); - Map result = new HashMap<>(); - for (Map.Entry> entry : raw.entrySet()) { - result.put(entry.getKey(), entry.getValue().stream().mapToLong(Long::longValue).toArray()); - } - return result; + return GenesisUtil.loadCostModels(costModelsFile); } private boolean waitForTx(String receiver, String txHash, Consumer writer) { diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoBootstrapService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoBootstrapService.java new file mode 100644 index 00000000..eb2c0f9a --- /dev/null +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoBootstrapService.java @@ -0,0 +1,193 @@ +package com.bloxbean.cardano.yacicli.localcluster.yano; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Map; +import java.util.function.Consumer; + +import static com.bloxbean.cardano.yacicli.util.ConsoleWriter.*; + +@Component +@Slf4j +public class YanoBootstrapService { + private final ObjectMapper objectMapper = new ObjectMapper(); + private final HttpClient httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + + public boolean waitForReady(int httpPort, Consumer writer) { + String healthUrl = "http://localhost:" + httpPort + "/q/health/ready"; + int maxAttempts = 30; + for (int i = 0; i < maxAttempts; i++) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(healthUrl)) + .timeout(Duration.ofSeconds(2)) + .GET() + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 200) { + writer.accept(success("Yano is ready")); + return true; + } + } catch (Exception e) { + // Not ready yet + } + try { + Thread.sleep(1000); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + return false; + } + writer.accept("Waiting for Yano HTTP API to be ready ..."); + } + writer.accept(error("Yano HTTP API did not become ready within timeout")); + return false; + } + + public boolean shiftEpochs(int httpPort, int epochs, Consumer writer) { + String url = "http://localhost:" + httpPort + "/api/v1/devnet/epochs/shift"; + try { + String body = objectMapper.writeValueAsString(Map.of("epochs", epochs)); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(Duration.ofSeconds(30)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() == 200) { + writer.accept(success("Shifted genesis back by %d epochs", epochs)); + log.debug("Shift response: {}", response.body()); + return true; + } else { + writer.accept(error("Failed to shift epochs: HTTP %d - %s", response.statusCode(), response.body())); + return false; + } + } catch (Exception e) { + writer.accept(error("Error shifting epochs: " + e.getMessage())); + return false; + } + } + + public boolean catchUpToWallClock(int httpPort, Consumer writer) { + String url = "http://localhost:" + httpPort + "/api/v1/devnet/epochs/catch-up"; + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(Duration.ofSeconds(120)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString("{}")) + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() == 200) { + writer.accept(success("Yano caught up to wall-clock time")); + log.debug("Catch-up response: {}", response.body()); + return true; + } else { + writer.accept(error("Failed to catch up: HTTP %d - %s", response.statusCode(), response.body())); + return false; + } + } catch (Exception e) { + writer.accept(error("Error catching up to wall-clock: " + e.getMessage())); + return false; + } + } + + public boolean fundAddress(int httpPort, String address, BigDecimal ada, Consumer writer) { + String url = "http://localhost:" + httpPort + "/api/v1/devnet/fund"; + try { + String body = objectMapper.writeValueAsString(Map.of("address", address, "ada", ada)); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(Duration.ofSeconds(30)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() == 200) { + log.debug("Funded {} with {} ADA", address, ada); + return true; + } else { + writer.accept(error("Failed to fund address: HTTP %d - %s", response.statusCode(), response.body())); + return false; + } + } catch (Exception e) { + writer.accept(error("Error funding address: " + e.getMessage())); + return false; + } + } + + public String submitTx(int httpPort, String cborHex, Consumer writer) { + String url = "http://localhost:" + httpPort + "/api/v1/tx/submit"; + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(Duration.ofSeconds(30)) + .header("Content-Type", "text/plain") + .POST(HttpRequest.BodyPublishers.ofString(cborHex)) + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() == 200 || response.statusCode() == 202) { + String txHash = response.body().replace("\"", ""); + log.debug("TX submitted: {}", txHash); + return txHash; + } else { + writer.accept(error("TX submission failed: HTTP %d - %s", response.statusCode(), response.body())); + return null; + } + } catch (Exception e) { + writer.accept(error("Error submitting TX: " + e.getMessage())); + return null; + } + } + + public JsonNode getEpochNonce(int httpPort) { + String url = "http://localhost:" + httpPort + "/api/v1/node/epoch-nonce"; + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(Duration.ofSeconds(5)) + .GET() + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 200) { + return objectMapper.readTree(response.body()); + } + } catch (Exception e) { + log.debug("Error getting epoch nonce: {}", e.getMessage()); + } + return null; + } + + public JsonNode getChainTip(int httpPort) { + String url = "http://localhost:" + httpPort + "/api/v1/blocks/latest"; + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(Duration.ofSeconds(5)) + .GET() + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 200) { + return objectMapper.readTree(response.body()); + } + } catch (Exception e) { + log.debug("Error getting chain tip: {}", e.getMessage()); + } + return null; + } +} diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoCompanionService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoCompanionService.java new file mode 100644 index 00000000..a08aa0b3 --- /dev/null +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoCompanionService.java @@ -0,0 +1,260 @@ +package com.bloxbean.cardano.yacicli.localcluster.yano; + +import com.bloxbean.cardano.yacicli.localcluster.ClusterInfo; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.bloxbean.cardano.yacicli.common.AnsiColors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import com.bloxbean.cardano.client.crypto.Blake2bUtil; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.time.Instant; +import java.util.HexFormat; +import java.util.function.Consumer; + +import static com.bloxbean.cardano.yacicli.util.ConsoleWriter.*; + +/** + * Orchestrates the Yano companion mode bootstrap: + * 1. Start Yano with past-time-travel + * 2. Shift epochs back to create room for governance enactment + * 3. Fund test accounts via Yano + * 4. Submit cost model governance proposals (TODO: Phase 4) + * 5. Catch up to wall-clock time + * 6. Update topology so Haskell node syncs from Yano + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class YanoCompanionService { + private static final int BOOTSTRAP_EPOCH_SHIFT = 3; + + private final YanoService yanoService; + private final YanoBootstrapService yanoBootstrapService; + private final YanoGovernanceService yanoGovernanceService; + private final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Run the full companion bootstrap sequence before the Haskell node starts. + * Returns true if bootstrap succeeded and Haskell node should proceed. + */ + public boolean bootstrap(ClusterInfo clusterInfo, Path clusterFolder, Consumer writer) { + int httpPort = clusterInfo.getYanoHttpPort(); + + writer.accept(header(AnsiColors.CYAN_BOLD, "Starting Yano companion bootstrap...")); + + // 1. Start Yano with past-time-travel mode + boolean started = yanoService.start(clusterInfo, clusterFolder, true, writer); + if (!started) { + writer.accept(error("Failed to start Yano. Falling back to haskell-only mode.")); + return false; + } + + // 2. Wait for Yano HTTP API + if (!yanoBootstrapService.waitForReady(httpPort, writer)) { + writer.accept(error("Yano HTTP API not ready. Falling back to haskell-only mode.")); + yanoService.stop(); + return false; + } + + // 3. Shift genesis back N epochs to create room for governance + writer.accept(info("Shifting genesis back %d epochs for protocol param enactment...", BOOTSTRAP_EPOCH_SHIFT)); + if (!yanoBootstrapService.shiftEpochs(httpPort, BOOTSTRAP_EPOCH_SHIFT, writer)) { + writer.accept(error("Failed to shift epochs. Falling back to haskell-only mode.")); + yanoService.stop(); + return false; + } + + // 4. Submit governance proposals (cost model update) via Yano + writer.accept(info("Submitting governance proposals via Yano...")); + boolean governanceDone = yanoGovernanceService.submitCostModelGovernance(clusterInfo, clusterFolder, writer); + if (!governanceDone) { + writer.accept(warn("Governance proposals failed. Cost models will need manual update after startup.")); + } + + // 5. Catch up to wall-clock time + writer.accept(info("Catching up to wall-clock time...")); + if (!yanoBootstrapService.catchUpToWallClock(httpPort, writer)) { + writer.accept(error("Failed to catch up to wall-clock. Falling back to haskell-only mode.")); + yanoService.stop(); + return false; + } + + // 6. Sync shifted genesis files back to Haskell node + try { + syncShiftedGenesisToHaskellNode(clusterInfo, clusterFolder, writer); + } catch (IOException e) { + writer.accept(error("Failed to sync genesis: " + e.getMessage())); + yanoService.stop(); + return false; + } + + // 6b. Query and log Yano's epoch nonce for comparison with Haskell + try { + var nonceInfo = yanoBootstrapService.getEpochNonce(httpPort); + if (nonceInfo != null) { + writer.accept(info("Yano epoch nonce: epoch=%s, nonce=%s", + nonceInfo.path("epoch").asText("?"), + nonceInfo.path("nonce").asText("?"))); + } + } catch (Exception e) { + log.debug("Could not query Yano epoch nonce: {}", e.getMessage()); + } + + // 7. Wait for a few blocks so Yano has a tip for Haskell to sync from + writer.accept(info("Waiting for Yano to produce a few blocks at wall-clock time...")); + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // 8. Update topology so Haskell node peers with Yano + try { + updateTopologyForCompanionMode(clusterInfo, clusterFolder, writer); + } catch (IOException e) { + writer.accept(error("Failed to update topology: " + e.getMessage())); + yanoService.stop(); + return false; + } + + writer.accept(success("Yano companion bootstrap complete. Starting Haskell node...")); + return true; + } + + /** + * Copy Yano's shifted shelley-genesis.json back to the Haskell node's genesis dir, + * and update byron-genesis.json startTime to match. + */ + private void syncShiftedGenesisToHaskellNode(ClusterInfo clusterInfo, Path clusterFolder, Consumer writer) throws IOException { + Path yanoConfigDir = clusterFolder.resolve("yano-config").resolve("network").resolve("devnet"); + Path haskellGenesisDir = clusterFolder.resolve("node").resolve("genesis"); + + // 1. Copy Yano's updated shelley-genesis.json back to Haskell node + Path yanoShelley = yanoConfigDir.resolve("shelley-genesis.json"); + Path haskellShelley = haskellGenesisDir.resolve("shelley-genesis.json"); + Files.copy(yanoShelley, haskellShelley, StandardCopyOption.REPLACE_EXISTING); + + // 2. Read the shifted systemStart and update byron-genesis.json startTime to match + ObjectNode shelleyJson = (ObjectNode) objectMapper.readTree(haskellShelley.toFile()); + String systemStart = shelleyJson.get("systemStart").asText(); + long epochSeconds = Instant.parse(systemStart).getEpochSecond(); + + Path byronGenesis = haskellGenesisDir.resolve("byron-genesis.json"); + ObjectNode byronJson = (ObjectNode) objectMapper.readTree(byronGenesis.toFile()); + byronJson.put("startTime", epochSeconds); + objectMapper.writer(new DefaultPrettyPrinter()).writeValue(byronGenesis.toFile(), byronJson); + + // 3. Update ClusterInfo with the shifted start time + clusterInfo.setStartTime(epochSeconds); + + // 4. Compute and log the genesis hash (= initial epoch nonce) + byte[] genesisBytes = Files.readAllBytes(haskellShelley); + byte[] genesisHash = Blake2bUtil.blake2bHash256(genesisBytes); + String genesisHashHex = HexFormat.of().formatHex(genesisHash); + writer.accept(info("Synced shifted genesis to Haskell node (systemStart: %s)", systemStart)); + writer.accept(info("Shelley genesis hash (epoch nonce): %s", genesisHashHex)); + } + + /** + * Update the Haskell node's topology.json to peer with Yano. + * Backs up original topology first. + */ + private void updateTopologyForCompanionMode(ClusterInfo clusterInfo, Path clusterFolder, Consumer writer) throws IOException { + Path topologyPath = clusterFolder.resolve("node").resolve("topology.json"); + Path topologyBackup = clusterFolder.resolve("node").resolve("topology-original.json"); + + if (!Files.exists(topologyBackup) && Files.exists(topologyPath)) { + Files.copy(topologyPath, topologyBackup, StandardCopyOption.REPLACE_EXISTING); + } + + // Build topology pointing to Yano + ObjectNode topology = objectMapper.createObjectNode(); + + ArrayNode localRoots = objectMapper.createArrayNode(); + ObjectNode localRoot = objectMapper.createObjectNode(); + ArrayNode accessPoints = objectMapper.createArrayNode(); + ObjectNode accessPoint = objectMapper.createObjectNode(); + accessPoint.put("address", "127.0.0.1"); + accessPoint.put("port", clusterInfo.getYanoServerPort()); + accessPoints.add(accessPoint); + localRoot.set("accessPoints", accessPoints); + localRoot.put("valency", 1); + localRoots.add(localRoot); + topology.set("localRoots", localRoots); + + ArrayNode publicRoots = objectMapper.createArrayNode(); + topology.set("publicRoots", publicRoots); + + topology.put("useLedgerAfterSlot", -1); + + objectMapper.writer(new DefaultPrettyPrinter()).writeValue(topologyPath.toFile(), topology); + writer.accept(info("Updated topology.json to peer with Yano (port %d)", clusterInfo.getYanoServerPort())); + } + + /** + * Hand over block production from Yano to the Haskell node. + * Waits for Haskell to sync Yano's chain, then stops Yano and restores topology. + * + * @param clusterInfo cluster configuration + * @param clusterFolder cluster folder path + * @param writer console output + */ + public void performHandover(ClusterInfo clusterInfo, Path clusterFolder, Consumer writer) { + writer.accept(info("Waiting for Haskell node to sync from Yano...")); + + try { + // Wait for node socket to appear (Haskell node is ready) + Path socketPath = Path.of(clusterInfo.getSocketPath()); + for (int i = 0; i < 30; i++) { + if (socketPath.toFile().exists()) break; + Thread.sleep(1000); + } + + // Wait for Haskell relay to sync Yano's chain. + // With epoch-length 600 and 3 epoch shift, Yano produces ~1800 blocks. + // Syncing locally typically takes 10-20 seconds. + // Stability window = 3k/f = 300 slots, so we have ~300s of margin. + writer.accept(info("Waiting 30s for relay to sync Yano's chain...")); + Thread.sleep(30000); + + // Stop Yano — relay has synced the chain to its db + writer.accept(info("Stopping Yano...")); + yanoService.stop(); + writer.accept(success("Yano stopped.")); + + // Restore original topology (remove Yano peer) + restoreOriginalTopology(clusterFolder, writer); + + } catch (Exception e) { + log.error("Error during Yano handover", e); + writer.accept(warn("Yano handover error: " + e.getMessage() + ". Yano may still be running.")); + } + } + + /** + * Restore the original topology.json (before Yano peering was added). + */ + private void restoreOriginalTopology(Path clusterFolder, Consumer writer) { + try { + Path topologyPath = clusterFolder.resolve("node").resolve("topology.json"); + Path topologyBackup = clusterFolder.resolve("node").resolve("topology-original.json"); + + if (Files.exists(topologyBackup)) { + Files.copy(topologyBackup, topologyPath, StandardCopyOption.REPLACE_EXISTING); + writer.accept(info("Restored original topology (removed Yano peer).")); + } + } catch (IOException e) { + log.warn("Could not restore original topology", e); + } + } + +} diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoConfigBuilder.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoConfigBuilder.java new file mode 100644 index 00000000..39eb4932 --- /dev/null +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoConfigBuilder.java @@ -0,0 +1,91 @@ +package com.bloxbean.cardano.yacicli.localcluster.yano; + +import com.bloxbean.cardano.yacicli.localcluster.ClusterConfig; +import com.bloxbean.cardano.yacicli.localcluster.ClusterInfo; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; + +import static com.bloxbean.cardano.yacicli.util.ConsoleWriter.error; + +/** + * Builds Yano's application.properties config file at {yanoHome}/config/. + * Similar to YaciStoreConfigBuilder for yaci-store. + *

+ * Quarkus picks up config/application.properties relative to the working directory. + * Since YanoService sets the working directory to yanoHome, this file is automatically loaded. + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class YanoConfigBuilder { + private final ClusterConfig clusterConfig; + + /** + * Build and write application.properties for Yano. + * + * @param clusterInfo cluster configuration + * @param yanoConfigDir resolved path to Yano's devnet config (genesis files, keys) + * @param yanoDataDir resolved path for Yano's chainstate storage + * @param pastTimeTravelMode whether past-time-travel mode is enabled + * @return true if config was written successfully + */ + public boolean build(ClusterInfo clusterInfo, Path yanoConfigDir, Path yanoDataDir, + boolean pastTimeTravelMode) { + Map props = new LinkedHashMap<>(); + + // Quarkus profile + props.put("quarkus.profile", "devnet"); + props.put("quarkus.http.port", String.valueOf(clusterInfo.getYanoHttpPort())); + + // Network + props.put("yaci.node.remote.protocol-magic", String.valueOf(clusterInfo.getProtocolMagic())); + props.put("yaci.node.server.port", String.valueOf(clusterInfo.getYanoServerPort())); + + // Genesis files + props.put("yaci.node.genesis.shelley-genesis-file", yanoConfigDir.resolve("shelley-genesis.json").toAbsolutePath().toString()); + props.put("yaci.node.genesis.byron-genesis-file", yanoConfigDir.resolve("byron-genesis.json").toAbsolutePath().toString()); + props.put("yaci.node.genesis.alonzo-genesis-file", yanoConfigDir.resolve("alonzo-genesis.json").toAbsolutePath().toString()); + props.put("yaci.node.genesis.conway-genesis-file", yanoConfigDir.resolve("conway-genesis.json").toAbsolutePath().toString()); + props.put("yaci.node.genesis.protocol-parameters-file", yanoConfigDir.resolve("protocol-param.json").toAbsolutePath().toString()); + + // Block producer keys + props.put("yaci.node.block-producer.vrf-skey-file", yanoConfigDir.resolve("vrf.skey").toAbsolutePath().toString()); + props.put("yaci.node.block-producer.kes-skey-file", yanoConfigDir.resolve("kes.skey").toAbsolutePath().toString()); + props.put("yaci.node.block-producer.opcert-file", yanoConfigDir.resolve("opcert.cert").toAbsolutePath().toString()); + + // Storage — inside the node folder so it gets cleaned up with create-node -o + props.put("yaci.node.storage.path", yanoDataDir.toAbsolutePath().toString()); + + // Past-time-travel mode + if (pastTimeTravelMode) { + props.put("yaci.node.block-producer.past-time-travel-mode", "true"); + } + + // Write to {yanoHome}/config/application.properties + Path configPath = Path.of(clusterConfig.getYanoHome(), "config", "application.properties"); + Path configFolder = configPath.getParent(); + if (!configFolder.toFile().exists()) { + configFolder.toFile().mkdirs(); + } + + try (BufferedWriter writer = Files.newBufferedWriter(configPath)) { + for (Map.Entry entry : props.entrySet()) { + writer.write(entry.getKey() + "=" + entry.getValue()); + writer.newLine(); + } + return true; + } catch (IOException e) { + log.error("Error creating Yano configuration file", e); + error("Error creating Yano configuration file: " + e.getMessage()); + return false; + } + } +} diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoGovernanceService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoGovernanceService.java new file mode 100644 index 00000000..81ac1043 --- /dev/null +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoGovernanceService.java @@ -0,0 +1,305 @@ +package com.bloxbean.cardano.yacicli.localcluster.yano; + +import com.bloxbean.cardano.client.address.Address; +import com.bloxbean.cardano.client.address.AddressProvider; +import com.bloxbean.cardano.client.address.Credential; +import com.bloxbean.cardano.client.api.UtxoSupplier; +import com.bloxbean.cardano.client.api.model.Amount; +import com.bloxbean.cardano.client.api.model.Result; +import com.bloxbean.cardano.client.api.model.Utxo; +import com.bloxbean.cardano.client.backend.api.BackendService; +import com.bloxbean.cardano.client.backend.api.DefaultUtxoSupplier; +import com.bloxbean.cardano.client.backend.blockfrost.service.BFBackendService; +import com.bloxbean.cardano.client.common.model.Networks; +import com.bloxbean.cardano.client.crypto.KeyGenUtil; +import com.bloxbean.cardano.client.crypto.SecretKey; +import com.bloxbean.cardano.client.crypto.VerificationKey; +import com.bloxbean.cardano.client.crypto.bip32.key.HdPublicKey; +import com.bloxbean.cardano.client.function.helper.SignerProviders; +import com.bloxbean.cardano.client.api.impl.StaticTransactionEvaluator; +import com.bloxbean.cardano.client.plutus.spec.*; +import com.bloxbean.cardano.client.quicktx.QuickTxBuilder; +import com.bloxbean.cardano.client.quicktx.ScriptTx; +import com.bloxbean.cardano.client.quicktx.Tx; +import com.bloxbean.cardano.client.transaction.spec.ProtocolParamUpdate; +import com.bloxbean.cardano.client.transaction.spec.governance.*; +import com.bloxbean.cardano.client.transaction.spec.governance.actions.GovActionId; +import com.bloxbean.cardano.client.transaction.spec.governance.actions.ParameterChangeAction; +import com.bloxbean.cardano.client.util.HexUtil; +import com.bloxbean.cardano.yacicli.localcluster.ClusterInfo; +import com.bloxbean.cardano.yacicli.localcluster.common.GenesisUtil; +import com.bloxbean.cardano.yacicli.common.Tuple; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.math.BigInteger; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.function.Consumer; + +import static com.bloxbean.cardano.client.common.CardanoConstants.LOVELACE; +import static com.bloxbean.cardano.yacicli.util.ConsoleWriter.*; + +/** + * Submits governance proposals (Plutus cost model updates) via Yano's Blockfrost-compatible HTTP API + * during companion mode bootstrap. Uses BFBackendService + QuickTxBuilder, same pattern as PeerService. + */ +@Component +@Slf4j +public class YanoGovernanceService { + private final YanoBootstrapService yanoBootstrapService; + private final Path plutusCostModelsBasePath; + private final ObjectMapper objectMapper = new ObjectMapper(); + + public YanoGovernanceService(YanoBootstrapService yanoBootstrapService, + @Value("${yaci.cli.plutus-costmodels-path:./config}") String plutusCostModelsBasePath) { + this.yanoBootstrapService = yanoBootstrapService; + this.plutusCostModelsBasePath = Paths.get(plutusCostModelsBasePath); + } + + /** + * Submit cost model governance proposals to Yano via its Blockfrost-compatible HTTP API. + * Registers stake + DRep in one TX, submits ParameterChangeAction, and votes YES. + * All done before Yano catches up to wall clock, so the proposal is enacted by epoch 2. + */ + public boolean submitCostModelGovernance(ClusterInfo clusterInfo, Path clusterFolder, Consumer writer) { + try { + int httpPort = clusterInfo.getYanoHttpPort(); + String yanoUrl = "http://localhost:" + httpPort + "/api/v1/"; + BackendService backendService = new BFBackendService(yanoUrl, "dummy_key"); + UtxoSupplier utxoSupplier = new DefaultUtxoSupplier(backendService.getUtxoService()); + QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService); + + // 1. Load UTXO keys + List> utxoKeys = GenesisUtil.loadUtxoKeys(clusterFolder); + + // 2. Find a funded key with sufficient balance (>2000 ADA for deposits + fees) + BigInteger minBalance = BigInteger.valueOf(2_000_000_000L); + String senderAddress = null; + SecretKey senderSkey = null; + + for (int i = 0; i < utxoKeys.size(); i++) { + Tuple tuple = utxoKeys.get(i); + HdPublicKey hdPublicKey = new HdPublicKey(); + hdPublicKey.setKeyData(tuple._1.getBytes()); + Address address = AddressProvider.getEntAddress(hdPublicKey, Networks.testnet()); + String addr = address.toBech32(); + + List utxos = utxoSupplier.getAll(addr); + Optional sufficient = utxos.stream() + .flatMap(utxo -> utxo.getAmount().stream()) + .filter(amt -> LOVELACE.equals(amt.getUnit()) && amt.getQuantity().compareTo(minBalance) > 0) + .findAny(); + + if (sufficient.isPresent()) { + senderAddress = addr; + senderSkey = tuple._2; + break; + } + } + + if (senderAddress == null) { + writer.accept(error("No funded UTXO key found for governance proposal via Yano")); + return false; + } + + // 3. Load cost models + Path costModelsFile = GenesisUtil.resolveCostModelsFile(plutusCostModelsBasePath, clusterInfo.getProtocolMajorVer()); + Map costModelMap = GenesisUtil.loadCostModels(costModelsFile); + writer.accept(info("Using Plutus cost models file: %s", costModelsFile.getFileName())); + + // 4. Build ProtocolParamUpdate with cost models + CostMdls costMdls = new CostMdls(); + Map languageMap = Map.of( + "PlutusV1", Language.PLUTUS_V1, + "PlutusV2", Language.PLUTUS_V2, + "PlutusV3", Language.PLUTUS_V3 + ); + for (Map.Entry entry : costModelMap.entrySet()) { + Language language = languageMap.get(entry.getKey()); + if (language != null) { + costMdls.add(new CostModel(language, entry.getValue())); + } + } + + ProtocolParamUpdate protocolParamUpdate = ProtocolParamUpdate.builder() + .costModels(costMdls) + .build(); + + // 5. Build always-true PlutusV3 guardrail script + PlutusV3Script guardrailScript = PlutusV3Script.builder() + .type("PlutusScriptV3") + .cborHex("46450101002499") + .build(); + + // 6. Build ParameterChangeAction + ParameterChangeAction paramChangeAction = ParameterChangeAction.builder() + .prevGovActionId(null) + .protocolParamUpdate(protocolParamUpdate) + .policyHash(guardrailScript.getScriptHash()) + .build(); + + // 7. Build anchor + Anchor anchor = Anchor.builder() + .anchorUrl("https://devkit.yaci.xyz/plutus-costmodel-update.json") + .anchorDataHash(new byte[32]) + .build(); + + // 8. Build reward address and DRep credential from sender key + var senderVkey = KeyGenUtil.getPublicKeyFromPrivateKey(senderSkey); + HdPublicKey hdPublicKey = new HdPublicKey(); + hdPublicKey.setKeyData(senderVkey.getBytes()); + Address rewardAddr = AddressProvider.getRewardAddress(hdPublicKey, Networks.testnet()); + String rewardAccount = rewardAddr.toBech32(); + + byte[] credHash = rewardAddr.getDelegationCredentialHash() + .orElseThrow(() -> new RuntimeException("Failed to get delegation credential hash")); + String credHashHex = HexUtil.encodeHexString(credHash); + Credential drepCredential = Credential.fromKey(credHashHex); + + // --- TX 1: Register stake + DRep + delegate voting power (all in one TX) --- + // Combining avoids cross-TX dependency (DRep registration requires registered stake credential, + // which is satisfied within the same TX since Cardano processes certificates sequentially) + writer.accept("Registering stake credential, DRep, and delegating voting power..."); + Tx combinedRegTx = new Tx() + .registerStakeAddress(rewardAddr) + .registerDRep(drepCredential) + .delegateVotingPowerTo(rewardAddr, DRep.addrKeyHash(credHashHex)) + .from(senderAddress); + Result regResult = quickTxBuilder.compose(combinedRegTx) + .withSigner(SignerProviders.signerFrom(senderSkey)) + .complete(); + + if (!regResult.isSuccessful()) { + writer.accept(error("Stake/DRep registration failed: " + regResult.getResponse())); + return false; + } + writer.accept(success("Stake + DRep registered, voting power delegated. Tx# : " + regResult.getValue())); + + // Wait for TX to be included in a block (not just mempool) + if (!waitForNextBlock(httpPort, regResult.getValue(), utxoSupplier, senderAddress, writer)) { + writer.accept(error("Registration TX not confirmed in a block within timeout")); + return false; + } + + // --- TX 2: Submit governance proposal (ScriptTx with guardrail script) --- + writer.accept("Submitting Plutus cost models governance proposal..."); + ScriptTx scriptTx = new ScriptTx() + .createProposal(paramChangeAction, rewardAccount, anchor, PlutusData.unit()) + .attachProposingValidator(guardrailScript); + + var staticEvaluator = new StaticTransactionEvaluator( + List.of(ExUnits.builder() + .mem(BigInteger.valueOf(500000)) + .steps(BigInteger.valueOf(200000000)) + .build())); + + Result proposalResult = quickTxBuilder.compose(scriptTx) + .feePayer(senderAddress) + .withSigner(SignerProviders.signerFrom(senderSkey)) + .withTxEvaluator(staticEvaluator) + .complete(); + + if (!proposalResult.isSuccessful()) { + writer.accept(error("Plutus cost models governance proposal failed: " + proposalResult.getResponse())); + return false; + } + writer.accept(success("Plutus cost models governance proposal submitted. Tx# : " + proposalResult.getValue())); + + // Wait for proposal TX to be in a block before voting + if (!waitForNextBlock(httpPort, proposalResult.getValue(), utxoSupplier, senderAddress, writer)) { + writer.accept(error("Proposal TX not confirmed in a block within timeout")); + return false; + } + + // --- TX 3: Vote YES on the proposal --- + writer.accept("Voting YES on Plutus cost models proposal..."); + Voter voter = Voter.builder() + .type(VoterType.DREP_KEY_HASH) + .credential(drepCredential) + .build(); + GovActionId govActionId = GovActionId.builder() + .transactionId(proposalResult.getValue()) + .govActionIndex(0) + .build(); + + Tx voteTx = new Tx() + .createVote(voter, govActionId, Vote.YES) + .from(senderAddress); + Result voteResult = quickTxBuilder.compose(voteTx) + .withSigner(SignerProviders.signerFrom(senderSkey)) + .complete(); + + if (!voteResult.isSuccessful()) { + writer.accept(error("DRep vote on proposal failed: " + voteResult.getResponse())); + return false; + } + writer.accept(success("DRep voted YES on Plutus cost models proposal. Tx# : " + voteResult.getValue())); + + // Wait for vote TX to be confirmed in a block BEFORE catchUpToWallClock. + // All governance TXs must be in epoch 0 blocks so they're enacted by epoch 2. + if (!waitForNextBlock(httpPort, voteResult.getValue(), utxoSupplier, senderAddress, writer)) { + writer.accept(warn("Vote TX not confirmed in a block within timeout")); + } + + writer.accept(info("Plutus cost models will be enacted at the next epoch boundary.")); + return true; + + } catch (Exception e) { + log.error("Failed to submit governance proposals via Yano", e); + writer.accept(error("Governance proposal via Yano failed: " + e.getMessage())); + return false; + } + } + + /** + * Wait for a transaction to be confirmed in a block by polling the chain tip. + * Yano in past-time-travel mode produces blocks rapidly, so this should resolve quickly. + */ + private boolean waitForNextBlock(int httpPort, String txHash, UtxoSupplier utxoSupplier, + String address, Consumer writer) { + // Get current tip height + long startHeight = getBlockHeight(httpPort); + + for (int i = 0; i < 30; i++) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + + // Check if tip advanced (a new block was produced) + long currentHeight = getBlockHeight(httpPort); + if (currentHeight > startHeight) { + // Verify the TX is in the UTXO set (confirmed, not just mempool) + boolean found = utxoSupplier.getAll(address).stream() + .anyMatch(utxo -> utxo.getTxHash().equals(txHash)); + if (found) { + writer.accept(info("TX confirmed at block height %d", currentHeight)); + return true; + } + } + } + + writer.accept(warn("TX %s not confirmed within 30s timeout", txHash)); + return false; + } + + private long getBlockHeight(int httpPort) { + try { + JsonNode tip = yanoBootstrapService.getChainTip(httpPort); + if (tip != null && tip.has("height")) { + return tip.get("height").asLong(-1); + } + } catch (Exception e) { + log.debug("Error getting chain tip: {}", e.getMessage()); + } + return -1; + } + +} diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoService.java new file mode 100644 index 00000000..a01b7d42 --- /dev/null +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoService.java @@ -0,0 +1,372 @@ +package com.bloxbean.cardano.yacicli.localcluster.yano; + +import com.bloxbean.cardano.yacicli.localcluster.ClusterConfig; +import com.bloxbean.cardano.yacicli.localcluster.ClusterInfo; +import com.bloxbean.cardano.yacicli.localcluster.events.ClusterStopped; +import com.bloxbean.cardano.yacicli.util.PortUtil; +import com.bloxbean.cardano.yacicli.util.ProcessStream; +import com.bloxbean.cardano.yacicli.util.ProcessUtil; +import com.google.common.collect.EvictingQueue; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +import static com.bloxbean.cardano.yacicli.util.ConsoleWriter.*; + +@Component +@RequiredArgsConstructor +@Slf4j +public class YanoService { + private static final String YANO_PROCESS_NAME = "yano"; + + private final ClusterConfig clusterConfig; + private final ProcessUtil processUtil; + private final YanoConfigBuilder yanoConfigBuilder; + + private List processes = new ArrayList<>(); + private List executors = new ArrayList<>(); + private Queue logs = EvictingQueue.create(300); + + public boolean start(ClusterInfo clusterInfo, Path clusterFolder, boolean pastTimeTravelMode, Consumer writer) { + logs.clear(); + + Path yanoBin = Path.of(clusterConfig.getYanoHome(), "yano"); + if (!yanoBin.toFile().exists()) { + writer.accept(error("yano binary not found at " + yanoBin)); + writer.accept(error("Please run 'download --component yano' first")); + return false; + } + + if (!PortUtil.isPortAvailable(clusterInfo.getYanoServerPort())) { + writer.accept(error("Yano n2n port " + clusterInfo.getYanoServerPort() + " is not available")); + return false; + } + if (!PortUtil.isPortAvailable(clusterInfo.getYanoHttpPort())) { + writer.accept(error("Yano HTTP port " + clusterInfo.getYanoHttpPort() + " is not available")); + return false; + } + + try { + // Prepare Yano config directory with genesis files and keys + Path yanoConfigDir = prepareYanoConfig(clusterInfo, clusterFolder, writer); + if (yanoConfigDir == null) return false; + + Process process = startYanoProcess(clusterInfo, clusterFolder, yanoConfigDir, pastTimeTravelMode, writer); + if (process != null) { + processes.add(process); + return true; + } + } catch (Exception e) { + log.error("Error starting Yano", e); + writer.accept(error("Failed to start Yano: " + e.getMessage())); + } + return false; + } + + private Path prepareYanoConfig(ClusterInfo clusterInfo, Path clusterFolder, Consumer writer) throws IOException { + Path yanoConfigDir = clusterFolder.resolve("yano-config").resolve("network").resolve("devnet"); + Files.createDirectories(yanoConfigDir); + + // 1. Start with Yano's bundled devnet config as base (protocol-param.json, keys, genesis defaults) + Path yanoBundledDevnetConfig = Path.of(clusterConfig.getYanoHome(), "config", "network", "devnet"); + if (yanoBundledDevnetConfig.toFile().exists()) { + FileUtils.copyDirectory(yanoBundledDevnetConfig.toFile(), yanoConfigDir.toFile()); + writer.accept(info("Copied Yano bundled devnet config as base")); + } else { + writer.accept(warn("Yano bundled config not found at " + yanoBundledDevnetConfig)); + } + + // 2. Overwrite genesis files from DevKit's cluster (these have DevKit's specific params) + Path genesisDir = clusterFolder.resolve("node").resolve("genesis"); + String[] genesisFiles = {"shelley-genesis.json", "byron-genesis.json", "alonzo-genesis.json", "conway-genesis.json"}; + for (String gf : genesisFiles) { + Path src = genesisDir.resolve(gf); + if (src.toFile().exists()) { + Files.copy(src, yanoConfigDir.resolve(gf), StandardCopyOption.REPLACE_EXISTING); + } + } + + // 3. Align protocol-param.json with DevKit's genesis params so Yano-produced blocks + // are valid from the Haskell node's perspective (deposits, fees, etc. must match) + alignProtocolParams(yanoConfigDir, genesisDir, writer); + + // 4. Overwrite VRF/KES keys from DevKit's pool-keys (must match genesis genDelegs) + Path poolKeysDir = clusterFolder.resolve("node").resolve("pool-keys"); + String[] keyFiles = {"vrf.skey", "kes.skey", "opcert.cert"}; + for (String kf : keyFiles) { + Path src = poolKeysDir.resolve(kf); + if (src.toFile().exists()) { + Files.copy(src, yanoConfigDir.resolve(kf), StandardCopyOption.REPLACE_EXISTING); + } else { + writer.accept(warn("Key file not found: " + src + ". Yano block production may fail.")); + } + } + + writer.accept(info("Yano config prepared at " + yanoConfigDir)); + return yanoConfigDir; + } + + /** + * Align Yano's protocol-param.json with DevKit's genesis protocol parameters. + * Yano's bundled defaults may differ from DevKit's genesis (e.g., govActionDeposit), + * which would cause Yano-produced blocks to be invalid from the Haskell node's perspective. + */ + private void alignProtocolParams(Path yanoConfigDir, Path genesisDir, Consumer writer) { + try { + ObjectMapper mapper = new ObjectMapper(); + Path ppPath = yanoConfigDir.resolve("protocol-param.json"); + if (!ppPath.toFile().exists()) return; + + ObjectNode pp = (ObjectNode) mapper.readTree(ppPath.toFile()); + + // Read conway-genesis for governance params and PlutusV3 cost model + Path conwayPath = genesisDir.resolve("conway-genesis.json"); + if (conwayPath.toFile().exists()) { + JsonNode conway = mapper.readTree(conwayPath.toFile()); + if (conway.has("govActionDeposit")) + pp.put("gov_action_deposit", conway.get("govActionDeposit").asLong()); + if (conway.has("dRepDeposit")) + pp.put("drep_deposit", conway.get("dRepDeposit").asLong()); + if (conway.has("committeeMinSize")) + pp.put("committee_min_size", conway.get("committeeMinSize").asInt()); + if (conway.has("committeeMaxTermLength")) + pp.put("committee_max_term_length", conway.get("committeeMaxTermLength").asInt()); + if (conway.has("govActionLifetime")) + pp.put("gov_action_lifetime", conway.get("govActionLifetime").asInt()); + if (conway.has("dRepActivity")) + pp.put("drep_activity", conway.get("dRepActivity").asInt()); + + // Align PlutusV3 cost model from conway-genesis. + // Genesis has an array of values; Yano needs a dict with named keys (sorted alphabetically). + // Use existing keys from Yano's bundled config, map genesis values to the first N keys. + JsonNode genesisV3 = conway.get("plutusV3CostModel"); + if (genesisV3 != null && genesisV3.isArray() && pp.has("cost_models")) { + ObjectNode costModels = (ObjectNode) pp.get("cost_models"); + alignCostModelFromArray(mapper, costModels, "PlutusV3", genesisV3); + } + } + + // Align PlutusV1/V2 cost models from alonzo-genesis + Path alonzoPath = genesisDir.resolve("alonzo-genesis.json"); + if (alonzoPath.toFile().exists() && pp.has("cost_models")) { + JsonNode alonzo = mapper.readTree(alonzoPath.toFile()); + JsonNode genesisCostModels = alonzo.get("costModels"); + if (genesisCostModels != null) { + ObjectNode costModels = (ObjectNode) pp.get("cost_models"); + for (String lang : new String[]{"PlutusV1", "PlutusV2"}) { + if (genesisCostModels.has(lang) && genesisCostModels.get(lang).isArray()) { + alignCostModelFromArray(mapper, costModels, lang, genesisCostModels.get(lang)); + } + } + } + } + + // Read shelley-genesis for base protocol params + Path shelleyPath = genesisDir.resolve("shelley-genesis.json"); + if (shelleyPath.toFile().exists()) { + JsonNode shelley = mapper.readTree(shelleyPath.toFile()); + JsonNode sp = shelley.get("protocolParams"); + if (sp != null) { + if (sp.has("keyDeposit")) + pp.put("key_deposit", sp.get("keyDeposit").asLong()); + if (sp.has("poolDeposit")) + pp.put("pool_deposit", sp.get("poolDeposit").asLong()); + if (sp.has("minFeeA")) + pp.put("min_fee_a", sp.get("minFeeA").asInt()); + if (sp.has("minFeeB")) + pp.put("min_fee_b", sp.get("minFeeB").asInt()); + if (sp.has("maxTxSize")) + pp.put("max_tx_size", sp.get("maxTxSize").asInt()); + if (sp.has("maxBlockBodySize")) + pp.put("max_block_size", sp.get("maxBlockBodySize").asInt()); + if (sp.has("minPoolCost")) + pp.put("min_pool_cost", sp.get("minPoolCost").asLong()); + } + } + + mapper.writerWithDefaultPrettyPrinter().writeValue(ppPath.toFile(), pp); + writer.accept(info("Aligned protocol-param.json with DevKit genesis")); + } catch (Exception e) { + log.warn("Failed to align protocol params: {}", e.getMessage()); + writer.accept(warn("Could not align protocol-param.json: " + e.getMessage())); + } + } + + /** + * Align a cost model in protocol-param.json from a genesis array. + * Takes the existing dict keys (sorted), maps genesis array values to the first N keys. + * If genesis has fewer values than keys, only the first N keys are kept. + */ + private void alignCostModelFromArray(ObjectMapper mapper, ObjectNode costModels, String language, JsonNode genesisArray) { + if (!costModels.has(language) || !costModels.get(language).isObject()) return; + + JsonNode existingDict = costModels.get(language); + List sortedKeys = new ArrayList<>(); + existingDict.fieldNames().forEachRemaining(sortedKeys::add); + Collections.sort(sortedKeys); + + ObjectNode aligned = mapper.createObjectNode(); + int genesisSize = genesisArray.size(); + for (int i = 0; i < Math.min(genesisSize, sortedKeys.size()); i++) { + aligned.put(sortedKeys.get(i), genesisArray.get(i).asLong()); + } + costModels.set(language, aligned); + } + + private Process startYanoProcess(ClusterInfo clusterInfo, Path clusterFolder, Path yanoConfigDir, + boolean pastTimeTravelMode, Consumer writer) + throws IOException, InterruptedException { + + Path yanoBin = Path.of(clusterConfig.getYanoHome(), "yano"); + + // Store Yano data inside node folder so it gets cleaned up with create-node -o + Path yanoDataDir = clusterFolder.resolve("node").resolve("yano"); + Files.createDirectories(yanoDataDir); + + // Write application.properties for Yano (persists config on disk for debugging) + yanoConfigBuilder.build(clusterInfo, yanoConfigDir, yanoDataDir, pastTimeTravelMode); + + ProcessBuilder builder = new ProcessBuilder(); + builder.directory(new File(clusterConfig.getYanoHome())); + + // Env vars override the properties file (Quarkus priority: env > config file) + // Keep them for runtime guarantee with native binaries + var env = builder.environment(); + env.put("QUARKUS_PROFILE", "devnet"); + env.put("YACI_NODE_REMOTE_PROTOCOL_MAGIC", String.valueOf(clusterInfo.getProtocolMagic())); + env.put("YACI_NODE_SERVER_PORT", String.valueOf(clusterInfo.getYanoServerPort())); + env.put("QUARKUS_HTTP_PORT", String.valueOf(clusterInfo.getYanoHttpPort())); + env.put("YACI_NODE_GENESIS_SHELLEY_GENESIS_FILE", yanoConfigDir.resolve("shelley-genesis.json").toAbsolutePath().toString()); + env.put("YACI_NODE_GENESIS_BYRON_GENESIS_FILE", yanoConfigDir.resolve("byron-genesis.json").toAbsolutePath().toString()); + env.put("YACI_NODE_GENESIS_ALONZO_GENESIS_FILE", yanoConfigDir.resolve("alonzo-genesis.json").toAbsolutePath().toString()); + env.put("YACI_NODE_GENESIS_CONWAY_GENESIS_FILE", yanoConfigDir.resolve("conway-genesis.json").toAbsolutePath().toString()); + env.put("YACI_NODE_GENESIS_PROTOCOL_PARAMETERS_FILE", yanoConfigDir.resolve("protocol-param.json").toAbsolutePath().toString()); + env.put("YACI_NODE_BLOCK_PRODUCER_VRF_SKEY_FILE", yanoConfigDir.resolve("vrf.skey").toAbsolutePath().toString()); + env.put("YACI_NODE_BLOCK_PRODUCER_KES_SKEY_FILE", yanoConfigDir.resolve("kes.skey").toAbsolutePath().toString()); + env.put("YACI_NODE_BLOCK_PRODUCER_OPCERT_FILE", yanoConfigDir.resolve("opcert.cert").toAbsolutePath().toString()); + env.put("YACI_NODE_STORAGE_PATH", yanoDataDir.toAbsolutePath().toString()); + + if (pastTimeTravelMode) { + env.put("YACI_NODE_BLOCK_PRODUCER_PAST_TIME_TRAVEL_MODE", "true"); + } + + builder.command(yanoBin.toAbsolutePath().toString()); + + Process process = builder.start(); + writer.accept(info("Starting Yano (n2n port: %d, HTTP port: %d) ...", + clusterInfo.getYanoServerPort(), clusterInfo.getYanoHttpPort())); + + AtomicBoolean started = new AtomicBoolean(false); + ProcessStream processStream = new ProcessStream(process.getInputStream(), line -> { + logs.add(line); + if (line != null && (line.contains("Listening on") || line.contains("started in"))) { + started.set(true); + } + }); + ExecutorService stdoutExecutor = Executors.newSingleThreadExecutor(); + stdoutExecutor.submit(processStream); + executors.add(stdoutExecutor); + + // Also capture stderr + ProcessStream errorStream = new ProcessStream(process.getErrorStream(), line -> logs.add(line)); + ExecutorService stderrExecutor = Executors.newSingleThreadExecutor(); + stderrExecutor.submit(errorStream); + executors.add(stderrExecutor); + + // Wait for startup + int counter = 0; + while (counter < 30) { + counter++; + if (started.get()) break; + if (!process.isAlive()) { + writer.accept(error("Yano process exited unexpectedly. Check logs with 'yano-logs'")); + return null; + } + Thread.sleep(1000); + writer.accept("Waiting for Yano to start ..."); + } + + if (!started.get()) { + writer.accept(error("Yano did not start within timeout. Check logs with 'yano-logs'")); + return null; + } + + writer.accept(success("Yano started successfully")); + processUtil.createProcessId(YANO_PROCESS_NAME, process); + return process; + } + + @EventListener + public void handleClusterStopped(ClusterStopped clusterStopped) { + stop(); + } + + public boolean stop() { + try { + if (processes != null && !processes.isEmpty()) + writeLn(info("Trying to stop Yano ...")); + + for (Process process : processes) { + if (process != null && process.isAlive()) { + process.descendants().forEach(ph -> { + ph.destroyForcibly(); + }); + process.destroyForcibly(); + process.waitFor(15, TimeUnit.SECONDS); + if (!process.isAlive()) { + writeLn(success("Yano stopped")); + } else { + writeLn(error("Yano process could not be killed")); + } + } + } + processUtil.deletePidFile(YANO_PROCESS_NAME); + executors.forEach(ExecutorService::shutdownNow); + executors.clear(); + logs.clear(); + } catch (Exception e) { + log.error("Error stopping Yano", e); + writeLn(error("Yano could not be stopped: " + e.getMessage())); + return false; + } finally { + processes.clear(); + } + return true; + } + + public boolean isRunning() { + return processes.stream().anyMatch(Process::isAlive); + } + + public void showLogs(Consumer consumer) { + if (logs.isEmpty()) { + consumer.accept("No Yano logs to show"); + } else { + int counter = 0; + while (!logs.isEmpty()) { + counter++; + if (counter == 200) return; + consumer.accept(logs.poll()); + } + } + } +} diff --git a/applications/cli/src/main/resources/localcluster/templates/devnet/genesis-templates/dijkstra-genesis.json b/applications/cli/src/main/resources/localcluster/templates/devnet/genesis-templates/dijkstra-genesis.json new file mode 100644 index 00000000..b11bbad9 --- /dev/null +++ b/applications/cli/src/main/resources/localcluster/templates/devnet/genesis-templates/dijkstra-genesis.json @@ -0,0 +1,6 @@ +{ + "maxRefScriptSizePerBlock": {{maxRefScriptSizePerBlock}}, + "maxRefScriptSizePerTx": {{maxRefScriptSizePerTx}}, + "refScriptCostStride": {{refScriptCostStride}}, + "refScriptCostMultiplier": {{refScriptCostMultiplier}} +} From 6b63c6b8bdbcb4373a02efb281bb147dc5904514 Mon Sep 17 00:00:00 2001 From: Satya Date: Thu, 9 Apr 2026 20:00:18 +0800 Subject: [PATCH 04/17] - Added yano-primary mode with rollback support --- .../yacicli/localcluster/ClusterInfo.java | 2 +- .../localcluster/ClusterPortInfoHelper.java | 4 +- .../localcluster/ClusterStartService.java | 129 ++++++++- .../yacicli/localcluster/NodeMode.java | 31 ++ .../localcluster/api/EpochController.java | 35 ++- .../localcluster/api/RollbackController.java | 21 ++ .../api/TransactionController.java | 24 +- .../api/service/TestTransactionService.java | 10 +- .../commands/RollbackCommands.java | 7 + .../localcluster/config/GenesisConfig.java | 7 +- .../FirstRunPlutusV3CostModelUpdate.java | 6 +- .../listeners/FirstRunTopupAccounts.java | 9 +- .../localcluster/service/AccountService.java | 29 ++ .../service/ClusterUtilService.java | 17 ++ .../localcluster/service/RollbackService.java | 48 ++++ .../service/YanoHttpNodeService.java | 270 ++++++++++++++++++ .../yacistore/YaciStoreConfigBuilder.java | 53 +++- .../yacistore/YaciStoreService.java | 36 ++- .../yano/YanoBootstrapService.java | 26 ++ .../yano/YanoCompanionService.java | 8 +- 20 files changed, 735 insertions(+), 37 deletions(-) create mode 100644 applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/NodeMode.java create mode 100644 applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/YanoHttpNodeService.java diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterInfo.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterInfo.java index 808c6f6c..76c389e7 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterInfo.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterInfo.java @@ -44,7 +44,7 @@ public class ClusterInfo { private int prometheusPort=12798; private int protocolMajorVer; - private String nodeMode; + private NodeMode nodeMode; @Builder.Default private int yanoServerPort = 14447; diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterPortInfoHelper.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterPortInfoHelper.java index 9df1c7fc..a4216467 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterPortInfoHelper.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterPortInfoHelper.java @@ -38,8 +38,8 @@ public void printUrls(String clusteName, ClusterInfo clusterInfo) { writeLn(infoLabel("Epoch Length", String.valueOf(clusterInfo.getEpochLength()))); writeLn(infoLabel("Security Param", String.valueOf(clusterInfo.getSecurityParam()))); writeLn(infoLabel("SlotsPerKESPeriod", String.valueOf(clusterInfo.getSlotsPerKESPeriod()))); - String nodeMode = clusterInfo.getNodeMode() != null ? clusterInfo.getNodeMode() : "haskell-only"; - writeLn(infoLabel("Node Mode", nodeMode)); + NodeMode nodeMode = clusterInfo.getNodeMode() != null ? clusterInfo.getNodeMode() : NodeMode.HASKELL_ONLY; + writeLn(infoLabel("Node Mode", nodeMode.getValue())); if (clusteName == null || !"default".equals(clusteName)) return; 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 baa0f25d..d81d5525 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 @@ -8,9 +8,13 @@ import com.bloxbean.cardano.yacicli.localcluster.events.FirstRunDone; import com.bloxbean.cardano.yacicli.localcluster.model.RunStatus; import com.bloxbean.cardano.yacicli.localcluster.peer.LocalPeerService; +import com.bloxbean.cardano.yacicli.localcluster.yano.YanoBootstrapService; import com.bloxbean.cardano.yacicli.localcluster.yano.YanoCompanionService; +import com.bloxbean.cardano.yacicli.localcluster.yano.YanoGovernanceService; +import com.bloxbean.cardano.yacicli.localcluster.yano.YanoService; import com.bloxbean.cardano.yacicli.util.PortUtil; import com.bloxbean.cardano.yacicli.util.ProcessUtil; +import org.apache.commons.io.FileUtils; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.LongNode; @@ -49,6 +53,9 @@ public class ClusterStartService { private final CustomGenesisConfig customGenesisConfig; private final LocalPeerService localPeerService; private final YanoCompanionService yanoCompanionService; + private final YanoService yanoService; + private final YanoBootstrapService yanoBootstrapService; + private final YanoGovernanceService yanoGovernanceService; private ObjectMapper objectMapper = new ObjectMapper(); private List processes = new ArrayList<>(); @@ -71,8 +78,9 @@ public RunStatus startCluster(ClusterInfo clusterInfo, Path clusterFolder, Consu try { boolean firstRun = checkIfFirstRun(clusterFolder); - boolean companionMode = "companion".equals(clusterInfo.getNodeMode()); - boolean yanoOnlyMode = "yano-only".equals(clusterInfo.getNodeMode()); + boolean companionMode = NodeMode.COMPANION == clusterInfo.getNodeMode(); + boolean yanoOnlyMode = NodeMode.YANO_ONLY == clusterInfo.getNodeMode(); + boolean yanoPrimaryMode = NodeMode.YANO_PRIMARY == clusterInfo.getNodeMode(); boolean companionBootstrapDone = false; if (clusterInfo.isMasterNode() && firstRun) @@ -87,9 +95,67 @@ public RunStatus startCluster(ClusterInfo clusterInfo, Path clusterFolder, Consu } if (yanoOnlyMode) { - // Yano-only: start Yano without past-time-travel, skip Haskell node entirely - // TODO: Full yano-only mode implementation (start Yano, fund, update cost models) - writer.accept(warn("yano-only mode is not yet fully implemented. Falling back to haskell-only.")); + // Yano-only: Yano is the sole block producer, no Haskell node + boolean yanoStarted = yanoService.start(clusterInfo, clusterFolder, false, writer); + if (!yanoStarted) { + writer.accept(error("Failed to start Yano.")); + return new RunStatus(false, firstRun); + } + + if (firstRun) { + int httpPort = clusterInfo.getYanoHttpPort(); + if (!yanoBootstrapService.waitForReady(httpPort, writer)) { + writer.accept(error("Yano HTTP API not ready.")); + yanoService.stop(); + return new RunStatus(false, firstRun); + } + + // Submit governance proposals via Yano's HTTP API + writer.accept(info("Submitting governance proposals via Yano...")); + yanoGovernanceService.submitCostModelGovernance(clusterInfo, clusterFolder, writer); + } + + // No Haskell node, no submit-api, no handover + return new RunStatus(true, firstRun); + } + + if (yanoPrimaryMode) { + // Like companion bootstrap but Yano stays as BP, Haskell stays as relay (no handover) + if (firstRun) { + boolean bootstrapDone = yanoCompanionService.bootstrap(clusterInfo, clusterFolder, writer); + if (!bootstrapDone) { + writer.accept(error("Yano bootstrap failed.")); + return new RunStatus(false, firstRun); + } + } else { + boolean yanoStarted = yanoService.start(clusterInfo, clusterFolder, false, writer); + if (!yanoStarted) { + writer.accept(error("Failed to start Yano.")); + return new RunStatus(false, firstRun); + } + if (!yanoBootstrapService.waitForReady(clusterInfo.getYanoHttpPort(), writer)) { + writer.accept(error("Yano HTTP API not ready.")); + yanoService.stop(); + return new RunStatus(false, firstRun); + } + } + + Process nodeProcess = startNode(clusterFolder, clusterInfo, true, writer); + if (nodeProcess == null) { + writer.accept(error("Node process could not be started.")); + yanoService.stop(); + return new RunStatus(false, firstRun); + } + processes.add(nodeProcess); + + Process submitApiProcess = startSubmitApi(clusterInfo, clusterFolder, writer); + if (submitApiProcess != null) processes.add(submitApiProcess); + + // Relay needs time to chain-sync from Yano before N2C queries work + writer.accept(info("Waiting for Haskell relay to sync from Yano...")); + Thread.sleep(5000); + + return new RunStatus(true, firstRun); } if (clusterInfo.isLocalMultiNodeEnabled()) { @@ -407,6 +473,56 @@ private void createRelayScript(Path nodeDir) throws IOException { relayScript.toFile().setExecutable(true); } + //Not used currently + /** + * Restart the Haskell relay node process after a Yano rollback. + * The chain-sync protocol stalls on deep rollbacks, so we kill the relay, + * delete the stale socket, and start a fresh relay that re-syncs from Yano's new tip. + */ + public void restartRelayNode(ClusterInfo clusterInfo, Path clusterFolder, Consumer writer) throws IOException, InterruptedException, ExecutionException, TimeoutException { + writer.accept(info("Restarting Haskell relay to re-sync after rollback...")); + + // Find and kill the node process (first in the list, identified by NODE_PROCESS_NAME pid file) + Process oldNodeProcess = null; + for (Process p : processes) { + if (p != null && p.isAlive()) { + // Check if this is the node process (not submit-api) + String cmdLine = p.info().commandLine().orElse(""); + if (cmdLine.contains("cardano-node")) { + oldNodeProcess = p; + break; + } + } + } + + if (oldNodeProcess != null) { + oldNodeProcess.descendants().forEach(ProcessHandle::destroyForcibly); + oldNodeProcess.destroyForcibly(); + oldNodeProcess.waitFor(15, TimeUnit.SECONDS); + processes.remove(oldNodeProcess); + } + + // Delete relay's DB and socket so it syncs fresh from Yano's new chain. + // The old DB has blocks from the pre-rollback fork that conflict with Yano's new chain. + Path nodeDir = clusterFolder.resolve(ClusterConfig.NODE_FOLDER_PREFIX); + Path dbPath = nodeDir.resolve("db"); + if (dbPath.toFile().exists()) { + FileUtils.deleteDirectory(dbPath.toFile()); + writer.accept(info("Cleared relay DB for fresh sync.")); + } + Path socketPath = nodeDir.resolve("node.sock"); + Files.deleteIfExists(socketPath); + + // Start a fresh relay + Process nodeProcess = startNode(clusterFolder, clusterInfo, true, writer); + if (nodeProcess != null) { + processes.add(nodeProcess); + writer.accept(success("Haskell relay restarted.")); + } else { + writer.accept(error("Failed to restart Haskell relay.")); + } + } + public boolean checkIfFirstRun(Path clusterFolder) { String node1Folder = ClusterConfig.NODE_FOLDER_PREFIX; Path db = clusterFolder.resolve(node1Folder).resolve("db"); @@ -424,6 +540,7 @@ public boolean isClusterRunning() { } } } - return false; + // In yano-only mode, the cluster is running if Yano is running + return yanoService.isRunning(); } } diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/NodeMode.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/NodeMode.java new file mode 100644 index 00000000..2e1f3ecc --- /dev/null +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/NodeMode.java @@ -0,0 +1,31 @@ +package com.bloxbean.cardano.yacicli.localcluster; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public enum NodeMode { + HASKELL_ONLY("haskell-only"), + COMPANION("companion"), + YANO_ONLY("yano-only"), + YANO_PRIMARY("yano-primary"); + + private final String value; + + NodeMode(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @JsonCreator + public static NodeMode fromValue(String value) { + if (value == null) return HASKELL_ONLY; + for (NodeMode mode : values()) { + if (mode.value.equals(value)) return mode; + } + return HASKELL_ONLY; + } +} diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/api/EpochController.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/api/EpochController.java index 3c74bd23..1a062394 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/api/EpochController.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/api/EpochController.java @@ -7,8 +7,11 @@ import com.bloxbean.cardano.yaci.core.protocol.localstate.queries.EpochNoQueryResult; import com.bloxbean.cardano.yaci.helper.LocalClientProvider; import com.bloxbean.cardano.yaci.helper.LocalStateQueryClient; +import com.bloxbean.cardano.yacicli.localcluster.ClusterInfoService; +import com.bloxbean.cardano.yacicli.localcluster.NodeMode; import com.bloxbean.cardano.yacicli.localcluster.common.LocalClientProviderHelper; import com.bloxbean.cardano.yacicli.localcluster.service.LocalProtocolParamSupplier; +import com.bloxbean.cardano.yacicli.localcluster.service.YanoHttpNodeService; import com.bloxbean.cardano.yacicli.common.CommandContext; import com.bloxbean.cardano.yacicli.localcluster.ClusterConfig; import io.swagger.v3.oas.annotations.Operation; @@ -27,15 +30,38 @@ public class EpochController { private LocalClientProviderHelper localQueryClientUtil; + private YanoHttpNodeService yanoHttpNodeService; + private ClusterInfoService clusterInfoService; - public EpochController(LocalClientProviderHelper localQueryClientUtil) { + public EpochController(LocalClientProviderHelper localQueryClientUtil, + YanoHttpNodeService yanoHttpNodeService, + ClusterInfoService clusterInfoService) { this.localQueryClientUtil = localQueryClientUtil; + this.yanoHttpNodeService = yanoHttpNodeService; + this.clusterInfoService = clusterInfoService; + } + + private boolean isYanoOnlyMode() { + try { + String clusterName = CommandContext.INSTANCE.getProperty(ClusterConfig.CLUSTER_NAME); + var info = clusterInfoService.getClusterInfo(clusterName); + return NodeMode.YANO_ONLY == info.getNodeMode(); + } catch (Exception e) { + return false; + } } @Operation(summary = "Retrieve the latest epoch.") @GetMapping("latest") public EpochContent getLatestEpoch() { String clusterName = CommandContext.INSTANCE.getProperty(ClusterConfig.CLUSTER_NAME); + + if (isYanoOnlyMode()) { + EpochContent result = yanoHttpNodeService.getLatestEpoch(clusterName); + if (result != null) return result; + throw new RuntimeException("Failed to get latest epoch from Yano"); + } + Era era = CommandContext.INSTANCE.getEra(); LocalClientProvider localClientProvider = null; @@ -59,6 +85,13 @@ public EpochContent getLatestEpoch() { @GetMapping("parameters") ProtocolParams getProtocolParameters() { String clusterName = CommandContext.INSTANCE.getProperty(ClusterConfig.CLUSTER_NAME); + + if (isYanoOnlyMode()) { + ProtocolParams result = yanoHttpNodeService.getProtocolParams(clusterName); + if (result != null) return result; + throw new RuntimeException("Failed to get protocol parameters from Yano"); + } + Era era = CommandContext.INSTANCE.getEra(); LocalClientProvider localClientProvider = null; diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/api/RollbackController.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/api/RollbackController.java index cfee0f78..b2aafc8f 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/api/RollbackController.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/api/RollbackController.java @@ -51,6 +51,27 @@ public String rollbackToDBSnapshot() { return sb.toString(); } + @PostMapping("/rollback") + @Operation(summary = "Rollback N blocks (yano-primary mode only)", + description = "Rollback the chain by the specified number of blocks via Yano's rollback API. " + + "Only available in yano-primary mode. Number of blocks must not exceed securityParam.") + public ResponseEntity rollback(@RequestBody RollbackRequest request) { + StringBuilder sb = new StringBuilder(); + boolean status = rollbackService.rollback(request.blocks(), msg -> sb.append(msg)); + + writeLn(sb.toString()); + if (status) { + return ResponseEntity.ok(sb.toString()); + } else { + return ResponseEntity.badRequest().body(sb.toString()); + } + } + + record RollbackRequest( + @Schema(description = "Number of blocks to rollback") + long blocks + ) {} + @PostMapping("/create-forks") @Operation(summary = "Create forks for rollback", description = "Create forks for rollback. This will detach the main node from the peer nodes and create" + diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/api/TransactionController.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/api/TransactionController.java index a51f6aad..f0901d73 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/api/TransactionController.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/api/TransactionController.java @@ -2,11 +2,16 @@ import com.bloxbean.cardano.client.backend.model.TransactionContent; import com.bloxbean.cardano.yaci.core.protocol.chainsync.messages.Point; +import com.bloxbean.cardano.yacicli.localcluster.ClusterConfig; +import com.bloxbean.cardano.yacicli.localcluster.ClusterInfoService; +import com.bloxbean.cardano.yacicli.localcluster.NodeMode; import com.bloxbean.cardano.yacicli.localcluster.service.ClusterUtilService; +import com.bloxbean.cardano.yacicli.common.CommandContext; import com.bloxbean.cardano.yacicli.common.Tuple; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -18,11 +23,26 @@ @RestController @RequestMapping(path = "/local-cluster/api") @Tag(name = "Transaction API", description = "Handles submission of transactions and simulate transaction lookups.") +@Slf4j public class TransactionController { private final ClusterUtilService clusterUtilService; + private final ClusterInfoService clusterInfoService; private RestTemplate restTemplate = new RestTemplate(); - private final String SUBMIT_API_URL = "http://localhost:8090/api/submit/tx"; + private static final String DEFAULT_SUBMIT_API_URL = "http://localhost:8090/api/submit/tx"; + + private String getSubmitUrl() { + try { + String clusterName = CommandContext.INSTANCE.getProperty(ClusterConfig.CLUSTER_NAME); + var info = clusterInfoService.getClusterInfo(clusterName); + if (NodeMode.YANO_ONLY == info.getNodeMode()) { + return "http://localhost:" + info.getYanoHttpPort() + "/api/v1/tx/submit"; + } + } catch (Exception e) { + log.debug("Error resolving submit URL, using default", e); + } + return DEFAULT_SUBMIT_API_URL; + } @Operation(summary = "Submit Transaction", description = "Submit a transaction in CBOR format to the cluster.", @@ -39,7 +59,7 @@ ResponseEntity submit(@RequestBody byte[] cborTx) { HttpEntity entity = new HttpEntity<>(cborTx, headers); try { ResponseEntity responseEntity = restTemplate - .exchange(SUBMIT_API_URL, HttpMethod.POST, entity, String.class); + .exchange(getSubmitUrl(), HttpMethod.POST, entity, String.class); return responseEntity; } catch (Exception e) { diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/api/service/TestTransactionService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/api/service/TestTransactionService.java index 1445ee6a..66b32a0c 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/api/service/TestTransactionService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/api/service/TestTransactionService.java @@ -13,6 +13,7 @@ import com.bloxbean.cardano.client.util.HexUtil; import com.bloxbean.cardano.yacicli.common.CommandContext; import com.bloxbean.cardano.yacicli.localcluster.ClusterConfig; +import com.bloxbean.cardano.yacicli.localcluster.NodeMode; import com.bloxbean.cardano.yacicli.localcluster.ClusterService; import com.bloxbean.cardano.yacicli.localcluster.service.DefaultAddressService; import lombok.RequiredArgsConstructor; @@ -136,9 +137,12 @@ private Optional getBackendService() { return Optional.empty(); } - var yaciStorePort = clusterInfo.getYaciStorePort(); - - String backendUrl = "http://localhost:" + yaciStorePort + "/api/v1/"; + String backendUrl; + if (NodeMode.YANO_ONLY == clusterInfo.getNodeMode()) { + backendUrl = "http://localhost:" + clusterInfo.getYanoHttpPort() + "/api/v1/"; + } else { + backendUrl = "http://localhost:" + clusterInfo.getYaciStorePort() + "/api/v1/"; + } return Optional.of(new BFBackendService(backendUrl, "dummy_key")); } catch (Exception e) { log.error("Error getting backend service", e); diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/commands/RollbackCommands.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/commands/RollbackCommands.java index 12259176..9507d643 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/commands/RollbackCommands.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/commands/RollbackCommands.java @@ -32,6 +32,13 @@ public void rollbackToLastDBSnapshot() { rollbackService.rollbackToLastDBSnapshot(msg -> writeLn(msg)); } + @ShellMethod(value = "Rollback N blocks (yano-primary mode only)", key = "rollback") + @ShellMethodAvailability("localClusterCmdAvailability") + public void rollback( + @ShellOption(value = {"--blocks"}, help = "Number of blocks to rollback") long blocks) { + rollbackService.rollback(blocks, msg -> writeLn(msg)); + } + @ShellMethod(value = "Create forks for rollback", key = "create-forks") @ShellMethodAvailability("localClusterCmdAvailability") public void createForks( 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 96babf4f..3a593736 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 @@ -2,6 +2,7 @@ import com.bloxbean.cardano.client.address.Address; import com.bloxbean.cardano.client.util.HexUtil; +import com.bloxbean.cardano.yacicli.localcluster.NodeMode; import jakarta.annotation.PostConstruct; import lombok.*; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -253,9 +254,9 @@ public class GenesisConfig { private long refScriptCostStride = 25600; private double refScriptCostMultiplier = 1.2; - // Node mode: companion (Yano bootstraps + Haskell takes over), + // Node mode: yano-primary (Yano BP + Haskell relay), companion (Yano bootstraps + Haskell takes over), // yano-only (Yano only, fastest), haskell-only (legacy, Haskell node only) - private String nodeMode = "haskell-only"; + private NodeMode nodeMode = NodeMode.HASKELL_ONLY; //Introduced for the issue https://github.com/bloxbean/yaci-devkit/issues/65 private int conwayHardForkAtEpoch = 0; @@ -659,7 +660,7 @@ public void merge(Map updatedValues) { constitutionScript = updatedValues.get("constitutionScript"); if (updatedValues.get("nodeMode") != null && !updatedValues.get("nodeMode").isEmpty()) - nodeMode = updatedValues.get("nodeMode"); + nodeMode = NodeMode.fromValue(updatedValues.get("nodeMode")); if (updatedValues.get("shiftStartTimeBehind") != null && !updatedValues.get("shiftStartTimeBehind").isEmpty()) shiftStartTimeBehind = Boolean.parseBoolean(updatedValues.get("shiftStartTimeBehind")); if (updatedValues.get("conwayHardForkAtEpoch") != null && !updatedValues.get("conwayHardForkAtEpoch").isEmpty()) diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/events/listeners/FirstRunPlutusV3CostModelUpdate.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/events/listeners/FirstRunPlutusV3CostModelUpdate.java index 898f7dad..a684d77e 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/events/listeners/FirstRunPlutusV3CostModelUpdate.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/events/listeners/FirstRunPlutusV3CostModelUpdate.java @@ -2,6 +2,7 @@ import com.bloxbean.cardano.yaci.core.protocol.localstate.api.Era; import com.bloxbean.cardano.yacicli.localcluster.ClusterService; +import com.bloxbean.cardano.yacicli.localcluster.NodeMode; import com.bloxbean.cardano.yacicli.localcluster.events.FirstRunDone; import com.bloxbean.cardano.yacicli.localcluster.service.AccountService; import com.bloxbean.cardano.yacicli.localcluster.service.ClusterUtilService; @@ -36,8 +37,9 @@ public void updatePlutusV3CostModel(FirstRunDone firstRunDone) { return; } - if (clusterInfo != null && "companion".equals(clusterInfo.getNodeMode())) { - writeLn(info("Skipping Plutus cost models update - already submitted via Yano companion bootstrap")); + NodeMode nodeMode = clusterInfo != null ? clusterInfo.getNodeMode() : null; + if (NodeMode.COMPANION == nodeMode || NodeMode.YANO_ONLY == nodeMode || NodeMode.YANO_PRIMARY == nodeMode) { + writeLn(info("Skipping Plutus cost models update - already submitted via Yano bootstrap")); return; } diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/events/listeners/FirstRunTopupAccounts.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/events/listeners/FirstRunTopupAccounts.java index 434bc424..d86f95dd 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/events/listeners/FirstRunTopupAccounts.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/events/listeners/FirstRunTopupAccounts.java @@ -2,6 +2,7 @@ import com.bloxbean.cardano.yaci.core.protocol.localstate.api.Era; import com.bloxbean.cardano.yacicli.localcluster.ClusterService; +import com.bloxbean.cardano.yacicli.localcluster.NodeMode; import com.bloxbean.cardano.yacicli.localcluster.events.FirstRunDone; import com.bloxbean.cardano.yacicli.localcluster.service.AccountService; import com.bloxbean.cardano.yacicli.localcluster.service.ClusterUtilService; @@ -34,7 +35,13 @@ public void topupInitialAccounts(FirstRunDone firstRunDone) { String clusterName = firstRunDone.getCluster(); var clusterInfo = localClusterService.getClusterInfo(clusterName); if (clusterInfo != null && !clusterInfo.isMasterNode()) { - //Return if it's a peer node, not the master node + return; + } + + if (clusterInfo != null && NodeMode.YANO_ONLY == clusterInfo.getNodeMode()) { + // In yano-only mode, no Haskell node socket — topup via local protocol not possible. + // Genesis UTXO keys already have funds. Custom topup can use Yano's HTTP API. + defaultAddressService.printDefaultAddresses(true); return; } diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/AccountService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/AccountService.java index 1574be82..908a1ea2 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/AccountService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/AccountService.java @@ -5,6 +5,7 @@ import com.bloxbean.cardano.yaci.core.protocol.localstate.api.Era; import com.bloxbean.cardano.yacicli.commands.common.RootLogService; import com.bloxbean.cardano.yacicli.localcluster.ClusterService; +import com.bloxbean.cardano.yacicli.localcluster.NodeMode; import com.bloxbean.cardano.yacicli.localcluster.common.GenesisUtil; import com.bloxbean.cardano.yacicli.localcluster.common.LocalClientProviderHelper; import lombok.extern.slf4j.Slf4j; @@ -29,24 +30,40 @@ public class AccountService { private final ClusterService clusterService; private final LocalClientProviderHelper localQueryClientUtil; private final RootLogService rootLogService; + private final YanoHttpNodeService yanoHttpNodeService; private final Path plutusCostModelsBasePath; public AccountService(ClusterService clusterService, LocalClientProviderHelper localQueryClientUtil, RootLogService rootLogService, + YanoHttpNodeService yanoHttpNodeService, @Value("${yaci.cli.plutus-costmodels-path:./config}") String plutusCostModelsBasePath) { this.clusterService = clusterService; this.localQueryClientUtil = localQueryClientUtil; this.rootLogService = rootLogService; + this.yanoHttpNodeService = yanoHttpNodeService; this.plutusCostModelsBasePath = Paths.get(plutusCostModelsBasePath); } + private boolean isYanoOnlyMode(String clusterName) { + try { + var info = clusterService.getClusterInfo(clusterName); + return NodeMode.YANO_ONLY == info.getNodeMode(); + } catch (Exception e) { + return false; + } + } + private Path resolveCostModelsFile(String clusterName) throws IOException { var clusterInfo = clusterService.getClusterInfo(clusterName); return GenesisUtil.resolveCostModelsFile(plutusCostModelsBasePath, clusterInfo.getProtocolMajorVer()); } public boolean topup(String clusterName, Era era, String address, double adaValue, Consumer writer) { + if (isYanoOnlyMode(clusterName)) { + return yanoHttpNodeService.topUp(clusterName, address, adaValue, writer); + } + Level orgLevel = rootLogService.getLogLevel(); if (!rootLogService.isDebugLevel()) rootLogService.setLogLevel(Level.OFF); @@ -71,6 +88,10 @@ public boolean topup(String clusterName, Era era, String address, double adaValu public boolean mint(String clusterName, Era era, String assetName, BigInteger quantity, String receiver, Consumer writer) { + if (isYanoOnlyMode(clusterName)) { + return yanoHttpNodeService.mint(clusterName, assetName, quantity, receiver, writer); + } + Level orgLevel = rootLogService.getLogLevel(); if (!rootLogService.isDebugLevel()) rootLogService.setLogLevel(Level.OFF); @@ -117,6 +138,10 @@ public boolean updateCostModels(String clusterName, Era era, Consumer wr } public Map> getUtxosAtDefaultAccounts(String clusterName, Era era, Consumer writer) { + if (isYanoOnlyMode(clusterName)) { + return yanoHttpNodeService.getFundsAtGenesisKeys(clusterName); + } + Level orgLevel = rootLogService.getLogLevel(); if (!rootLogService.isDebugLevel()) rootLogService.setLogLevel(Level.OFF); @@ -141,6 +166,10 @@ public Map> getUtxosAtDefaultAccounts(String clusterName, Era } public List getUtxos(String clusterName, Era era, String address, Consumer writer) { + if (isYanoOnlyMode(clusterName)) { + return yanoHttpNodeService.getUtxos(clusterName, address); + } + Level orgLevel = rootLogService.getLogLevel(); if (!rootLogService.isDebugLevel()) rootLogService.setLogLevel(Level.OFF); diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/ClusterUtilService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/ClusterUtilService.java index 4f367b81..cfb71d26 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/ClusterUtilService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/ClusterUtilService.java @@ -6,6 +6,7 @@ import com.bloxbean.cardano.yacicli.commands.common.RootLogService; import com.bloxbean.cardano.yacicli.localcluster.ClusterInfoService; import com.bloxbean.cardano.yacicli.localcluster.ClusterService; +import com.bloxbean.cardano.yacicli.localcluster.NodeMode; import com.bloxbean.cardano.yacicli.localcluster.common.LocalClientProviderHelper; import com.bloxbean.cardano.yacicli.common.CommandContext; import com.bloxbean.cardano.yacicli.common.Tuple; @@ -28,6 +29,17 @@ public class ClusterUtilService { private final ClusterInfoService clusterInfoService; private final LocalClientProviderHelper localQueryClientUtil; private final RootLogService rootLogService; + private final YanoHttpNodeService yanoHttpNodeService; + + private boolean isYanoOnlyMode() { + try { + String clusterName = CommandContext.INSTANCE.getProperty(ClusterConfig.CLUSTER_NAME); + var info = clusterInfoService.getClusterInfo(clusterName); + return NodeMode.YANO_ONLY == info.getNodeMode(); + } catch (Exception e) { + return false; + } + } public Tuple getTip(Consumer writer) { return getTip(writer, null); @@ -35,6 +47,11 @@ public Tuple getTip(Consumer writer) { public Tuple getTip(Consumer writer, String nodeName) { String clusterName = CommandContext.INSTANCE.getProperty(ClusterConfig.CLUSTER_NAME); + + if (isYanoOnlyMode()) { + return yanoHttpNodeService.getTip(clusterName); + } + Era era = CommandContext.INSTANCE.getEra(); LocalNodeService localNodeService = null; diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/RollbackService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/RollbackService.java index 9a21d740..4157b714 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/RollbackService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/RollbackService.java @@ -2,10 +2,13 @@ import com.bloxbean.cardano.yacicli.common.CommandContext; import com.bloxbean.cardano.yacicli.localcluster.ClusterConfig; +import com.bloxbean.cardano.yacicli.localcluster.ClusterInfoService; import com.bloxbean.cardano.yacicli.localcluster.ClusterService; +import com.bloxbean.cardano.yacicli.localcluster.NodeMode; import com.bloxbean.cardano.yacicli.localcluster.events.RollbackDone; import com.bloxbean.cardano.yacicli.localcluster.peer.LocalPeerService; import com.bloxbean.cardano.yacicli.localcluster.proxy.TcpProxyManager; +import com.bloxbean.cardano.yacicli.localcluster.yano.YanoBootstrapService; import com.bloxbean.cardano.yacicli.util.PortUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -26,10 +29,12 @@ public class RollbackService { public static final String DB = "db"; public static final String DB_ROLLBACK_POINT_FOLDER = "db_rollback_point"; private final ClusterService clusterService; + private final ClusterInfoService clusterInfoService; private final LocalPeerService localPeerService; private final ClusterUtilService clusterUtilService; private final ApplicationEventPublisher publisher; private final TcpProxyManager tcpProxyManager; + private final YanoBootstrapService yanoBootstrapService; public void takeDBSnapshot(Consumer writer) { String clusterName = CommandContext.INSTANCE.getProperty(ClusterConfig.CLUSTER_NAME); @@ -105,6 +110,49 @@ public boolean rollbackToLastDBSnapshot(Consumer writer) { } } + public boolean rollback(long blocks, Consumer writer) { + String clusterName = CommandContext.INSTANCE.getProperty(ClusterConfig.CLUSTER_NAME); + if (clusterName != null && !clusterName.equals("default")) { + writer.accept(error("Rollback cannot be performed in a non-default cluster.")); + return false; + } + + try { + var clusterInfo = clusterInfoService.getClusterInfo(clusterName); + if (NodeMode.YANO_PRIMARY != clusterInfo.getNodeMode()) { + writer.accept(error("The 'rollback' command is only available in yano-primary mode. Use 'rollback-to-db-snapshot' for other modes.")); + return false; + } + + if (blocks <= 0) { + writer.accept(error("Number of blocks must be positive.")); + return false; + } + + long securityParam = clusterInfo.getSecurityParam(); + if (blocks > securityParam) { + writer.accept(error("Cannot rollback more than %d blocks (securityParam). Requested: %d", securityParam, blocks)); + return false; + } + + int httpPort = clusterInfo.getYanoHttpPort(); + boolean success = yanoBootstrapService.rollback(httpPort, blocks, writer); + if (success) { + // Yano propagates the rollback via N2N to the Haskell relay. + // For deep rollbacks, the relay's chain-sync may stall, requiring a relay restart. + // Path clusterFolder = clusterInfoService.getClusterFolder(clusterName); + //clusterStartService.restartRelayNode(clusterInfo, clusterFolder, writer); + // Yaci Store re-syncs from the restarted relay + //publisher.publishEvent(new RollbackDone(clusterName)); + } + return success; + } catch (Exception e) { + writer.accept(error("Rollback failed: " + e.getMessage())); + log.error("Error during Yano rollback", e); + return false; + } + } + public boolean createForks(boolean restartNode, long waitInSecBeforeRestart, Consumer writer) { String clusterName = CommandContext.INSTANCE.getProperty(ClusterConfig.CLUSTER_NAME); if (clusterName != null && !clusterName.equals("default")) { diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/YanoHttpNodeService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/YanoHttpNodeService.java new file mode 100644 index 00000000..1442c1af --- /dev/null +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/YanoHttpNodeService.java @@ -0,0 +1,270 @@ +package com.bloxbean.cardano.yacicli.localcluster.service; + +import com.bloxbean.cardano.client.address.Address; +import com.bloxbean.cardano.client.address.AddressProvider; +import com.bloxbean.cardano.client.api.ProtocolParamsSupplier; +import com.bloxbean.cardano.client.api.UtxoSupplier; +import com.bloxbean.cardano.client.api.model.Amount; +import com.bloxbean.cardano.client.api.model.ProtocolParams; +import com.bloxbean.cardano.client.api.model.Result; +import com.bloxbean.cardano.client.api.model.Utxo; +import com.bloxbean.cardano.client.backend.api.BackendService; +import com.bloxbean.cardano.client.backend.api.DefaultProtocolParamsSupplier; +import com.bloxbean.cardano.client.backend.api.DefaultUtxoSupplier; +import com.bloxbean.cardano.client.backend.blockfrost.service.BFBackendService; +import com.bloxbean.cardano.client.backend.model.EpochContent; +import com.bloxbean.cardano.client.cip.cip20.MessageMetadata; +import com.bloxbean.cardano.client.common.model.Networks; +import com.bloxbean.cardano.client.crypto.KeyGenUtil; +import com.bloxbean.cardano.client.crypto.SecretKey; +import com.bloxbean.cardano.client.crypto.VerificationKey; +import com.bloxbean.cardano.client.crypto.bip32.key.HdPublicKey; +import com.bloxbean.cardano.client.function.helper.SignerProviders; +import com.bloxbean.cardano.client.quicktx.QuickTxBuilder; +import com.bloxbean.cardano.client.quicktx.Tx; +import com.bloxbean.cardano.client.transaction.spec.Asset; +import com.bloxbean.cardano.client.transaction.spec.script.ScriptPubkey; +import com.bloxbean.cardano.yaci.core.protocol.chainsync.messages.Point; +import com.bloxbean.cardano.yacicli.localcluster.ClusterInfo; +import com.bloxbean.cardano.yacicli.localcluster.ClusterService; +import com.bloxbean.cardano.yacicli.localcluster.common.GenesisUtil; +import com.bloxbean.cardano.yacicli.localcluster.yano.YanoBootstrapService; +import com.bloxbean.cardano.yacicli.common.Tuple; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.file.Path; +import java.util.*; +import java.util.function.Consumer; + +import static com.bloxbean.cardano.client.common.CardanoConstants.LOVELACE; +import static com.bloxbean.cardano.yacicli.util.AdaConversionUtil.adaToLovelace; +import static com.bloxbean.cardano.yacicli.util.ConsoleWriter.*; + +/** + * Provides LocalNodeService-equivalent operations using Yano's Blockfrost-compatible HTTP API. + * Used in yano-only mode where there is no Haskell node or N2C socket. + */ +@Component +@Slf4j +public class YanoHttpNodeService { + private final ClusterService clusterService; + private final YanoBootstrapService yanoBootstrapService; + + public YanoHttpNodeService(ClusterService clusterService, YanoBootstrapService yanoBootstrapService) { + this.clusterService = clusterService; + this.yanoBootstrapService = yanoBootstrapService; + } + + private BackendService getBackendService(String clusterName) throws IOException { + ClusterInfo info = clusterService.getClusterInfo(clusterName); + String url = "http://localhost:" + info.getYanoHttpPort() + "/api/v1/"; + return new BFBackendService(url, "dummy_key"); + } + + private int getYanoHttpPort(String clusterName) throws IOException { + return clusterService.getClusterInfo(clusterName).getYanoHttpPort(); + } + + public List getUtxos(String clusterName, String address) { + try { + BackendService backendService = getBackendService(clusterName); + UtxoSupplier utxoSupplier = new DefaultUtxoSupplier(backendService.getUtxoService()); + return utxoSupplier.getAll(address); + } catch (Exception e) { + log.error("Error getting UTXOs via Yano HTTP", e); + return Collections.emptyList(); + } + } + + public Map> getFundsAtGenesisKeys(String clusterName) { + try { + Path clusterFolder = clusterService.getClusterFolder(clusterName); + List> utxoKeys = GenesisUtil.loadUtxoKeys(clusterFolder); + BackendService backendService = getBackendService(clusterName); + UtxoSupplier utxoSupplier = new DefaultUtxoSupplier(backendService.getUtxoService()); + + Map> utxosMap = new LinkedHashMap<>(); + for (Tuple tuple : utxoKeys) { + HdPublicKey hdPublicKey = new HdPublicKey(); + hdPublicKey.setKeyData(tuple._1.getBytes()); + Address address = AddressProvider.getEntAddress(hdPublicKey, Networks.testnet()); + String addr = address.toBech32(); + List utxos = utxoSupplier.getAll(addr); + utxosMap.put(addr, utxos); + } + return utxosMap; + } catch (Exception e) { + log.error("Error getting genesis key UTXOs via Yano HTTP", e); + return Collections.emptyMap(); + } + } + + public boolean topUp(String clusterName, String receiver, double adaAmount, Consumer writer) { + try { + Path clusterFolder = clusterService.getClusterFolder(clusterName); + List> utxoKeys = GenesisUtil.loadUtxoKeys(clusterFolder); + BackendService backendService = getBackendService(clusterName); + UtxoSupplier utxoSupplier = new DefaultUtxoSupplier(backendService.getUtxoService()); + + BigInteger amount = adaToLovelace(adaAmount); + String senderAddress = null; + SecretKey senderSkey = null; + + for (int i = 0; i < utxoKeys.size(); i++) { + Tuple tuple = utxoKeys.get(i); + HdPublicKey hdPublicKey = new HdPublicKey(); + hdPublicKey.setKeyData(tuple._1.getBytes()); + Address address = AddressProvider.getEntAddress(hdPublicKey, Networks.testnet()); + String addr = address.toBech32(); + + Optional sufficient = utxoSupplier.getAll(addr).stream() + .flatMap(utxo -> utxo.getAmount().stream()) + .filter(amt -> LOVELACE.equals(amt.getUnit()) && amt.getQuantity().compareTo(amount) > 0) + .findAny(); + + if (sufficient.isPresent()) { + senderAddress = addr; + senderSkey = tuple._2; + break; + } + } + + if (senderAddress == null) { + writer.accept(error("No funded UTXO key found for topup via Yano")); + return false; + } + + QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService); + Tx tx = new Tx() + .payToAddress(receiver, Amount.lovelace(amount)) + .attachMetadata(MessageMetadata.create().add("Topup Fund")) + .from(senderAddress); + + Result result = quickTxBuilder.compose(tx) + .withSigner(SignerProviders.signerFrom(senderSkey)) + .complete(); + + if (!result.isSuccessful()) { + writer.accept(error("Topup TX failed: " + result.getResponse())); + return false; + } + + writer.accept(success("Transaction submitted successfully")); + writer.accept(infoLabel("Txn# : ", result.getValue())); + return true; + } catch (Exception e) { + log.error("Error during topup via Yano HTTP", e); + writer.accept(error("Topup error: " + e.getMessage())); + return false; + } + } + + public boolean mint(String clusterName, String assetName, BigInteger quantity, String receiver, Consumer writer) { + try { + Path clusterFolder = clusterService.getClusterFolder(clusterName); + List> utxoKeys = GenesisUtil.loadUtxoKeys(clusterFolder); + BackendService backendService = getBackendService(clusterName); + UtxoSupplier utxoSupplier = new DefaultUtxoSupplier(backendService.getUtxoService()); + + String senderAddress = null; + SecretKey senderSkey = null; + + for (int i = 0; i < utxoKeys.size(); i++) { + Tuple tuple = utxoKeys.get(i); + HdPublicKey hdPublicKey = new HdPublicKey(); + hdPublicKey.setKeyData(tuple._1.getBytes()); + Address address = AddressProvider.getEntAddress(hdPublicKey, Networks.testnet()); + String addr = address.toBech32(); + + Optional sufficient = utxoSupplier.getAll(addr).stream() + .flatMap(utxo -> utxo.getAmount().stream()) + .filter(amt -> LOVELACE.equals(amt.getUnit()) && amt.getQuantity().compareTo(quantity) > 0) + .findAny(); + + if (sufficient.isPresent()) { + senderAddress = addr; + senderSkey = tuple._2; + break; + } + } + + if (senderAddress == null) { + writer.accept(error("No funded UTXO key found for mint via Yano")); + return false; + } + + var verificationKey = KeyGenUtil.getPublicKeyFromPrivateKey(senderSkey); + var scriptPubkey = ScriptPubkey.create(verificationKey); + + QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService); + Tx tx = new Tx() + .mintAssets(scriptPubkey, Asset.builder() + .name(assetName) + .value(quantity) + .build(), receiver) + .payToAddress(senderAddress, Amount.ada(1)) + .from(senderAddress); + + Result result = quickTxBuilder.compose(tx) + .withSigner(SignerProviders.signerFrom(senderSkey)) + .complete(); + + if (!result.isSuccessful()) { + writer.accept(error("Mint TX failed: " + result.getResponse())); + return false; + } + + writer.accept(success("Transaction submitted successfully")); + writer.accept(info("Txn# : " + result.getValue())); + return true; + } catch (Exception e) { + log.error("Error during mint via Yano HTTP", e); + writer.accept(error("Mint error: " + e.getMessage())); + return false; + } + } + + public Tuple getTip(String clusterName) { + try { + int httpPort = getYanoHttpPort(clusterName); + JsonNode tip = yanoBootstrapService.getChainTip(httpPort); + if (tip != null) { + long height = tip.has("height") ? tip.get("height").asLong(0) : 0; + long slot = tip.has("slot") ? tip.get("slot").asLong(0) : 0; + String hash = tip.has("hash") ? tip.get("hash").asText("") : ""; + return new Tuple<>(height, new Point(slot, hash)); + } + } catch (Exception e) { + log.error("Error getting tip via Yano HTTP", e); + } + return null; + } + + public EpochContent getLatestEpoch(String clusterName) { + try { + BackendService backendService = getBackendService(clusterName); + Result result = backendService.getEpochService().getLatestEpoch(); + if (result.isSuccessful()) { + return result.getValue(); + } + } catch (Exception e) { + log.error("Error getting latest epoch via Yano HTTP", e); + } + return null; + } + + public ProtocolParams getProtocolParams(String clusterName) { + try { + BackendService backendService = getBackendService(clusterName); + ProtocolParamsSupplier supplier = new DefaultProtocolParamsSupplier(backendService.getEpochService()); + return supplier.getProtocolParams(); + } catch (Exception e) { + log.error("Error getting protocol params via Yano HTTP", e); + } + return null; + } +} diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yacistore/YaciStoreConfigBuilder.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yacistore/YaciStoreConfigBuilder.java index 4bf5e31b..2300348f 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yacistore/YaciStoreConfigBuilder.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yacistore/YaciStoreConfigBuilder.java @@ -2,6 +2,7 @@ import com.bloxbean.cardano.yacicli.localcluster.ClusterConfig; import com.bloxbean.cardano.yacicli.localcluster.ClusterInfo; +import com.bloxbean.cardano.yacicli.localcluster.NodeMode; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -32,12 +33,24 @@ public boolean build(ClusterInfo clusterInfo) { Map storeProperties = new LinkedHashMap(); storeProperties.put("server.port", String.valueOf(clusterInfo.getYaciStorePort())); + boolean yanoOnly = NodeMode.YANO_ONLY == clusterInfo.getNodeMode(); + storeProperties.put("store.cardano.host", "localhost"); - storeProperties.put("store.cardano.port", String.valueOf(clusterInfo.getNodePort())); + if (yanoOnly) { + // Connect to Yano's N2N port instead of Haskell node + storeProperties.put("store.cardano.port", String.valueOf(clusterInfo.getYanoServerPort())); + } else { + storeProperties.put("store.cardano.port", String.valueOf(clusterInfo.getNodePort())); + storeProperties.put("store.cardano.n2c-node-socket-path", clusterInfo.getSocketPath()); + } storeProperties.put("store.cardano.protocol-magic", String.valueOf(clusterInfo.getProtocolMagic())); - storeProperties.put("store.cardano.n2c-node-socket-path", clusterInfo.getSocketPath()); - storeProperties.put("store.cardano.submit-api-url", "http://localhost:" + clusterInfo.getSubmitApiPort() + "/api/submit/tx"); + if (yanoOnly) { + // Yano's HTTP API handles tx submission + storeProperties.put("store.cardano.submit-api-url", "http://localhost:" + clusterInfo.getYanoHttpPort() + "/api/v1/tx/submit"); + } else { + storeProperties.put("store.cardano.submit-api-url", "http://localhost:" + clusterInfo.getSubmitApiPort() + "/api/submit/tx"); + } storeProperties.put("store.cardano.ogmios-url", "http://localhost:" + clusterInfo.getOgmiosPort()); storeProperties.put("spring.datasource.url", "jdbc:h2:file:" + nodeFolder + "/yaci_store/storedb;MV_STORE=TRUE;AUTO_SERVER=TRUE;AUTO_RECONNECT=TRUE;LOCK_TIMEOUT=120000"); storeProperties.put("spring.datasource.username", "sa"); @@ -66,10 +79,23 @@ public boolean build(ClusterInfo clusterInfo) { storeProperties.put("store.live.enabled", "true"); - storeProperties.put("store.epoch.endpoints.epoch.local.enabled", "true"); + if (yanoOnly) { + // Explicitly disable N2C-based local epoch endpoint — no Haskell node socket in yano-only mode. + // The n2c profile (baked into Yaci Store binary) sets this to true by default, + // so we must explicitly override it to false. + storeProperties.put("store.epoch.endpoints.epoch.local.enabled", "false"); + } else { + storeProperties.put("store.epoch.endpoints.epoch.local.enabled", "true"); + } storeProperties.put("spring.batch.job.enabled", "false"); + if (yanoOnly) { + // Disable MCP server in yano-only mode — McpDAppRegistryService fails with custom + // protocol magic (NetworkType.fromProtocolMagic returns null for devnet magic 42) + storeProperties.put("yaci.store.mcp-server.enabled", "false"); + } + Path yaciStoreConfigPath = Path.of(clusterConfig.getYaciStoreBinPath(), "config", "application.properties"); Path configFolder = yaciStoreConfigPath.getParent(); @@ -83,12 +109,29 @@ public boolean build(ClusterInfo clusterInfo) { writer.write(key + "=" + value); writer.newLine(); } - return true; } catch (IOException e) { e.printStackTrace(); error("Error creating Yaci Store configuration file: " + e.getMessage()); return false; } + // In yano-only mode, write a profile-specific override to disable N2C local epoch endpoint. + // The n2c profile (baked into Yaci Store binary) bundles application-n2c.properties which sets + // store.epoch.endpoints.epoch.local.enabled=true. Profile-specific properties inside the binary + // override non-profile application.properties. An external application-n2c.properties in the + // config/ folder takes precedence over the bundled one. + if (yanoOnly) { + Path n2cOverridePath = configFolder.resolve("application-n2c.properties"); + try (BufferedWriter writer = Files.newBufferedWriter(n2cOverridePath)) { + writer.write("store.epoch.endpoints.epoch.local.enabled=false"); + writer.newLine(); + } catch (IOException e) { + e.printStackTrace(); + error("Error creating N2C override config: " + e.getMessage()); + } + } + + return true; + } } diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yacistore/YaciStoreService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yacistore/YaciStoreService.java index 03d123fd..73a90c4f 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yacistore/YaciStoreService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yacistore/YaciStoreService.java @@ -7,6 +7,7 @@ import com.bloxbean.cardano.yacicli.localcluster.ClusterInfo; import com.bloxbean.cardano.yacicli.localcluster.ClusterService; import com.bloxbean.cardano.yacicli.localcluster.ClusterStartService; +import com.bloxbean.cardano.yacicli.localcluster.NodeMode; import com.bloxbean.cardano.yacicli.localcluster.config.ApplicationConfig; import com.bloxbean.cardano.yacicli.localcluster.events.ClusterDeleted; import com.bloxbean.cardano.yacicli.localcluster.events.ClusterStarted; @@ -116,13 +117,27 @@ private Process startStoreApp(ClusterInfo clusterInfo, Era era) throws IOExcepti ProcessBuilder builder = new ProcessBuilder(); builder.directory(new File(clusterConfig.getYaciStoreBinPath())); - if (yaciStoreMode == null || yaciStoreMode.equals("java")) { + boolean yanoOnly = NodeMode.YANO_ONLY == clusterInfo.getNodeMode(); + + // In yano-only mode, force Java mode if jar is available. + // The native binary bakes in the n2c profile at compile time, making it impossible + // to disable LocalEpochController at runtime. Java mode respects runtime config. + String effectiveMode = yaciStoreMode; + if (yanoOnly && "native".equals(effectiveMode)) { + Path yaciStoreJar = Path.of(clusterConfig.getYaciStoreBinPath(), "yaci-store.jar"); + if (yaciStoreJar.toFile().exists()) { + effectiveMode = "java"; + writeLn(info("Yano-only mode: using Yaci Store JAR (N2C disabled)")); + } + } + + if (effectiveMode == null || effectiveMode.equals("java")) { Path yaciStoreJar = Path.of(clusterConfig.getYaciStoreBinPath(), "yaci-store.jar"); if (!yaciStoreJar.toFile().exists()) { writeLn(error("yaci-store.jar is not found at " + clusterConfig.getYaciStoreBinPath())); return null; } - } else if (yaciStoreMode != null && yaciStoreMode.equals("native")) { + } else if (effectiveMode.equals("native")) { Path yaciStoreBin = Path.of(clusterConfig.getYaciStoreBinPath(), "yaci-store"); if (!yaciStoreBin.toFile().exists()) { writeLn(error("yaci-store binary is not found at " + clusterConfig.getYaciStoreBinPath())); @@ -134,7 +149,7 @@ private Process startStoreApp(ClusterInfo clusterInfo, Era era) throws IOExcepti yaciStoreConfigBuilder.build(clusterInfo); } - if (yaciStoreMode != null && yaciStoreMode.equals("native")) { + if (effectiveMode != null && effectiveMode.equals("native")) { builder.environment().put("STORE_CARDANO_N2C_ERA", era.name()); builder.environment().put("STORE_CARDANO_PROTOCOL_MAGIC", String.valueOf(clusterInfo.getProtocolMagic())); if (OSUtil.getOperatingSystem() == OSUtil.OS.WINDOWS) { @@ -145,11 +160,18 @@ private Process startStoreApp(ClusterInfo clusterInfo, Era era) throws IOExcepti } else { String javaExecPath = jreResolver.getJavaCommand(); - if (OSUtil.getOperatingSystem() == OSUtil.OS.WINDOWS) { - builder.command(javaExecPath, "-Dstore.cardano.n2c-era=" + era.name(), "-Dstore.cardano.protocol-magic=" + clusterInfo.getProtocolMagic(), "-jar", clusterConfig.getYaciStoreBinPath() + File.separator + "yaci-store.jar"); - } else { - builder.command(javaExecPath, "-Dstore.cardano.n2c-era=" + era.name(), "-Dstore.cardano.protocol-magic=" + clusterInfo.getProtocolMagic(), "-jar", clusterConfig.getYaciStoreBinPath() + File.separator + "yaci-store.jar"); + List cmd = new ArrayList<>(); + cmd.add(javaExecPath); + if (!yanoOnly) { + cmd.add("-Dstore.cardano.n2c-era=" + era.name()); + } + cmd.add("-Dstore.cardano.protocol-magic=" + clusterInfo.getProtocolMagic()); + if (yanoOnly) { + cmd.add("-Dstore.epoch.endpoints.epoch.local.enabled=false"); } + cmd.add("-jar"); + cmd.add(clusterConfig.getYaciStoreBinPath() + File.separator + "yaci-store.jar"); + builder.command(cmd); writeLn(info("Java Path: " + javaExecPath)); } diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoBootstrapService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoBootstrapService.java index eb2c0f9a..43111089 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoBootstrapService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoBootstrapService.java @@ -173,6 +173,32 @@ public JsonNode getEpochNonce(int httpPort) { return null; } + public boolean rollback(int httpPort, long blocks, Consumer writer) { + String url = "http://localhost:" + httpPort + "/api/v1/devnet/rollback"; + try { + String body = objectMapper.writeValueAsString(Map.of("count", blocks)); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(Duration.ofSeconds(30)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() == 200) { + writer.accept(success("Rollback of %d blocks completed successfully", blocks)); + log.debug("Rollback response: {}", response.body()); + return true; + } else { + writer.accept(error("Failed to rollback: HTTP %d - %s", response.statusCode(), response.body())); + return false; + } + } catch (Exception e) { + writer.accept(error("Error during rollback: " + e.getMessage())); + return false; + } + } + public JsonNode getChainTip(int httpPort) { String url = "http://localhost:" + httpPort + "/api/v1/blocks/latest"; try { diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoCompanionService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoCompanionService.java index a08aa0b3..3b562ef7 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoCompanionService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoCompanionService.java @@ -90,7 +90,7 @@ public boolean bootstrap(ClusterInfo clusterInfo, Path clusterFolder, Consumer writer) throws IOException { + public void syncYanoGenesisToHaskellNode(ClusterInfo clusterInfo, Path clusterFolder, Consumer writer) throws IOException { Path yanoConfigDir = clusterFolder.resolve("yano-config").resolve("network").resolve("devnet"); Path haskellGenesisDir = clusterFolder.resolve("node").resolve("genesis"); @@ -168,7 +168,7 @@ private void syncShiftedGenesisToHaskellNode(ClusterInfo clusterInfo, Path clust * Update the Haskell node's topology.json to peer with Yano. * Backs up original topology first. */ - private void updateTopologyForCompanionMode(ClusterInfo clusterInfo, Path clusterFolder, Consumer writer) throws IOException { + public void updateTopologyForYanoPeering(ClusterInfo clusterInfo, Path clusterFolder, Consumer writer) throws IOException { Path topologyPath = clusterFolder.resolve("node").resolve("topology.json"); Path topologyBackup = clusterFolder.resolve("node").resolve("topology-original.json"); From ca372d0523245d6cdf1c5995fefd1d1cdb03caeb Mon Sep 17 00:00:00 2001 From: Satya Date: Sat, 9 May 2026 22:31:17 +0800 Subject: [PATCH 05/17] Add updated logo --- README.md | 2 +- static/DevKit-logo.svg | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 static/DevKit-logo.svg diff --git a/README.md b/README.md index 1395002c..9a082f2d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- +

Complete Cardano development environment with instant local devnet

diff --git a/static/DevKit-logo.svg b/static/DevKit-logo.svg new file mode 100644 index 00000000..dcd79b5f --- /dev/null +++ b/static/DevKit-logo.svg @@ -0,0 +1,7 @@ + + + + YaciDevKit \ No newline at end of file From 5cb56e903817ed6e087465307e7d2b465842d748 Mon Sep 17 00:00:00 2001 From: Satya Date: Mon, 11 May 2026 19:23:20 +0800 Subject: [PATCH 06/17] fix: Update yaci.node.* config to yano.* --- README.md | 2 +- .../localcluster/yano/YanoConfigBuilder.java | 24 +++++++++---------- .../localcluster/yano/YanoService.java | 24 +++++++++---------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 9a082f2d..80a39903 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@
- +

Yaci DevKit logo

Complete Cardano development environment with instant local devnet

diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoConfigBuilder.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoConfigBuilder.java index 39eb4932..1478101f 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoConfigBuilder.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoConfigBuilder.java @@ -46,27 +46,27 @@ public boolean build(ClusterInfo clusterInfo, Path yanoConfigDir, Path yanoDataD props.put("quarkus.http.port", String.valueOf(clusterInfo.getYanoHttpPort())); // Network - props.put("yaci.node.remote.protocol-magic", String.valueOf(clusterInfo.getProtocolMagic())); - props.put("yaci.node.server.port", String.valueOf(clusterInfo.getYanoServerPort())); + props.put("yano.remote.protocol-magic", String.valueOf(clusterInfo.getProtocolMagic())); + props.put("yano.server.port", String.valueOf(clusterInfo.getYanoServerPort())); // Genesis files - props.put("yaci.node.genesis.shelley-genesis-file", yanoConfigDir.resolve("shelley-genesis.json").toAbsolutePath().toString()); - props.put("yaci.node.genesis.byron-genesis-file", yanoConfigDir.resolve("byron-genesis.json").toAbsolutePath().toString()); - props.put("yaci.node.genesis.alonzo-genesis-file", yanoConfigDir.resolve("alonzo-genesis.json").toAbsolutePath().toString()); - props.put("yaci.node.genesis.conway-genesis-file", yanoConfigDir.resolve("conway-genesis.json").toAbsolutePath().toString()); - props.put("yaci.node.genesis.protocol-parameters-file", yanoConfigDir.resolve("protocol-param.json").toAbsolutePath().toString()); + props.put("yano.genesis.shelley-genesis-file", yanoConfigDir.resolve("shelley-genesis.json").toAbsolutePath().toString()); + props.put("yano.genesis.byron-genesis-file", yanoConfigDir.resolve("byron-genesis.json").toAbsolutePath().toString()); + props.put("yano.genesis.alonzo-genesis-file", yanoConfigDir.resolve("alonzo-genesis.json").toAbsolutePath().toString()); + props.put("yano.genesis.conway-genesis-file", yanoConfigDir.resolve("conway-genesis.json").toAbsolutePath().toString()); + props.put("yano.genesis.protocol-parameters-file", yanoConfigDir.resolve("protocol-param.json").toAbsolutePath().toString()); // Block producer keys - props.put("yaci.node.block-producer.vrf-skey-file", yanoConfigDir.resolve("vrf.skey").toAbsolutePath().toString()); - props.put("yaci.node.block-producer.kes-skey-file", yanoConfigDir.resolve("kes.skey").toAbsolutePath().toString()); - props.put("yaci.node.block-producer.opcert-file", yanoConfigDir.resolve("opcert.cert").toAbsolutePath().toString()); + props.put("yano.block-producer.vrf-skey-file", yanoConfigDir.resolve("vrf.skey").toAbsolutePath().toString()); + props.put("yano.block-producer.kes-skey-file", yanoConfigDir.resolve("kes.skey").toAbsolutePath().toString()); + props.put("yano.block-producer.opcert-file", yanoConfigDir.resolve("opcert.cert").toAbsolutePath().toString()); // Storage — inside the node folder so it gets cleaned up with create-node -o - props.put("yaci.node.storage.path", yanoDataDir.toAbsolutePath().toString()); + props.put("yano.storage.path", yanoDataDir.toAbsolutePath().toString()); // Past-time-travel mode if (pastTimeTravelMode) { - props.put("yaci.node.block-producer.past-time-travel-mode", "true"); + props.put("yano.block-producer.past-time-travel-mode", "true"); } // Write to {yanoHome}/config/application.properties diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoService.java index a01b7d42..113311a3 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoService.java @@ -252,21 +252,21 @@ private Process startYanoProcess(ClusterInfo clusterInfo, Path clusterFolder, Pa // Keep them for runtime guarantee with native binaries var env = builder.environment(); env.put("QUARKUS_PROFILE", "devnet"); - env.put("YACI_NODE_REMOTE_PROTOCOL_MAGIC", String.valueOf(clusterInfo.getProtocolMagic())); - env.put("YACI_NODE_SERVER_PORT", String.valueOf(clusterInfo.getYanoServerPort())); + env.put("YANO_REMOTE_PROTOCOL_MAGIC", String.valueOf(clusterInfo.getProtocolMagic())); + env.put("YANO_SERVER_PORT", String.valueOf(clusterInfo.getYanoServerPort())); env.put("QUARKUS_HTTP_PORT", String.valueOf(clusterInfo.getYanoHttpPort())); - env.put("YACI_NODE_GENESIS_SHELLEY_GENESIS_FILE", yanoConfigDir.resolve("shelley-genesis.json").toAbsolutePath().toString()); - env.put("YACI_NODE_GENESIS_BYRON_GENESIS_FILE", yanoConfigDir.resolve("byron-genesis.json").toAbsolutePath().toString()); - env.put("YACI_NODE_GENESIS_ALONZO_GENESIS_FILE", yanoConfigDir.resolve("alonzo-genesis.json").toAbsolutePath().toString()); - env.put("YACI_NODE_GENESIS_CONWAY_GENESIS_FILE", yanoConfigDir.resolve("conway-genesis.json").toAbsolutePath().toString()); - env.put("YACI_NODE_GENESIS_PROTOCOL_PARAMETERS_FILE", yanoConfigDir.resolve("protocol-param.json").toAbsolutePath().toString()); - env.put("YACI_NODE_BLOCK_PRODUCER_VRF_SKEY_FILE", yanoConfigDir.resolve("vrf.skey").toAbsolutePath().toString()); - env.put("YACI_NODE_BLOCK_PRODUCER_KES_SKEY_FILE", yanoConfigDir.resolve("kes.skey").toAbsolutePath().toString()); - env.put("YACI_NODE_BLOCK_PRODUCER_OPCERT_FILE", yanoConfigDir.resolve("opcert.cert").toAbsolutePath().toString()); - env.put("YACI_NODE_STORAGE_PATH", yanoDataDir.toAbsolutePath().toString()); + env.put("YANO_GENESIS_SHELLEY_GENESIS_FILE", yanoConfigDir.resolve("shelley-genesis.json").toAbsolutePath().toString()); + env.put("YANO_GENESIS_BYRON_GENESIS_FILE", yanoConfigDir.resolve("byron-genesis.json").toAbsolutePath().toString()); + env.put("YANO_GENESIS_ALONZO_GENESIS_FILE", yanoConfigDir.resolve("alonzo-genesis.json").toAbsolutePath().toString()); + env.put("YANO_GENESIS_CONWAY_GENESIS_FILE", yanoConfigDir.resolve("conway-genesis.json").toAbsolutePath().toString()); + env.put("YANO_GENESIS_PROTOCOL_PARAMETERS_FILE", yanoConfigDir.resolve("protocol-param.json").toAbsolutePath().toString()); + env.put("YANO_BLOCK_PRODUCER_VRF_SKEY_FILE", yanoConfigDir.resolve("vrf.skey").toAbsolutePath().toString()); + env.put("YANO_BLOCK_PRODUCER_KES_SKEY_FILE", yanoConfigDir.resolve("kes.skey").toAbsolutePath().toString()); + env.put("YANO_BLOCK_PRODUCER_OPCERT_FILE", yanoConfigDir.resolve("opcert.cert").toAbsolutePath().toString()); + env.put("YANO_STORAGE_PATH", yanoDataDir.toAbsolutePath().toString()); if (pastTimeTravelMode) { - env.put("YACI_NODE_BLOCK_PRODUCER_PAST_TIME_TRAVEL_MODE", "true"); + env.put("YANO_BLOCK_PRODUCER_PAST_TIME_TRAVEL_MODE", "true"); } builder.command(yanoBin.toAbsolutePath().toString()); From 14a417a91e8218c256b21288e2320047ae2f8e0c Mon Sep 17 00:00:00 2001 From: Satya Date: Mon, 11 May 2026 19:24:03 +0800 Subject: [PATCH 07/17] Removed hardcoded 30sec delay between Yano start and Haskell sync --- .../yano/YanoCompanionService.java | 52 ++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoCompanionService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoCompanionService.java index 3b562ef7..0f24472e 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoCompanionService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoCompanionService.java @@ -1,6 +1,9 @@ package com.bloxbean.cardano.yacicli.localcluster.yano; +import com.bloxbean.cardano.yacicli.common.Tuple; import com.bloxbean.cardano.yacicli.localcluster.ClusterInfo; +import com.bloxbean.cardano.yacicli.localcluster.service.ClusterUtilService; +import com.bloxbean.cardano.yaci.core.protocol.chainsync.messages.Point; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -8,6 +11,7 @@ import com.bloxbean.cardano.yacicli.common.AnsiColors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.stereotype.Component; import com.bloxbean.cardano.client.crypto.Blake2bUtil; @@ -40,6 +44,10 @@ public class YanoCompanionService { private final YanoService yanoService; private final YanoBootstrapService yanoBootstrapService; private final YanoGovernanceService yanoGovernanceService; + // ObjectProvider to break the constructor cycle: + // ClusterService -> ClusterStartService -> YanoCompanionService -> ClusterUtilService -> ClusterService. + // The bean is resolved lazily at call time (see performHandover). + private final ObjectProvider clusterUtilServiceProvider; private final ObjectMapper objectMapper = new ObjectMapper(); /** @@ -219,12 +227,44 @@ public void performHandover(ClusterInfo clusterInfo, Path clusterFolder, Consume Thread.sleep(1000); } - // Wait for Haskell relay to sync Yano's chain. - // With epoch-length 600 and 3 epoch shift, Yano produces ~1800 blocks. - // Syncing locally typically takes 10-20 seconds. - // Stability window = 3k/f = 300 slots, so we have ~300s of margin. - writer.accept(info("Waiting 30s for relay to sync Yano's chain...")); - Thread.sleep(30000); + // Wait for Haskell relay to sync past Yano's shifted bootstrap region (~3 epochs). + // Poll the relay's tip every ~1s and exit early once epoch >= BOOTSTRAP_EPOCH_SHIFT. + // Soft 30s deadline — getTip itself can block up to ~10s on a stuck socket, so + // wall-clock worst case is ~40s; on healthy runs this exits in ~10-20s. + long epochLength = clusterInfo.getEpochLength(); + writer.accept(info("Waiting up to 30s for relay to sync past epoch %d...", BOOTSTRAP_EPOCH_SHIFT)); + + final long deadlineMs = System.currentTimeMillis() + 30_000L; + final ClusterUtilService clusterUtilService = clusterUtilServiceProvider.getObject(); + boolean synced = false; + + while (true) { + long remaining = deadlineMs - System.currentTimeMillis(); + if (remaining <= 0) break; + + // ClusterUtilService.getTip prints "Find tip error ..." directly via static + // ConsoleWriter on exception (bypassing the consumer); transient noise during + // the first one or two polls after the socket appears is expected. + Tuple tip = clusterUtilService.getTip(msg -> {}); + if (tip != null && tip._2 != null && epochLength > 0) { + long slot = tip._2.getSlot(); + long epoch = slot / epochLength; + if (epoch >= BOOTSTRAP_EPOCH_SHIFT) { + writer.accept(success("Relay synced to epoch " + epoch + + " (slot " + slot + ", height " + tip._1 + ")")); + synced = true; + break; + } + } + + remaining = deadlineMs - System.currentTimeMillis(); + if (remaining <= 0) break; + Thread.sleep(Math.min(1000L, remaining)); + } + if (!synced) { + writer.accept(warn("Relay sync did not reach epoch " + + BOOTSTRAP_EPOCH_SHIFT + " within 30s. Proceeding anyway.")); + } // Stop Yano — relay has synced the chain to its db writer.accept(info("Stopping Yano...")); From a5f4d754f50b5e9535e499d00f1666ba178c7d26 Mon Sep 17 00:00:00 2001 From: Satya Date: Mon, 11 May 2026 19:45:00 +0800 Subject: [PATCH 08/17] fix: Delete yano db and config folder on reset command --- .../localcluster/yano/YanoService.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoService.java index 113311a3..f7bdae90 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoService.java @@ -2,6 +2,7 @@ import com.bloxbean.cardano.yacicli.localcluster.ClusterConfig; import com.bloxbean.cardano.yacicli.localcluster.ClusterInfo; +import com.bloxbean.cardano.yacicli.localcluster.events.ClusterDeleted; import com.bloxbean.cardano.yacicli.localcluster.events.ClusterStopped; import com.bloxbean.cardano.yacicli.util.PortUtil; import com.bloxbean.cardano.yacicli.util.ProcessStream; @@ -320,6 +321,25 @@ public void handleClusterStopped(ClusterStopped clusterStopped) { stop(); } + @EventListener + public void handleClusterDeleted(ClusterDeleted clusterDeleted) { + Path clusterFolder = Path.of(clusterConfig.getClusterHome(), clusterDeleted.getClusterName()); + deleteIfPresent(clusterFolder.resolve("node").resolve("yano"), "Yano data"); + deleteIfPresent(clusterFolder.resolve("yano-config"), "Yano config"); + } + + private void deleteIfPresent(Path path, String label) { + if (path.toFile().exists()) { + try { + FileUtils.deleteDirectory(path.toFile()); + writeLn(success(label + " folder deleted: " + path.toAbsolutePath())); + } catch (IOException e) { + writeLn(error(label + " folder could not be deleted: " + path.toAbsolutePath() + + " — " + e.getMessage())); + } + } + } + public boolean stop() { try { if (processes != null && !processes.isEmpty()) From 734b123f60033273490c8ba1b576306ce6f8b9e5 Mon Sep 17 00:00:00 2001 From: Satya Date: Mon, 11 May 2026 22:20:06 +0800 Subject: [PATCH 09/17] fix(yaci-store): wait for indexer sync before YaciStoreService.start returns --- .../yacicli/localcluster/ClusterCommands.java | 12 ++ .../yacistore/YaciStoreService.java | 120 ++++++++++++++++-- 2 files changed, 124 insertions(+), 8 deletions(-) diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterCommands.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterCommands.java index fe03beac..9bc0bbee 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterCommands.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterCommands.java @@ -235,6 +235,18 @@ public void startLocalCluster() { if (!runStatus.stared()) return; + // Ensure CommandContext.era is populated for downstream listeners and the + // waitForNextBlocks call below (both read CommandContext.INSTANCE.getEra()). + // The HTTP /devnet/reset path does not set era anywhere otherwise. + try { + ClusterInfo clusterInfo = localClusterService.getClusterInfo(clusterName); + if (clusterInfo != null && clusterInfo.getEra() != null) { + CommandContext.INSTANCE.setEra(clusterInfo.getEra()); + } + } catch (IOException e) { + log.warn("Could not load cluster info to set era for {}", clusterName, e); + } + if (runStatus.isFirstRun()) { publisher.publishEvent(new FirstRunDone(clusterName)); publisher.publishEvent(new ClusterStarted(clusterName)); diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yacistore/YaciStoreService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yacistore/YaciStoreService.java index 73a90c4f..3f110f0a 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yacistore/YaciStoreService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yacistore/YaciStoreService.java @@ -1,8 +1,11 @@ package com.bloxbean.cardano.yacicli.localcluster.yacistore; +import com.bloxbean.cardano.yaci.core.protocol.chainsync.messages.Point; import com.bloxbean.cardano.yaci.core.protocol.localstate.api.Era; import com.bloxbean.cardano.yaci.core.util.OSUtil; import com.bloxbean.cardano.yacicli.commands.common.JreResolver; +import com.bloxbean.cardano.yacicli.common.CommandContext; +import com.bloxbean.cardano.yacicli.common.Tuple; import com.bloxbean.cardano.yacicli.localcluster.ClusterConfig; import com.bloxbean.cardano.yacicli.localcluster.ClusterInfo; import com.bloxbean.cardano.yacicli.localcluster.ClusterService; @@ -13,15 +16,21 @@ import com.bloxbean.cardano.yacicli.localcluster.events.ClusterStarted; import com.bloxbean.cardano.yacicli.localcluster.events.ClusterStopped; import com.bloxbean.cardano.yacicli.localcluster.events.RollbackDone; +import com.bloxbean.cardano.yacicli.localcluster.service.ClusterUtilService; import com.bloxbean.cardano.yacicli.util.PortUtil; import com.bloxbean.cardano.yacicli.util.ProcessStream; import com.bloxbean.cardano.yacicli.util.ProcessUtil; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.EvictingQueue; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.event.EventListener; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; import java.io.File; import java.io.IOException; @@ -44,6 +53,7 @@ public class YaciStoreService { private final ClusterService clusterService; private final ClusterStartService clusterStartService; private final ClusterConfig clusterConfig; + private final ClusterUtilService clusterUtilService; private final JreResolver jreResolver; private final YaciStoreConfigBuilder yaciStoreConfigBuilder; private final YaciStoreCustomDbHelper customDBHelper; @@ -88,9 +98,17 @@ public boolean start(String clusterName, Consumer writer) { return false; Era era = clusterInfo.getEra(); - Process process = startStoreApp(clusterInfo, era); - if (process != null) - processes.add(process); + StoreStartResult result = startStoreApp(clusterInfo, era); + // Always track a live process so it can be cleaned up by stop(), even when + // the boot log wasn't observed (the process may still recover). + if (result.process() != null) + processes.add(result.process()); + + // Wait for indexer to catch up to chain tip — only if boot was actually + // observed, otherwise we'd burn the 60s deadline on a stuck process. + if (result.bootObserved() && result.process() != null && result.process().isAlive()) { + waitForSyncToTip(clusterInfo, writer); + } // Process viewerProcess = startViewerApp(clusterStarted.getClusterName()); // processes.add(viewerProcess); @@ -101,6 +119,10 @@ public boolean start(String clusterName, Consumer writer) { return true; } + /** Result of an attempt to spawn Yaci Store: the process (may be null) and + * whether the "Started YaciStoreApplication" boot log line was observed. */ + private record StoreStartResult(Process process, boolean bootObserved) {} + private static boolean portAvailabilityCheck(ClusterInfo clusterInfo, Consumer writer) { boolean yaciPortAvailable = PortUtil.isPortAvailable(clusterInfo.getYaciStorePort()); if (!yaciPortAvailable) { @@ -113,7 +135,89 @@ private static boolean portAvailabilityCheck(ClusterInfo clusterInfo, Consumer 1 block + * 0.3s -> 4 blocks + * 0.2s -> 5 blocks (cap) + * + * Returns silently after a soft 60s deadline; matches existing "warn + continue" pattern. + */ + private void waitForSyncToTip(ClusterInfo clusterInfo, Consumer writer) { + // ClusterUtilService.getTip reads CommandContext.INSTANCE.getEra(); the primary + // fix lives in ClusterCommands.startLocalCluster() but set here too as defense + // against alternate call paths. + if (clusterInfo.getEra() != null) { + CommandContext.INSTANCE.setEra(clusterInfo.getEra()); + } + + double blockTime = Math.max(clusterInfo.getBlockTime(), 0.1); + final long maxLagBlocks = Math.min(5L, Math.max(1L, (long) Math.ceil(1.0 / blockTime))); + + String url = "http://localhost:" + clusterInfo.getYaciStorePort() + "/api/v1/blocks/latest"; + RestTemplate restTemplate = new RestTemplate(); + ObjectMapper mapper = new ObjectMapper(); + + long deadlineMs = System.currentTimeMillis() + SYNC_WAIT_DEADLINE_MS; + writer.accept(info("Waiting for Yaci Store to sync to chain tip (lag tolerance " + maxLagBlocks + " blocks)...")); + + while (true) { + long remaining = deadlineMs - System.currentTimeMillis(); + if (remaining <= 0) { + writer.accept(warn("Yaci Store did not reach chain tip within 60s. Proceeding anyway.")); + return; + } + + Tuple nodeTip = clusterUtilService.getTip(msg -> {}); // re-queried each iteration + Long indexerHeight = fetchIndexerHeight(restTemplate, mapper, url); + + if (nodeTip != null && nodeTip._1 != null && indexerHeight != null) { + long nodeHeight = nodeTip._1; + long lag = nodeHeight - indexerHeight; + // Non-negative guard: if indexer is ahead of node (stale DB / aborted reset), + // don't false-pass — keep waiting until the indexer's height makes sense. + if (lag >= 0 && lag <= maxLagBlocks) { + writer.accept(success("Yaci Store synced to chain tip (indexer height " + + indexerHeight + ", node height " + nodeHeight + ", lag " + lag + ")")); + return; + } + } + // null/negative-lag cases fall through to the sleep + retry within the deadline. + + remaining = deadlineMs - System.currentTimeMillis(); + if (remaining <= 0) continue; // loop top will emit the timeout warn + try { + Thread.sleep(Math.min(1000L, remaining)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + } + + private Long fetchIndexerHeight(RestTemplate restTemplate, ObjectMapper mapper, String url) { + try { + ResponseEntity resp = restTemplate.getForEntity(url, String.class); + if (!resp.getStatusCode().is2xxSuccessful() || resp.getBody() == null) return null; + JsonNode body = mapper.readTree(resp.getBody()); + JsonNode height = body.get("height"); + return (height == null || height.isNull()) ? null : height.asLong(); + } catch (RestClientException e) { + // indexer HTTP not ready, or 404 because no blocks indexed yet — retry + return null; + } catch (Exception e) { + return null; + } + } + + private StoreStartResult startStoreApp(ClusterInfo clusterInfo, Era era) throws IOException, InterruptedException, ExecutionException, TimeoutException { ProcessBuilder builder = new ProcessBuilder(); builder.directory(new File(clusterConfig.getYaciStoreBinPath())); @@ -135,13 +239,13 @@ private Process startStoreApp(ClusterInfo clusterInfo, Era era) throws IOExcepti Path yaciStoreJar = Path.of(clusterConfig.getYaciStoreBinPath(), "yaci-store.jar"); if (!yaciStoreJar.toFile().exists()) { writeLn(error("yaci-store.jar is not found at " + clusterConfig.getYaciStoreBinPath())); - return null; + return new StoreStartResult(null, false); } } else if (effectiveMode.equals("native")) { Path yaciStoreBin = Path.of(clusterConfig.getYaciStoreBinPath(), "yaci-store"); if (!yaciStoreBin.toFile().exists()) { writeLn(error("yaci-store binary is not found at " + clusterConfig.getYaciStoreBinPath())); - return null; + return new StoreStartResult(null, false); } } @@ -221,7 +325,7 @@ private Process startStoreApp(ClusterInfo clusterInfo, Era era) throws IOExcepti writeLn("Waiting for Yaci Store to start ..."); } - if (counter == 40) { + if (!started.get()) { writeLn(error("Waited too long. Could not start Yaci Store. Something is wrong..")); writeLn(error("Use \"yaci-store-logs\" to see the logs")); writeLn(error("Please verify if another yaci-store in running in the same port. " + @@ -241,7 +345,7 @@ private Process startStoreApp(ClusterInfo clusterInfo, Era era) throws IOExcepti processUtil.createProcessId(STORE_PROCESS_NAME, process); - return process; + return new StoreStartResult(process, started.get()); } private Process startViewerApp(String cluster) throws IOException, InterruptedException, ExecutionException, TimeoutException { From 0ece3a2eafd87b1469be9665c800356599901885 Mon Sep 17 00:00:00 2001 From: Satya Date: Mon, 11 May 2026 22:20:41 +0800 Subject: [PATCH 10/17] Update default genesis value for latest protocol version and maxTxExUnitsMem --- .../cardano/yacicli/localcluster/config/GenesisConfig.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 3a593736..2d18f962 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 @@ -43,7 +43,7 @@ public class GenesisConfig { private BigInteger minUTxOValue = BigInteger.valueOf(1000000); private int nOpt = 100; private BigInteger poolDeposit = BigInteger.valueOf(500000000); - private int protocolMajorVer = 8; + private int protocolMajorVer = 11; private int protocolMinorVer = 0; private float monetaryExpansionRate = 0.003f; private float treasuryGrowthRate = 0.20f; @@ -109,7 +109,7 @@ public class GenesisConfig { private long maxBlockExUnitsMem = 62000000; private long maxBlockExUnitsSteps = 20000000000L; private int maxCollateralInputs = 3; - private long maxTxExUnitsMem = 14000000; + private long maxTxExUnitsMem = 16500000; private long maxTxExUnitsSteps = 10000000000L; private int maxValueSize = 5000; From de99919db69333431f15a026e78a77c6449f16ee Mon Sep 17 00:00:00 2001 From: Satya Date: Fri, 15 May 2026 19:49:18 +0800 Subject: [PATCH 11/17] Auto adjust slot length if blocktime < 1 and slotLength > blocktime --- .../cardano/yacicli/localcluster/ClusterCommands.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterCommands.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterCommands.java index 9bc0bbee..e45fb181 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterCommands.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterCommands.java @@ -97,7 +97,7 @@ public void createCluster(@ShellOption(value = {"-n", "--name"}, defaultValue = @ShellOption(value = {"--port"}, help = "Node port (Used with --create option only)", defaultValue = "3001") int port, @ShellOption(value = {"--submit-api-port"}, help = "Submit Api Port", defaultValue = "8090") int submitApiPort, @ShellOption(value = {"-s", "--slot-length"}, help = "Slot Length in sec. (0.1 to ..)", defaultValue = "1") double slotLength, - @ShellOption(value = {"-b", "--block-time"}, help = "Block time in sec. (1 - 20)", defaultValue = "1") double blockTime, + @ShellOption(value = {"-b", "--block-time"}, help = "Block time in sec. (0.1 - 20)", defaultValue = "1") double blockTime, @ShellOption(value = {"-e", "--epoch-length"}, help = "No of slots in an epoch", defaultValue = "600") int epochLength, @ShellOption(value = {"-o", "--overwrite"}, defaultValue = "false", help = "Overwrite existing node directory. default: false") boolean overwrite, @ShellOption(value = {"--start"}, defaultValue = "false", help = "Automatically start the node after create. default: false") boolean start, @@ -116,6 +116,11 @@ public void createCluster(@ShellOption(value = {"-n", "--name"}, defaultValue = return; } + if (blockTime < 1 && slotLength > blockTime) { + slotLength = blockTime; + writeLn(info("Slot length adjusted to " + slotLength + " sec to match block time")); + } + if (slotLength > blockTime) { writeLn(error("Slot length should be less than block time")); return; From e38051ced2d7f52691a3ce8b1af52b43a647e66b Mon Sep 17 00:00:00 2001 From: Satya Date: Fri, 15 May 2026 21:14:09 +0800 Subject: [PATCH 12/17] Initial implementation of Yano companion mode and yano only mode --- .gitignore | 1 + .../cli/config/plutus-costmodels-v11.json | 1079 ++++++++++++++++- .../localcluster/ClusterStartService.java | 12 +- .../FirstRunPlutusV3CostModelUpdate.java | 13 +- .../localcluster/peer/LocalPeerService.java | 180 ++- .../yano/YanoBootstrapService.java | 73 ++ .../yano/YanoCompanionService.java | 42 +- .../localcluster/yano/YanoConfigBuilder.java | 3 + .../yano/YanoGovernanceService.java | 16 + .../localcluster/yano/YanoService.java | 3 + config/plutus-costmodels-v11.json | 1022 ++++++++++++++++ 11 files changed, 2355 insertions(+), 89 deletions(-) create mode 100644 config/plutus-costmodels-v11.json diff --git a/.gitignore b/.gitignore index 3694443b..e6d37f7a 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ applications/store-build/tmp applications/cli/wallet-ui/node_modules/ applications/cli/src/main/resources/static/wallet/ /applications/cli/chainstate/ +/e2e-tests/evolution-sdk/node_modules/ diff --git a/applications/cli/config/plutus-costmodels-v11.json b/applications/cli/config/plutus-costmodels-v11.json index cc725ecd..a3d556d1 100644 --- a/applications/cli/config/plutus-costmodels-v11.json +++ b/applications/cli/config/plutus-costmodels-v11.json @@ -1,73 +1,1022 @@ { "PlutusV1": [ - 100788, 420, 1, 1, 1000, 173, 0, 1, 1000, 59957, - 4, 1, 11183, 32, 201305, 8356, 4, 16000, 100, 16000, - 100, 16000, 100, 16000, 100, 16000, 100, 16000, 100, 100, - 100, 16000, 100, 94375, 32, 132994, 32, 61462, 4, 72010, - 178, 0, 1, 22151, 32, 91189, 769, 4, 2, 85848, - 228465, 122, 0, 1, 1, 1000, 42921, 4, 2, 24548, - 29498, 38, 1, 898148, 27279, 1, 51775, 558, 1, 39184, - 1000, 60594, 1, 141895, 32, 83150, 32, 15299, 32, 76049, - 1, 13169, 4, 22100, 10, 28999, 74, 1, 28999, 74, - 1, 43285, 552, 1, 44749, 541, 1, 33852, 32, 68246, - 32, 72362, 32, 7243, 32, 7391, 32, 11546, 32, 85848, - 228465, 122, 0, 1, 1, 90434, 519, 0, 1, 74433, - 32, 85848, 228465, 122, 0, 1, 1, 85848, 228465, 122, - 0, 1, 1, 270652, 22588, 4, 1457325, 64566, 4, 20467, - 1, 4, 0, 141992, 32, 100788, 420, 1, 1, 81663, - 32, 59498, 32, 20142, 32, 24588, 32, 20744, 32, 25933, - 32, 24623, 32, 53384111, 14333, 10 + 100788, + 420, + 1, + 1, + 1000, + 173, + 0, + 1, + 1000, + 59957, + 4, + 1, + 11183, + 32, + 201305, + 8356, + 4, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 100, + 100, + 16000, + 100, + 94375, + 32, + 132994, + 32, + 61462, + 4, + 72010, + 178, + 0, + 1, + 22151, + 32, + 91189, + 769, + 4, + 2, + 85848, + 228465, + 122, + 0, + 1, + 1, + 1000, + 42921, + 4, + 2, + 30623, + 28755, + 75, + 1, + 898148, + 27279, + 1, + 51775, + 558, + 1, + 39184, + 1000, + 60594, + 1, + 141895, + 32, + 83150, + 32, + 15299, + 32, + 76049, + 1, + 13169, + 4, + 22100, + 10, + 28999, + 74, + 1, + 28999, + 74, + 1, + 43285, + 552, + 1, + 44749, + 541, + 1, + 33852, + 32, + 68246, + 32, + 72362, + 32, + 7243, + 32, + 7391, + 32, + 11546, + 32, + 85848, + 228465, + 122, + 0, + 1, + 1, + 90434, + 519, + 0, + 1, + 74433, + 32, + 85848, + 228465, + 122, + 0, + 1, + 1, + 85848, + 228465, + 122, + 0, + 1, + 1, + 270652, + 22588, + 4, + 1457325, + 64566, + 4, + 20467, + 1, + 4, + 0, + 141992, + 32, + 100788, + 420, + 1, + 1, + 81663, + 32, + 59498, + 32, + 20142, + 32, + 24588, + 32, + 20744, + 32, + 25933, + 32, + 24623, + 32, + 53384111, + 14333, + 10, + 955506, + 213312, + 0, + 2, + 43053543, + 10, + 43574283, + 26308, + 10, + 16000, + 100, + 16000, + 100, + 962335, + 18, + 2780678, + 6, + 442008, + 1, + 52538055, + 3756, + 18, + 267929, + 18, + 76433006, + 8868, + 18, + 52948122, + 18, + 1995836, + 36, + 3227919, + 12, + 901022, + 1, + 166917843, + 4307, + 36, + 284546, + 36, + 158221314, + 26549, + 36, + 74698472, + 36, + 333849714, + 1, + 254006273, + 72, + 2174038, + 72, + 2261318, + 64571, + 4, + 207616, + 8310, + 4, + 1293828, + 28716, + 63, + 0, + 1, + 1006041, + 43623, + 251, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 107878, + 680, + 0, + 1, + 95336, + 1, + 281145, + 18848, + 0, + 1, + 180194, + 159, + 1, + 1, + 158519, + 8942, + 0, + 1, + 159378, + 8813, + 0, + 1, + 107490, + 3298, + 1, + 106057, + 655, + 1, + 1964219, + 24520, + 3, + 607153, + 231697, + 53144, + 0, + 1, + 116711, + 1957, + 4, + 231883, + 10, + 1000, + 24838, + 7, + 1, + 232010, + 32, + 321837444, + 25087669, + 18, + 617887431, + 67302824, + 36, + 356924, + 18413, + 45, + 21, + 219951, + 9444, + 1, + 1000, + 172116, + 183150, + 6, + 24, + 21, + 213283, + 618401, + 1998, + 28258, + 1, + 1000, + 38159, + 2, + 22, + 1000, + 95933, + 1, + 1, + 11, + 1000, + 277577, + 12, + 21 ], "PlutusV2": [ - 100788, 420, 1, 1, 1000, 173, 0, 1, 1000, 59957, - 4, 1, 11183, 32, 201305, 8356, 4, 16000, 100, 16000, - 100, 16000, 100, 16000, 100, 16000, 100, 16000, 100, 100, - 100, 16000, 100, 94375, 32, 132994, 32, 61462, 4, 72010, - 178, 0, 1, 22151, 32, 91189, 769, 4, 2, 85848, - 228465, 122, 0, 1, 1, 1000, 42921, 4, 2, 24548, - 29498, 38, 1, 898148, 27279, 1, 51775, 558, 1, 39184, - 1000, 60594, 1, 141895, 32, 83150, 32, 15299, 32, 76049, - 1, 13169, 4, 22100, 10, 28999, 74, 1, 28999, 74, - 1, 43285, 552, 1, 44749, 541, 1, 33852, 32, 68246, - 32, 72362, 32, 7243, 32, 7391, 32, 11546, 32, 85848, - 228465, 122, 0, 1, 1, 90434, 519, 0, 1, 74433, - 32, 85848, 228465, 122, 0, 1, 1, 85848, 228465, 122, - 0, 1, 1, 955506, 213312, 0, 2, 270652, 22588, 4, - 1457325, 64566, 4, 20467, 1, 4, 0, 141992, 32, 100788, - 420, 1, 1, 81663, 32, 59498, 32, 20142, 32, 24588, - 32, 20744, 32, 25933, 32, 24623, 32, 43053543, 10, 53384111, - 14333, 10, 43574283, 26308, 10 + 100788, + 420, + 1, + 1, + 1000, + 173, + 0, + 1, + 1000, + 59957, + 4, + 1, + 11183, + 32, + 201305, + 8356, + 4, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 100, + 100, + 16000, + 100, + 94375, + 32, + 132994, + 32, + 61462, + 4, + 72010, + 178, + 0, + 1, + 22151, + 32, + 91189, + 769, + 4, + 2, + 85848, + 228465, + 122, + 0, + 1, + 1, + 1000, + 42921, + 4, + 2, + 30623, + 28755, + 75, + 1, + 898148, + 27279, + 1, + 51775, + 558, + 1, + 39184, + 1000, + 60594, + 1, + 141895, + 32, + 83150, + 32, + 15299, + 32, + 76049, + 1, + 13169, + 4, + 22100, + 10, + 28999, + 74, + 1, + 28999, + 74, + 1, + 43285, + 552, + 1, + 44749, + 541, + 1, + 33852, + 32, + 68246, + 32, + 72362, + 32, + 7243, + 32, + 7391, + 32, + 11546, + 32, + 85848, + 228465, + 122, + 0, + 1, + 1, + 90434, + 519, + 0, + 1, + 74433, + 32, + 85848, + 228465, + 122, + 0, + 1, + 1, + 85848, + 228465, + 122, + 0, + 1, + 1, + 955506, + 213312, + 0, + 2, + 270652, + 22588, + 4, + 1457325, + 64566, + 4, + 20467, + 1, + 4, + 0, + 141992, + 32, + 100788, + 420, + 1, + 1, + 81663, + 32, + 59498, + 32, + 20142, + 32, + 24588, + 32, + 20744, + 32, + 25933, + 32, + 24623, + 32, + 43053543, + 10, + 53384111, + 14333, + 10, + 43574283, + 26308, + 10, + 1293828, + 28716, + 63, + 0, + 1, + 1006041, + 43623, + 251, + 0, + 1, + 16000, + 100, + 16000, + 100, + 962335, + 18, + 2780678, + 6, + 442008, + 1, + 52538055, + 3756, + 18, + 267929, + 18, + 76433006, + 8868, + 18, + 52948122, + 18, + 1995836, + 36, + 3227919, + 12, + 901022, + 1, + 166917843, + 4307, + 36, + 284546, + 36, + 158221314, + 26549, + 36, + 74698472, + 36, + 333849714, + 1, + 254006273, + 72, + 2174038, + 72, + 2261318, + 64571, + 4, + 207616, + 8310, + 4, + 100181, + 726, + 719, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 107878, + 680, + 0, + 1, + 95336, + 1, + 281145, + 18848, + 0, + 1, + 180194, + 159, + 1, + 1, + 158519, + 8942, + 0, + 1, + 159378, + 8813, + 0, + 1, + 107490, + 3298, + 1, + 106057, + 655, + 1, + 1964219, + 24520, + 3, + 607153, + 231697, + 53144, + 0, + 1, + 116711, + 1957, + 4, + 231883, + 10, + 1000, + 24838, + 7, + 1, + 232010, + 32, + 321837444, + 25087669, + 18, + 617887431, + 67302824, + 36, + 356924, + 18413, + 45, + 21, + 219951, + 9444, + 1, + 1000, + 172116, + 183150, + 6, + 24, + 21, + 213283, + 618401, + 1998, + 28258, + 1, + 1000, + 38159, + 2, + 22, + 1000, + 95933, + 1, + 1, + 11, + 1000, + 277577, + 12, + 21 ], "PlutusV3": [ - 100788, 420, 1, 1, 1000, 173, 0, 1, 1000, 59957, - 4, 1, 11183, 32, 201305, 8356, 4, 16000, 100, 16000, - 100, 16000, 100, 16000, 100, 16000, 100, 16000, 100, 100, - 100, 16000, 100, 94375, 32, 132994, 32, 61462, 4, 72010, - 178, 0, 1, 22151, 32, 91189, 769, 4, 2, 85848, - 123203, 7305, -900, 1716, 549, 57, 85848, 0, 1, 1, - 1000, 42921, 4, 2, 24548, 29498, 38, 1, 898148, 27279, - 1, 51775, 558, 1, 39184, 1000, 60594, 1, 141895, 32, - 83150, 32, 15299, 32, 76049, 1, 13169, 4, 22100, 10, - 28999, 74, 1, 28999, 74, 1, 43285, 552, 1, 44749, - 541, 1, 33852, 32, 68246, 32, 72362, 32, 7243, 32, - 7391, 32, 11546, 32, 85848, 123203, 7305, -900, 1716, 549, - 57, 85848, 0, 1, 90434, 519, 0, 1, 74433, 32, - 85848, 123203, 7305, -900, 1716, 549, 57, 85848, 0, 1, - 1, 85848, 123203, 7305, -900, 1716, 549, 57, 85848, 0, - 1, 955506, 213312, 0, 2, 270652, 22588, 4, 1457325, 64566, - 4, 20467, 1, 4, 0, 141992, 32, 100788, 420, 1, - 1, 81663, 32, 59498, 32, 20142, 32, 24588, 32, 20744, - 32, 25933, 32, 24623, 32, 43053543, 10, 53384111, 14333, 10, - 43574283, 26308, 10, 16000, 100, 16000, 100, 962335, 18, 2780678, - 6, 442008, 1, 52538055, 3756, 18, 267929, 18, 76433006, 8868, - 18, 52948122, 18, 1995836, 36, 3227919, 12, 901022, 1, 166917843, - 4307, 36, 284546, 36, 158221314, 26549, 36, 74698472, 36, 333849714, - 1, 254006273, 72, 2174038, 72, 2261318, 64571, 4, 207616, 8310, - 4, 1293828, 28716, 63, 0, 1, 1006041, 43623, 251, 0, - 1, 100181, 726, 719, 0, 1, 100181, 726, 719, 0, - 1, 100181, 726, 719, 0, 1, 107878, 680, 0, 1, - 95336, 1, 281145, 18848, 0, 1, 180194, 159, 1, 1, - 158519, 8942, 0, 1, 159378, 8813, 0, 1, 107490, 3298, - 1, 106057, 655, 1, 1964219, 24520, 3 + 100788, + 420, + 1, + 1, + 1000, + 173, + 0, + 1, + 1000, + 59957, + 4, + 1, + 11183, + 32, + 201305, + 8356, + 4, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 100, + 100, + 16000, + 100, + 94375, + 32, + 132994, + 32, + 61462, + 4, + 72010, + 178, + 0, + 1, + 22151, + 32, + 91189, + 769, + 4, + 2, + 85848, + 123203, + 7305, + -900, + 1716, + 960, + 57, + 85848, + 0, + 1, + 1, + 1000, + 42921, + 4, + 2, + 30623, + 28755, + 75, + 1, + 898148, + 27279, + 1, + 51775, + 558, + 1, + 39184, + 1000, + 60594, + 1, + 141895, + 32, + 83150, + 32, + 15299, + 32, + 76049, + 1, + 13169, + 4, + 22100, + 10, + 28999, + 74, + 1, + 28999, + 74, + 1, + 43285, + 552, + 1, + 44749, + 541, + 1, + 33852, + 32, + 68246, + 32, + 72362, + 32, + 7243, + 32, + 7391, + 32, + 11546, + 32, + 85848, + 123203, + 7305, + -900, + 1716, + 960, + 57, + 85848, + 0, + 1, + 90434, + 519, + 0, + 1, + 74433, + 32, + 85848, + 123203, + 7305, + -900, + 1716, + 960, + 57, + 85848, + 0, + 1, + 1, + 85848, + 123203, + 7305, + -900, + 1716, + 960, + 57, + 85848, + 0, + 1, + 955506, + 213312, + 0, + 2, + 270652, + 22588, + 4, + 1457325, + 64566, + 4, + 20467, + 1, + 4, + 0, + 141992, + 32, + 100788, + 420, + 1, + 1, + 81663, + 32, + 59498, + 32, + 20142, + 32, + 24588, + 32, + 20744, + 32, + 25933, + 32, + 24623, + 32, + 43053543, + 10, + 53384111, + 14333, + 10, + 43574283, + 26308, + 10, + 16000, + 100, + 16000, + 100, + 962335, + 18, + 2780678, + 6, + 442008, + 1, + 52538055, + 3756, + 18, + 267929, + 18, + 76433006, + 8868, + 18, + 52948122, + 18, + 1995836, + 36, + 3227919, + 12, + 901022, + 1, + 166917843, + 4307, + 36, + 284546, + 36, + 158221314, + 26549, + 36, + 74698472, + 36, + 333849714, + 1, + 254006273, + 72, + 2174038, + 72, + 2261318, + 64571, + 4, + 207616, + 8310, + 4, + 1293828, + 28716, + 63, + 0, + 1, + 1006041, + 43623, + 251, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 107878, + 680, + 0, + 1, + 95336, + 1, + 281145, + 18848, + 0, + 1, + 180194, + 159, + 1, + 1, + 158519, + 8942, + 0, + 1, + 159378, + 8813, + 0, + 1, + 107490, + 3298, + 1, + 106057, + 655, + 1, + 1964219, + 24520, + 3, + 607153, + 231697, + 53144, + 0, + 1, + 116711, + 1957, + 4, + 231883, + 10, + 1000, + 24838, + 7, + 1, + 232010, + 32, + 321837444, + 25087669, + 18, + 617887431, + 67302824, + 36, + 356924, + 18413, + 45, + 21, + 219951, + 9444, + 1, + 1000, + 172116, + 183150, + 6, + 24, + 21, + 213283, + 618401, + 1998, + 28258, + 1, + 1000, + 38159, + 2, + 22, + 1000, + 95933, + 1, + 1, + 11, + 1000, + 277577, + 12, + 21 ] } 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 d81d5525..3e923498 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 @@ -82,6 +82,7 @@ public RunStatus startCluster(ClusterInfo clusterInfo, Path clusterFolder, Consu boolean yanoOnlyMode = NodeMode.YANO_ONLY == clusterInfo.getNodeMode(); boolean yanoPrimaryMode = NodeMode.YANO_PRIMARY == clusterInfo.getNodeMode(); boolean companionBootstrapDone = false; + String clusterName = CommandContext.INSTANCE.getProperty(ClusterConfig.CLUSTER_NAME); if (clusterInfo.isMasterNode() && firstRun) setupFirstRun(clusterInfo, clusterFolder, writer); @@ -158,8 +159,7 @@ public RunStatus startCluster(ClusterInfo clusterInfo, Path clusterFolder, Consu return new RunStatus(true, firstRun); } - if (clusterInfo.isLocalMultiNodeEnabled()) { - String clusterName = CommandContext.INSTANCE.getProperty(ClusterConfig.CLUSTER_NAME); + if (clusterInfo.isLocalMultiNodeEnabled() && !companionBootstrapDone) { if (firstRun) { localPeerService.handleFirstRun(new FirstRunDone(clusterName)); } @@ -203,10 +203,18 @@ public RunStatus startCluster(ClusterInfo clusterInfo, Path clusterFolder, Consu Path socketPath = clusterFolder.resolve(ClusterConfig.NODE_FOLDER_PREFIX).resolve("node.sock"); Files.deleteIfExists(socketPath); + if (clusterInfo.isLocalMultiNodeEnabled()) { + localPeerService.preparePeersAfterCompanionHandover(clusterName, writer); + } + nodeProcess = startNode(clusterFolder, clusterInfo, false, writer); if (nodeProcess != null) { processes.add(nodeProcess); writer.accept(success("Haskell node restarted as block producer.")); + if (clusterInfo.isLocalMultiNodeEnabled()) { + localPeerService.startLocalPeersAfterCompanionHandover(clusterName, writer); + yanoCompanionService.waitForPostHandoverBlock(writer); + } } else { writer.accept(error("Failed to restart node as block producer.")); } diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/events/listeners/FirstRunPlutusV3CostModelUpdate.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/events/listeners/FirstRunPlutusV3CostModelUpdate.java index a684d77e..25d55217 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/events/listeners/FirstRunPlutusV3CostModelUpdate.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/events/listeners/FirstRunPlutusV3CostModelUpdate.java @@ -6,6 +6,7 @@ import com.bloxbean.cardano.yacicli.localcluster.events.FirstRunDone; import com.bloxbean.cardano.yacicli.localcluster.service.AccountService; import com.bloxbean.cardano.yacicli.localcluster.service.ClusterUtilService; +import com.bloxbean.cardano.yacicli.localcluster.yano.YanoGovernanceService; import com.bloxbean.cardano.yacicli.common.AnsiColors; import com.bloxbean.cardano.yacicli.common.CommandContext; import lombok.RequiredArgsConstructor; @@ -25,6 +26,7 @@ public class FirstRunPlutusV3CostModelUpdate { private final ClusterService localClusterService; private final ClusterUtilService clusterUtilService; private final AccountService accountService; + private final YanoGovernanceService yanoGovernanceService; @EventListener @Order(100) // Run after FirstRunTopupAccounts @@ -38,10 +40,17 @@ public void updatePlutusV3CostModel(FirstRunDone firstRunDone) { } NodeMode nodeMode = clusterInfo != null ? clusterInfo.getNodeMode() : null; - if (NodeMode.COMPANION == nodeMode || NodeMode.YANO_ONLY == nodeMode || NodeMode.YANO_PRIMARY == nodeMode) { - writeLn(info("Skipping Plutus cost models update - already submitted via Yano bootstrap")); + if (NodeMode.YANO_ONLY == nodeMode || NodeMode.YANO_PRIMARY == nodeMode) { + writeLn(info("Skipping Plutus cost models update - handled by Yano mode")); return; } + if (NodeMode.COMPANION == nodeMode) { + if (yanoGovernanceService.wasCostModelGovernanceSubmitted(clusterName)) { + writeLn(info("Skipping Plutus cost models update - already submitted via Yano bootstrap")); + return; + } + writeLn(warn("Yano bootstrap did not submit Plutus cost models. Submitting via Haskell node.")); + } Era era = CommandContext.INSTANCE.getEra(); if (era != Era.Conway) { diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/peer/LocalPeerService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/peer/LocalPeerService.java index 628b4079..4d45d130 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/peer/LocalPeerService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/peer/LocalPeerService.java @@ -118,21 +118,48 @@ public void adjustAndCopyRequiredFilesForMultiNodeSetup(String clusterName, Clus } public void handleFirstRun(FirstRunDone firstRunDone) { - var clusterName = firstRunDone.getCluster(); - Path clusterFolder = clusterConfig.getClusterFolder(clusterName); - Path primaryNodeFolder = clusterFolder.resolve("node"); - Path node2Folder = clusterFolder.resolve("node-2"); - Path node3Folder = clusterFolder.resolve("node-3"); + copyPrimaryGenesisToPeers(firstRunDone.getCluster(), msg -> writeLn(msg)); + } - //Copy the primary node genesis file to node-2 and node-2's genesis folders + /** + * Companion mode shifts genesis while Yano is bootstrapping. In multi-node + * mode, peer nodes must start only after node-1 has synced that shifted + * history and restored its normal multi-node topology. + */ + public void startLocalPeersAfterCompanionHandover(String clusterName, Consumer writer) { try { - //Copy genesis file - Path primaryGenesisFolder = primaryNodeFolder.resolve("genesis"); - FileUtils.copyDirectory(primaryGenesisFolder.toFile(), node2Folder.resolve("genesis").toFile()); - FileUtils.copyDirectory(primaryGenesisFolder.toFile(), node3Folder.resolve("genesis").toFile()); + ClusterInfo clusterInfo = clusterInfoService.getClusterInfo(clusterName); + if (clusterInfo == null || !clusterInfo.isLocalMultiNodeEnabled()) { + return; + } } catch (Exception e) { - writeLn(error("Failed to copy genesis files: " + e.getMessage())); + writer.accept(error("Unable to get cluster info for " + clusterName + ": " + e.getMessage())); + return; } + + startLocalPeersWithProxiesFirst(clusterName, writer); + } + + /** + * Prepare peer nodes for companion handover while node-1 is stopped. + * Copying cardano-node DBs while a node process is live is unsafe, so this + * must be called after the relay process has been stopped and before node-1 + * is restarted as a block producer. + */ + public void preparePeersAfterCompanionHandover(String clusterName, Consumer writer) { + try { + ClusterInfo clusterInfo = clusterInfoService.getClusterInfo(clusterName); + if (clusterInfo == null || !clusterInfo.isLocalMultiNodeEnabled()) { + return; + } + } catch (Exception e) { + writer.accept(error("Unable to get cluster info for " + clusterName + ": " + e.getMessage())); + return; + } + + copyPrimaryGenesisToPeers(clusterName, writer); + restoreMultiNodeTopology(clusterName, writer); + seedPeerDatabasesFromPrimary(clusterName, writer); } public void handleClusterStarted(ClusterStarted clusterStarted) { @@ -143,9 +170,14 @@ public void handleClusterStarted(ClusterStarted clusterStarted) { } catch (IOException e) { writeLn(error("Unable to get cluster info for " + clusterName + ": " + e.getMessage())); } - if (!clusterInfo.isLocalMultiNodeEnabled()) + if (clusterInfo == null || !clusterInfo.isLocalMultiNodeEnabled()) return; + if (processes.stream().anyMatch(process -> process != null && process.isAlive())) { + writeLn(info("Local peer nodes are already running.")); + return; + } + Path clusterFolder = clusterConfig.getClusterFolder(clusterName); Path node2Folder = clusterFolder.resolve("node-2"); Path node3Folder = clusterFolder.resolve("node-3"); @@ -200,6 +232,130 @@ public void handleClusterStarted(ClusterStarted clusterStarted) { } } + private void startLocalPeersWithProxiesFirst(String clusterName, Consumer writer) { + ClusterInfo clusterInfo = null; + try { + clusterInfo = clusterInfoService.getClusterInfo(clusterName); + } catch (IOException e) { + writer.accept(error("Unable to get cluster info for " + clusterName + ": " + e.getMessage())); + } + if (clusterInfo == null || !clusterInfo.isLocalMultiNodeEnabled()) { + return; + } + + if (processes.stream().anyMatch(process -> process != null && process.isAlive())) { + writer.accept(info("Local peer nodes are already running.")); + return; + } + + Path clusterFolder = clusterConfig.getClusterFolder(clusterName); + Path node2Folder = clusterFolder.resolve("node-2"); + Path node3Folder = clusterFolder.resolve("node-3"); + + boolean proxyPort1Available = PortUtil.isPortAvailable(4001); + boolean proxyPort2Available = PortUtil.isPortAvailable(4002); + boolean proxyPort3Available = PortUtil.isPortAvailable(4003); + boolean node2PortAvailable = PortUtil.isPortAvailable(3002); + boolean node3PortAvailable = PortUtil.isPortAvailable(3003); + + if (!proxyPort1Available || !proxyPort2Available || !proxyPort3Available || + !node2PortAvailable || !node3PortAvailable) { + writer.accept(error("Not all required ports are not available to start peer nodes and proxies for rollback." + + " Please check the following ports: " + + "4001, 4002, 4003, 3002, 3003")); + return; + } + + try { + tcpProxyManager.startProxy(4001, "127.0.0.1", 3001); + tcpProxyManager.startProxy(4002, "127.0.0.1", 3002); + tcpProxyManager.startProxy(4003, "127.0.0.1", 3003); + } catch (IOException e) { + writer.accept(error("Failed to start peer proxies: " + e.getMessage())); + return; + } + + try { + var node2Process = startNode("node-2", node2Folder, node2Logs, writer); + if (node2Process == null) { + writer.accept(error("Failed to start node-2. Please check the logs for more details.")); + return; + } + processes.add(node2Process); + } catch (IOException | InterruptedException | ExecutionException | TimeoutException e) { + writer.accept(error("Error starting node-2: " + e.getMessage())); + } + + try { + var node3Process = startNode("node-3", node3Folder, node3Logs, writer); + if (node3Process == null) { + writer.accept(error("Failed to start node-3. Please check the logs for more details.")); + return; + } + processes.add(node3Process); + } catch (IOException | InterruptedException | ExecutionException | TimeoutException e) { + writer.accept(error("Error starting node-3: " + e.getMessage())); + } + } + + private void copyPrimaryGenesisToPeers(String clusterName, Consumer writer) { + Path clusterFolder = clusterConfig.getClusterFolder(clusterName); + Path primaryNodeFolder = clusterFolder.resolve("node"); + Path node2Folder = clusterFolder.resolve("node-2"); + Path node3Folder = clusterFolder.resolve("node-3"); + + try { + Path primaryGenesisFolder = primaryNodeFolder.resolve("genesis"); + FileUtils.copyDirectory(primaryGenesisFolder.toFile(), node2Folder.resolve("genesis").toFile()); + FileUtils.copyDirectory(primaryGenesisFolder.toFile(), node3Folder.resolve("genesis").toFile()); + } catch (Exception e) { + writer.accept(error("Failed to copy genesis files: " + e.getMessage())); + } + } + + private void seedPeerDatabasesFromPrimary(String clusterName, Consumer writer) { + Path clusterFolder = clusterConfig.getClusterFolder(clusterName); + Path primaryDb = clusterFolder.resolve("node").resolve("db"); + + if (!Files.exists(primaryDb)) { + writer.accept(error("Primary node DB is not available for peer seeding: " + primaryDb.toAbsolutePath())); + return; + } + + seedPeerDatabase(primaryDb, clusterFolder.resolve("node-2"), writer); + seedPeerDatabase(primaryDb, clusterFolder.resolve("node-3"), writer); + } + + private void seedPeerDatabase(Path primaryDb, Path peerFolder, Consumer writer) { + Path peerDb = peerFolder.resolve("db"); + try { + FileUtils.deleteDirectory(peerDb.toFile()); + FileUtils.copyDirectory(primaryDb.toFile(), peerDb.toFile()); + Files.deleteIfExists(peerDb.resolve("lock")); + Files.deleteIfExists(peerFolder.resolve("node.sock")); + writer.accept(success("Seeded " + peerFolder.getFileName() + " DB from node-1 handover chain")); + } catch (IOException e) { + writer.accept(error("Failed to seed " + peerFolder.getFileName() + " DB: " + e.getMessage())); + } + } + + private void restoreMultiNodeTopology(String clusterName, Consumer writer) { + Path clusterFolder = clusterConfig.getClusterFolder(clusterName); + Path primaryNodeFolder = clusterFolder.resolve("node"); + Path multiNodeTopology = primaryNodeFolder.resolve("topology-multinode.json"); + Path topology = primaryNodeFolder.resolve("topology.json"); + + if (!Files.exists(multiNodeTopology)) { + return; + } + + try { + FileUtils.copyFile(multiNodeTopology.toFile(), topology.toFile()); + } catch (IOException e) { + writer.accept(error("Failed to restore multi-node topology: " + e.getMessage())); + } + } + private Process startNode(String nodeName, Path nodeFolder, Queue logs, Consumer writer) throws IOException, InterruptedException, ExecutionException, TimeoutException { String startScript = "node"; diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoBootstrapService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoBootstrapService.java index 43111089..d75993ca 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoBootstrapService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoBootstrapService.java @@ -105,6 +105,79 @@ public boolean catchUpToWallClock(int httpPort, Consumer writer) { } } + public boolean waitForProtocolParams(int httpPort, Duration timeout, Consumer writer) { + String url = "http://localhost:" + httpPort + "/api/v1/epochs/latest/parameters"; + long deadlineMs = System.currentTimeMillis() + timeout.toMillis(); + writer.accept(info("Waiting for Yano protocol parameters to become available...")); + + while (System.currentTimeMillis() < deadlineMs) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(Duration.ofSeconds(5)) + .GET() + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 200) { + writer.accept(success("Yano protocol parameters are available")); + return true; + } + log.debug("Protocol parameters not ready: HTTP {} - {}", response.statusCode(), response.body()); + } catch (Exception e) { + log.debug("Error checking Yano protocol parameters: {}", e.getMessage()); + } + + long remaining = deadlineMs - System.currentTimeMillis(); + if (remaining <= 0) break; + try { + Thread.sleep(Math.min(1000L, remaining)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + + writer.accept(warn("Yano protocol parameters did not become available within " + + timeout.toSeconds() + "s.")); + return false; + } + + public boolean waitForTipEpoch(int httpPort, long epochLength, int targetEpoch, + Duration timeout, Consumer writer) { + if (epochLength <= 0) { + writer.accept(warn("Cannot wait for Yano tip epoch because epoch length is not configured.")); + return false; + } + + long deadlineMs = System.currentTimeMillis() + timeout.toMillis(); + writer.accept(info("Waiting for Yano to produce a block at or past epoch %d...", targetEpoch)); + + while (System.currentTimeMillis() < deadlineMs) { + JsonNode tip = getChainTip(httpPort); + if (tip != null && tip.has("slot")) { + long slot = tip.get("slot").asLong(-1); + long epoch = slot / epochLength; + if (epoch >= targetEpoch) { + writer.accept(success("Yano tip reached epoch " + epoch + " (slot " + slot + ")")); + return true; + } + } + + long remaining = deadlineMs - System.currentTimeMillis(); + if (remaining <= 0) break; + try { + Thread.sleep(Math.min(1000L, remaining)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + + writer.accept(warn("Yano tip did not reach epoch " + targetEpoch + + " within " + timeout.toSeconds() + "s.")); + return false; + } + public boolean fundAddress(int httpPort, String address, BigDecimal ada, Consumer writer) { String url = "http://localhost:" + httpPort + "/api/v1/devnet/fund"; try { diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoCompanionService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoCompanionService.java index 0f24472e..c384af7e 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoCompanionService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoCompanionService.java @@ -21,6 +21,7 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.time.Instant; +import java.time.Duration; import java.util.HexFormat; import java.util.function.Consumer; @@ -40,6 +41,7 @@ @Slf4j public class YanoCompanionService { private static final int BOOTSTRAP_EPOCH_SHIFT = 3; + private static final String TOPOLOGY_BEFORE_YANO = "topology-before-yano.json"; private final YanoService yanoService; private final YanoBootstrapService yanoBootstrapService; @@ -117,12 +119,23 @@ public boolean bootstrap(ClusterInfo clusterInfo, Path clusterFolder, Consumer writer) throws IOException { Path topologyPath = clusterFolder.resolve("node").resolve("topology.json"); - Path topologyBackup = clusterFolder.resolve("node").resolve("topology-original.json"); + Path topologyBackup = clusterFolder.resolve("node").resolve(TOPOLOGY_BEFORE_YANO); if (!Files.exists(topologyBackup) && Files.exists(topologyPath)) { Files.copy(topologyPath, topologyBackup, StandardCopyOption.REPLACE_EXISTING); @@ -280,13 +293,26 @@ public void performHandover(ClusterInfo clusterInfo, Path clusterFolder, Consume } } + public void waitForPostHandoverBlock(Consumer writer) { + try { + writer.accept(info("Waiting for Haskell network to produce a post-handover block...")); + boolean blockProduced = clusterUtilServiceProvider.getObject().waitForNextBlocks(1, writer); + if (!blockProduced) { + writer.accept(warn("No post-handover Haskell block observed within timeout. The network may still be waiting for a leader slot.")); + } + } catch (Exception e) { + log.warn("Error waiting for post-handover Haskell block", e); + writer.accept(warn("Could not verify post-handover Haskell block production: " + e.getMessage())); + } + } + /** * Restore the original topology.json (before Yano peering was added). */ private void restoreOriginalTopology(Path clusterFolder, Consumer writer) { try { Path topologyPath = clusterFolder.resolve("node").resolve("topology.json"); - Path topologyBackup = clusterFolder.resolve("node").resolve("topology-original.json"); + Path topologyBackup = clusterFolder.resolve("node").resolve(TOPOLOGY_BEFORE_YANO); if (Files.exists(topologyBackup)) { Files.copy(topologyBackup, topologyPath, StandardCopyOption.REPLACE_EXISTING); diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoConfigBuilder.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoConfigBuilder.java index 1478101f..5fb3491f 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoConfigBuilder.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoConfigBuilder.java @@ -67,6 +67,9 @@ public boolean build(ClusterInfo clusterInfo, Path yanoConfigDir, Path yanoDataD // Past-time-travel mode if (pastTimeTravelMode) { props.put("yano.block-producer.past-time-travel-mode", "true"); + if (clusterInfo.isLocalMultiNodeEnabled()) { + props.put("yano.block-producer.past-time-travel-slot-leader-mode", "true"); + } } // Write to {yanoHome}/config/application.properties diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoGovernanceService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoGovernanceService.java index 81ac1043..67e7b85d 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoGovernanceService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoGovernanceService.java @@ -38,7 +38,9 @@ import java.math.BigInteger; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Duration; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import static com.bloxbean.cardano.client.common.CardanoConstants.LOVELACE; @@ -54,6 +56,7 @@ public class YanoGovernanceService { private final YanoBootstrapService yanoBootstrapService; private final Path plutusCostModelsBasePath; private final ObjectMapper objectMapper = new ObjectMapper(); + private final Set successfulGovernanceClusters = ConcurrentHashMap.newKeySet(); public YanoGovernanceService(YanoBootstrapService yanoBootstrapService, @Value("${yaci.cli.plutus-costmodels-path:./config}") String plutusCostModelsBasePath) { @@ -67,8 +70,16 @@ public YanoGovernanceService(YanoBootstrapService yanoBootstrapService, * All done before Yano catches up to wall clock, so the proposal is enacted by epoch 2. */ public boolean submitCostModelGovernance(ClusterInfo clusterInfo, Path clusterFolder, Consumer writer) { + String clusterName = clusterFolder.getFileName().toString(); + successfulGovernanceClusters.remove(clusterName); + try { int httpPort = clusterInfo.getYanoHttpPort(); + if (!yanoBootstrapService.waitForProtocolParams(httpPort, Duration.ofSeconds(60), writer)) { + writer.accept(error("Yano protocol parameters are not available; cannot submit governance proposals yet.")); + return false; + } + String yanoUrl = "http://localhost:" + httpPort + "/api/v1/"; BackendService backendService = new BFBackendService(yanoUrl, "dummy_key"); UtxoSupplier utxoSupplier = new DefaultUtxoSupplier(backendService.getUtxoService()); @@ -247,6 +258,7 @@ public boolean submitCostModelGovernance(ClusterInfo clusterInfo, Path clusterFo } writer.accept(info("Plutus cost models will be enacted at the next epoch boundary.")); + successfulGovernanceClusters.add(clusterName); return true; } catch (Exception e) { @@ -256,6 +268,10 @@ public boolean submitCostModelGovernance(ClusterInfo clusterInfo, Path clusterFo } } + public boolean wasCostModelGovernanceSubmitted(String clusterName) { + return successfulGovernanceClusters.contains(clusterName); + } + /** * Wait for a transaction to be confirmed in a block by polling the chain tip. * Yano in past-time-travel mode produces blocks rapidly, so this should resolve quickly. diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoService.java index f7bdae90..12898d7e 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoService.java @@ -268,6 +268,9 @@ private Process startYanoProcess(ClusterInfo clusterInfo, Path clusterFolder, Pa if (pastTimeTravelMode) { env.put("YANO_BLOCK_PRODUCER_PAST_TIME_TRAVEL_MODE", "true"); + if (clusterInfo.isLocalMultiNodeEnabled()) { + env.put("YANO_BLOCK_PRODUCER_PAST_TIME_TRAVEL_SLOT_LEADER_MODE", "true"); + } } builder.command(yanoBin.toAbsolutePath().toString()); diff --git a/config/plutus-costmodels-v11.json b/config/plutus-costmodels-v11.json new file mode 100644 index 00000000..a3d556d1 --- /dev/null +++ b/config/plutus-costmodels-v11.json @@ -0,0 +1,1022 @@ +{ + "PlutusV1": [ + 100788, + 420, + 1, + 1, + 1000, + 173, + 0, + 1, + 1000, + 59957, + 4, + 1, + 11183, + 32, + 201305, + 8356, + 4, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 100, + 100, + 16000, + 100, + 94375, + 32, + 132994, + 32, + 61462, + 4, + 72010, + 178, + 0, + 1, + 22151, + 32, + 91189, + 769, + 4, + 2, + 85848, + 228465, + 122, + 0, + 1, + 1, + 1000, + 42921, + 4, + 2, + 30623, + 28755, + 75, + 1, + 898148, + 27279, + 1, + 51775, + 558, + 1, + 39184, + 1000, + 60594, + 1, + 141895, + 32, + 83150, + 32, + 15299, + 32, + 76049, + 1, + 13169, + 4, + 22100, + 10, + 28999, + 74, + 1, + 28999, + 74, + 1, + 43285, + 552, + 1, + 44749, + 541, + 1, + 33852, + 32, + 68246, + 32, + 72362, + 32, + 7243, + 32, + 7391, + 32, + 11546, + 32, + 85848, + 228465, + 122, + 0, + 1, + 1, + 90434, + 519, + 0, + 1, + 74433, + 32, + 85848, + 228465, + 122, + 0, + 1, + 1, + 85848, + 228465, + 122, + 0, + 1, + 1, + 270652, + 22588, + 4, + 1457325, + 64566, + 4, + 20467, + 1, + 4, + 0, + 141992, + 32, + 100788, + 420, + 1, + 1, + 81663, + 32, + 59498, + 32, + 20142, + 32, + 24588, + 32, + 20744, + 32, + 25933, + 32, + 24623, + 32, + 53384111, + 14333, + 10, + 955506, + 213312, + 0, + 2, + 43053543, + 10, + 43574283, + 26308, + 10, + 16000, + 100, + 16000, + 100, + 962335, + 18, + 2780678, + 6, + 442008, + 1, + 52538055, + 3756, + 18, + 267929, + 18, + 76433006, + 8868, + 18, + 52948122, + 18, + 1995836, + 36, + 3227919, + 12, + 901022, + 1, + 166917843, + 4307, + 36, + 284546, + 36, + 158221314, + 26549, + 36, + 74698472, + 36, + 333849714, + 1, + 254006273, + 72, + 2174038, + 72, + 2261318, + 64571, + 4, + 207616, + 8310, + 4, + 1293828, + 28716, + 63, + 0, + 1, + 1006041, + 43623, + 251, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 107878, + 680, + 0, + 1, + 95336, + 1, + 281145, + 18848, + 0, + 1, + 180194, + 159, + 1, + 1, + 158519, + 8942, + 0, + 1, + 159378, + 8813, + 0, + 1, + 107490, + 3298, + 1, + 106057, + 655, + 1, + 1964219, + 24520, + 3, + 607153, + 231697, + 53144, + 0, + 1, + 116711, + 1957, + 4, + 231883, + 10, + 1000, + 24838, + 7, + 1, + 232010, + 32, + 321837444, + 25087669, + 18, + 617887431, + 67302824, + 36, + 356924, + 18413, + 45, + 21, + 219951, + 9444, + 1, + 1000, + 172116, + 183150, + 6, + 24, + 21, + 213283, + 618401, + 1998, + 28258, + 1, + 1000, + 38159, + 2, + 22, + 1000, + 95933, + 1, + 1, + 11, + 1000, + 277577, + 12, + 21 + ], + "PlutusV2": [ + 100788, + 420, + 1, + 1, + 1000, + 173, + 0, + 1, + 1000, + 59957, + 4, + 1, + 11183, + 32, + 201305, + 8356, + 4, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 100, + 100, + 16000, + 100, + 94375, + 32, + 132994, + 32, + 61462, + 4, + 72010, + 178, + 0, + 1, + 22151, + 32, + 91189, + 769, + 4, + 2, + 85848, + 228465, + 122, + 0, + 1, + 1, + 1000, + 42921, + 4, + 2, + 30623, + 28755, + 75, + 1, + 898148, + 27279, + 1, + 51775, + 558, + 1, + 39184, + 1000, + 60594, + 1, + 141895, + 32, + 83150, + 32, + 15299, + 32, + 76049, + 1, + 13169, + 4, + 22100, + 10, + 28999, + 74, + 1, + 28999, + 74, + 1, + 43285, + 552, + 1, + 44749, + 541, + 1, + 33852, + 32, + 68246, + 32, + 72362, + 32, + 7243, + 32, + 7391, + 32, + 11546, + 32, + 85848, + 228465, + 122, + 0, + 1, + 1, + 90434, + 519, + 0, + 1, + 74433, + 32, + 85848, + 228465, + 122, + 0, + 1, + 1, + 85848, + 228465, + 122, + 0, + 1, + 1, + 955506, + 213312, + 0, + 2, + 270652, + 22588, + 4, + 1457325, + 64566, + 4, + 20467, + 1, + 4, + 0, + 141992, + 32, + 100788, + 420, + 1, + 1, + 81663, + 32, + 59498, + 32, + 20142, + 32, + 24588, + 32, + 20744, + 32, + 25933, + 32, + 24623, + 32, + 43053543, + 10, + 53384111, + 14333, + 10, + 43574283, + 26308, + 10, + 1293828, + 28716, + 63, + 0, + 1, + 1006041, + 43623, + 251, + 0, + 1, + 16000, + 100, + 16000, + 100, + 962335, + 18, + 2780678, + 6, + 442008, + 1, + 52538055, + 3756, + 18, + 267929, + 18, + 76433006, + 8868, + 18, + 52948122, + 18, + 1995836, + 36, + 3227919, + 12, + 901022, + 1, + 166917843, + 4307, + 36, + 284546, + 36, + 158221314, + 26549, + 36, + 74698472, + 36, + 333849714, + 1, + 254006273, + 72, + 2174038, + 72, + 2261318, + 64571, + 4, + 207616, + 8310, + 4, + 100181, + 726, + 719, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 107878, + 680, + 0, + 1, + 95336, + 1, + 281145, + 18848, + 0, + 1, + 180194, + 159, + 1, + 1, + 158519, + 8942, + 0, + 1, + 159378, + 8813, + 0, + 1, + 107490, + 3298, + 1, + 106057, + 655, + 1, + 1964219, + 24520, + 3, + 607153, + 231697, + 53144, + 0, + 1, + 116711, + 1957, + 4, + 231883, + 10, + 1000, + 24838, + 7, + 1, + 232010, + 32, + 321837444, + 25087669, + 18, + 617887431, + 67302824, + 36, + 356924, + 18413, + 45, + 21, + 219951, + 9444, + 1, + 1000, + 172116, + 183150, + 6, + 24, + 21, + 213283, + 618401, + 1998, + 28258, + 1, + 1000, + 38159, + 2, + 22, + 1000, + 95933, + 1, + 1, + 11, + 1000, + 277577, + 12, + 21 + ], + "PlutusV3": [ + 100788, + 420, + 1, + 1, + 1000, + 173, + 0, + 1, + 1000, + 59957, + 4, + 1, + 11183, + 32, + 201305, + 8356, + 4, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 100, + 100, + 16000, + 100, + 94375, + 32, + 132994, + 32, + 61462, + 4, + 72010, + 178, + 0, + 1, + 22151, + 32, + 91189, + 769, + 4, + 2, + 85848, + 123203, + 7305, + -900, + 1716, + 960, + 57, + 85848, + 0, + 1, + 1, + 1000, + 42921, + 4, + 2, + 30623, + 28755, + 75, + 1, + 898148, + 27279, + 1, + 51775, + 558, + 1, + 39184, + 1000, + 60594, + 1, + 141895, + 32, + 83150, + 32, + 15299, + 32, + 76049, + 1, + 13169, + 4, + 22100, + 10, + 28999, + 74, + 1, + 28999, + 74, + 1, + 43285, + 552, + 1, + 44749, + 541, + 1, + 33852, + 32, + 68246, + 32, + 72362, + 32, + 7243, + 32, + 7391, + 32, + 11546, + 32, + 85848, + 123203, + 7305, + -900, + 1716, + 960, + 57, + 85848, + 0, + 1, + 90434, + 519, + 0, + 1, + 74433, + 32, + 85848, + 123203, + 7305, + -900, + 1716, + 960, + 57, + 85848, + 0, + 1, + 1, + 85848, + 123203, + 7305, + -900, + 1716, + 960, + 57, + 85848, + 0, + 1, + 955506, + 213312, + 0, + 2, + 270652, + 22588, + 4, + 1457325, + 64566, + 4, + 20467, + 1, + 4, + 0, + 141992, + 32, + 100788, + 420, + 1, + 1, + 81663, + 32, + 59498, + 32, + 20142, + 32, + 24588, + 32, + 20744, + 32, + 25933, + 32, + 24623, + 32, + 43053543, + 10, + 53384111, + 14333, + 10, + 43574283, + 26308, + 10, + 16000, + 100, + 16000, + 100, + 962335, + 18, + 2780678, + 6, + 442008, + 1, + 52538055, + 3756, + 18, + 267929, + 18, + 76433006, + 8868, + 18, + 52948122, + 18, + 1995836, + 36, + 3227919, + 12, + 901022, + 1, + 166917843, + 4307, + 36, + 284546, + 36, + 158221314, + 26549, + 36, + 74698472, + 36, + 333849714, + 1, + 254006273, + 72, + 2174038, + 72, + 2261318, + 64571, + 4, + 207616, + 8310, + 4, + 1293828, + 28716, + 63, + 0, + 1, + 1006041, + 43623, + 251, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 107878, + 680, + 0, + 1, + 95336, + 1, + 281145, + 18848, + 0, + 1, + 180194, + 159, + 1, + 1, + 158519, + 8942, + 0, + 1, + 159378, + 8813, + 0, + 1, + 107490, + 3298, + 1, + 106057, + 655, + 1, + 1964219, + 24520, + 3, + 607153, + 231697, + 53144, + 0, + 1, + 116711, + 1957, + 4, + 231883, + 10, + 1000, + 24838, + 7, + 1, + 232010, + 32, + 321837444, + 25087669, + 18, + 617887431, + 67302824, + 36, + 356924, + 18413, + 45, + 21, + 219951, + 9444, + 1, + 1000, + 172116, + 183150, + 6, + 24, + 21, + 213283, + 618401, + 1998, + 28258, + 1, + 1000, + 38159, + 2, + 22, + 1000, + 95933, + 1, + 1, + 11, + 1000, + 277577, + 12, + 21 + ] +} From 1c0394bdc9a6dd2b6abb9e25d964c2e2cd2ee457 Mon Sep 17 00:00:00 2001 From: Satya Date: Sat, 16 May 2026 23:26:53 +0800 Subject: [PATCH 13/17] Set default value for ccThreshold , so that governance action auto pass --- .../cardano/yacicli/localcluster/config/GenesisConfig.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 2d18f962..b3fd1238 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 @@ -146,8 +146,8 @@ public class GenesisConfig { private String constitutionScript = "186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4"; private List ccMembers = new ArrayList<>(); - private int ccThresholdNumerator = 2; - private int ccThresholdDenominator = 3; + private int ccThresholdNumerator = 0; + private int ccThresholdDenominator = 1; private boolean disableFaucet = false; private boolean disableShelleyInitialFunds = false; From d546c55009b761914edef15a0d29c841449508a6 Mon Sep 17 00:00:00 2001 From: Satya Date: Sat, 16 May 2026 23:58:02 +0800 Subject: [PATCH 14/17] feat(cli): align devkit defaults for Yano and Store tx evaluation Disable Ogmios by default in docker devkit and make Yaci Store choose the tx evaluator mode from the active Ogmios runtime state. Store now uses Ogmios only when Ogmios is enabled and running, otherwise it falls back to Scalus. Add Yano download support and align Yano ports to 6060 for HTTP and 14447 for N2N across CLI config, docker-compose, and runtime defaults. Update Cardano/Yaci component versions, add root Plutus cost model config, and extend native-image metadata so N2C commands work correctly in the native CLI build. --- .gitignore | 1 + applications/cli/build.gradle | 8 +- .../cli/config/application.properties | 2 +- applications/cli/config/download.properties | 12 +- applications/cli/config/node.properties | 12 +- .../cli/docker/application.properties | 1 + applications/cli/docker/download-amd64.sh | 4 +- applications/cli/docker/download-arm64.sh | 4 +- .../commands/common/DownloadService.java | 118 ++++++++++++++++++ .../commands/general/DownloadCommand.java | 13 +- .../yacicli/localcluster/ClusterInfo.java | 2 +- .../localcluster/ClusterStartService.java | 17 ++- .../localcluster/commands/TxnCommands.java | 10 +- .../localcluster/ogmios/OgmiosService.java | 12 +- .../yacistore/YaciStoreConfigBuilder.java | 8 +- .../yacistore/YaciStoreService.java | 27 +++- .../yano/YanoCompanionService.java | 2 + .../yaci-cli/native-image.properties | 1 + .../yaci-cli/native-image.properties.ci | 1 + .../native-image/yaci-cli/reflect-config.json | 94 +++++++++++++- .../cli/src/main/resources/logback-spring.xml | 3 + config/env | 4 +- config/node.properties | 6 + e2e-tests/evolution-sdk/devnet.ts | 88 +++++++++++++ scripts/docker-compose.yml | 2 + 25 files changed, 406 insertions(+), 46 deletions(-) create mode 100644 e2e-tests/evolution-sdk/devnet.ts diff --git a/.gitignore b/.gitignore index e6d37f7a..6a007e21 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ applications/cli/wallet-ui/node_modules/ applications/cli/src/main/resources/static/wallet/ /applications/cli/chainstate/ /e2e-tests/evolution-sdk/node_modules/ +/chainstate/ diff --git a/applications/cli/build.gradle b/applications/cli/build.gradle index e665dd9c..a151c284 100644 --- a/applications/cli/build.gradle +++ b/applications/cli/build.gradle @@ -38,12 +38,12 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-logging' implementation 'org.springframework.boot:spring-boot-starter-mustache' - implementation('com.bloxbean.cardano:yaci:0.4.0') { + implementation('com.bloxbean.cardano:yaci:0.4.3') { exclude group: 'com.bloxbean.cardano', module: 'cardano-client-core' } - implementation 'com.bloxbean.cardano:cardano-client-lib:0.7.1' - implementation 'com.bloxbean.cardano:cardano-client-backend:0.7.1' - implementation 'com.bloxbean.cardano:cardano-client-backend-blockfrost:0.7.1' + implementation 'com.bloxbean.cardano:cardano-client-lib:0.7.2' + implementation 'com.bloxbean.cardano:cardano-client-backend:0.7.2' + implementation 'com.bloxbean.cardano:cardano-client-backend-blockfrost:0.7.2' implementation 'org.apache.commons:commons-compress:1.23.0' diff --git a/applications/cli/config/application.properties b/applications/cli/config/application.properties index 7cd0066b..3c7dffb6 100644 --- a/applications/cli/config/application.properties +++ b/applications/cli/config/application.properties @@ -26,7 +26,7 @@ bp.create.enabled=true #socat.port=3333 #prometheus.port=12798 #yano.server.port=14447 -#yano.http.port=6666 +#yano.http.port=6060 ###################################################### diff --git a/applications/cli/config/download.properties b/applications/cli/config/download.properties index 1059baaa..01c6ea1d 100644 --- a/applications/cli/config/download.properties +++ b/applications/cli/config/download.properties @@ -1,14 +1,14 @@ #Please specify either the version or the full url for the following components -node.version=10.6.3 +node.version=11.0.1 ogmios.version=6.14.0 kupo.version=2.11.0 -yaci.store.tag=rel-native-2.0.0 -yaci.store.version=2.0.0 -yaci.store.jar.version=2.0.0 +yaci.store.tag=rel-native-2.0.1-rc1 +yaci.store.version=2.0.1-rc1 +yaci.store.jar.version=2.0.1-rc1 -yano.tag=v0.1.0-pre1 -yano.version=0.1.0-pre1 +yano.tag=v0.1.0-pre3 +yano.version=0.1.0-pre3 #node.url= #ogmios.url= diff --git a/applications/cli/config/node.properties b/applications/cli/config/node.properties index 02f65ae1..ea6d9a4e 100644 --- a/applications/cli/config/node.properties +++ b/applications/cli/config/node.properties @@ -1,3 +1,9 @@ +# Node mode: yano-primary (Yano BP + Haskell relay, supports rollback), +# companion (Yano bootstraps + Haskell takes over), +# yano-only (Yano only, fastest startup), +# haskell-only (legacy, Haskell node only) +nodeMode=companion + #protocolMagic=42 #maxKESEvolutions=60 #securityParam=80 @@ -65,9 +71,9 @@ #dvtPPGovGroup=0.51f #dvtTreasuryWithdrawal=0.51f -committeeMinSize=0 -ccThresholdNumerator=0 -ccThresholdDenominator=1 +#committeeMinSize=0 +#ccThresholdNumerator=0 +#ccThresholdDenominator=1 #committeeMaxTermLength=200 #govActionLifetime=10 #govActionDeposit=1000000000 diff --git a/applications/cli/docker/application.properties b/applications/cli/docker/application.properties index 9de797f0..5319999a 100644 --- a/applications/cli/docker/application.properties +++ b/applications/cli/docker/application.properties @@ -2,6 +2,7 @@ spring.config.import=optional:file:/app/config/node.properties cardano.cli.path=/app/cardano-bin yaci.store.folder=/app/store +yano.folder=/app/yano ogmios.folder=/app/ogmios kupo.folder=/app/kupo local.cluster.home=/clusters/nodes diff --git a/applications/cli/docker/download-amd64.sh b/applications/cli/docker/download-amd64.sh index 05921d9d..1ca0ce47 100644 --- a/applications/cli/docker/download-amd64.sh +++ b/applications/cli/docker/download-amd64.sh @@ -1,5 +1,5 @@ -file=cardano-node-10.6.2-linux-amd64.tar.gz -wget https://github.com/IntersectMBO/cardano-node/releases/download/10.6.2/cardano-node-10.6.2-linux-amd64.tar.gz +file=cardano-node-11.0.1-linux-amd64.tar.gz +wget https://github.com/IntersectMBO/cardano-node/releases/download/11.0.1/cardano-node-11.0.1-linux-amd64.tar.gz mkdir /app/cardano-bin diff --git a/applications/cli/docker/download-arm64.sh b/applications/cli/docker/download-arm64.sh index 86866201..1be0adb5 100755 --- a/applications/cli/docker/download-arm64.sh +++ b/applications/cli/docker/download-arm64.sh @@ -1,5 +1,5 @@ -file=cardano-10_6_2-aarch64-static-musl-ghc_9122.tar.zst -dir=cardano-10_5_0-aarch64-static-musl-ghc_9122 +file=cardano-11_0_1-aarch64-static-musl-ghc_9122.tar.zst +dir=cardano-11_0_1-aarch64-static-musl-ghc_9122 wget https://github.com/armada-alliance/cardano-node-binaries/raw/main/static-binaries/$file?raw=true -O - | tar -I zstd -xv #unzip $file diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/commands/common/DownloadService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/commands/common/DownloadService.java index 28d210d8..b88d5265 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/commands/common/DownloadService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/commands/common/DownloadService.java @@ -31,6 +31,7 @@ public class DownloadService { private final static String YACI_STORE_DOWNLOAD_URL = "https://github.com/bloxbean/yaci-store/releases/download"; private final static String OGMIOS_DOWNLOAD_URL = "https://github.com/CardanoSolutions/ogmios/releases/download"; private final static String KUPO_DOWNLOAD_URL = "https://github.com/CardanoSolutions/kupo/releases/download"; + private final static String YANO_DOWNLOAD_URL = "https://github.com/bloxbean/yano/releases/download"; private final ClusterConfig clusterConfig; @@ -67,6 +68,15 @@ public class DownloadService { @Value("${kupo.url:#{null}}") private String kupoUrl; + @Value("${yano.version:#{null}}") + private String yanoVersion; + + @Value("${yano.tag:#{null}}") + private String yanoTag; + + @Value("${yano.url:#{null}}") + private String yanoUrl; + public boolean downloadNode(boolean overwrite) { String downloadPath = resolveNodeDownloadPath(); @@ -347,6 +357,80 @@ public boolean downloadKupo(boolean overwrite) { return false; } + public boolean downloadYano(boolean overwrite) { + String downloadPath = resolveYanoDownloadPath(); + + if (downloadPath == null) { + writeLn(error("Download URL for Yano is not set. Please set the download URL in download.properties")); + return false; + } + + Path yanoExec = Path.of(clusterConfig.getYanoHome(), "yano"); + + if (yanoExec.toFile().exists()) { + if (!overwrite) { + writeLn(info("Yano already exists in %s", yanoExec.toFile().getAbsolutePath())); + writeLn(info("Use --overwrite to overwrite the existing yano")); + return false; + } else { + deleteExistingDir("yano", clusterConfig.getYanoHome()); + } + } + + String targetDir = clusterConfig.getYanoHome(); + var downloadedFile = download("yano", downloadPath, targetDir, "yano-native.zip"); + if (downloadedFile != null) { + try { + // Extract full zip to a temp folder, then move contents to yanoHome + var tmpFolder = Paths.get(clusterConfig.getYanoHome(), "tmp"); + extractZip(downloadedFile.toFile().getAbsolutePath(), tmpFolder.toFile().getAbsolutePath()); + + // The zip contains a single folder (e.g. yano-native-0.1.0-pre1-macos-arm64/) + // Move all its contents to yanoHome + File[] extractedItems = tmpFolder.toFile().listFiles(); + File extractedRoot = null; + if (extractedItems != null) { + for (File f : extractedItems) { + if (f.isDirectory() && f.getName().startsWith("yano")) { + extractedRoot = f; + break; + } + } + } + + if (extractedRoot != null) { + // Copy entire contents (binary, config, scripts) to yanoHome + File[] contents = extractedRoot.listFiles(); + if (contents != null) { + Path yanoHome = Path.of(clusterConfig.getYanoHome()); + for (File item : contents) { + Path dest = yanoHome.resolve(item.getName()); + if (item.isDirectory()) { + FileUtils.copyDirectory(item, dest.toFile()); + } else { + Files.copy(item.toPath(), dest, StandardCopyOption.REPLACE_EXISTING); + } + } + } + writeLn(success("Extracted yano to " + clusterConfig.getYanoHome())); + } else { + writeLn(error("yano folder not found in the extracted archive")); + } + + setExecutablePermission(yanoExec.toFile().getAbsolutePath()); + FileUtils.deleteDirectory(tmpFolder.toFile()); + Files.deleteIfExists(downloadedFile); + return true; + } catch (IOException e) { + writeLn(error("Error extracting yano: " + e.getMessage())); + } + } else { + writeLn(error("Download failed for yano")); + } + + return false; + } + private void setExecutablePermission(String path) { File file = new File(path); if (!file.exists()) return; @@ -620,6 +704,40 @@ private String resolveKupoDownloadPath() { return url; } + private String resolveYanoDownloadPath() { + if (!StringUtils.isEmpty(yanoUrl)) { + return yanoUrl; + } + + if (StringUtils.isEmpty(yanoVersion) || StringUtils.isEmpty(yanoTag)) { + writeLn(error("Yano version/tag is not set. Please set yano.version and yano.tag or yano.url in download.properties")); + return null; + } + + String osPrefix = null; + if (SystemUtils.IS_OS_MAC) { + osPrefix = "macos"; + } else if (SystemUtils.IS_OS_LINUX) { + osPrefix = "linux"; + } else { + writeLn(error("Unsupported OS : " + System.getProperty("os.name"))); + } + + String arch = System.getProperty("os.arch"); + String cpuArch; + if (arch.startsWith("aarch") || arch.startsWith("arm")) { + cpuArch = "arm64"; + } else { + cpuArch = "x64"; + } + + if (osPrefix == null) + return null; + + // Release asset pattern: yano-native-{version}-{os}-{arch}.zip + return YANO_DOWNLOAD_URL + "/" + yanoTag + "/yano-native-" + yanoVersion + "-" + osPrefix + "-" + cpuArch + ".zip"; + } + private void deleteExistingDir(String componentName, String componentHome) { Objects.requireNonNull(componentHome, "Component home cannot be null"); diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/commands/general/DownloadCommand.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/commands/general/DownloadCommand.java index 71f804b6..81bf2552 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/commands/general/DownloadCommand.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/commands/general/DownloadCommand.java @@ -25,7 +25,7 @@ public class DownloadCommand { @ShellMethod(value = "Download", key = "download") @ShellMethodAvailability({"nonDockerCommandAvailability"}) public boolean download( - @ShellOption(value = {"--component", "-c"}, defaultValue = "all", help = "Provide list of components separated by space. Components: node,ogmios,kupo,yaci-store,yaci-store-jar") String[] components, + @ShellOption(value = {"--component", "-c"}, defaultValue = "all", help = "Provide list of components separated by space. Components: node,ogmios,kupo,yaci-store,yaci-store-jar,yano") String[] components, @ShellOption(value = {"-o", "--overwrite"}, defaultValue = "false", help = "Overwrite existing installation. default: false") boolean overwrite ) { @@ -63,6 +63,11 @@ public boolean download( validComponent = true; } + if (componentList.contains("all") || componentList.contains("yano")) { + downloadService.downloadYano(overwrite); + validComponent = true; + } + if (!validComponent) { writeLn(error("Invalid components : " + componentList)); return false; @@ -85,11 +90,11 @@ public void downloadAndStart( @ShellOption(value = {"--port"}, help = "Node port (Used with --create option only)", defaultValue = "3001") int port, @ShellOption(value = {"--submit-api-port"}, help = "Submit Api Port", defaultValue = "8090") int submitApiPort, @ShellOption(value = {"-s", "--slot-length"}, help = "Slot Length in sec. (0.1 to ..)", defaultValue = "1") double slotLength, - @ShellOption(value = {"-b", "--block-time"}, help = "Block time in sec. (1 - 20)", defaultValue = "1") double blockTime, + @ShellOption(value = {"-b", "--block-time"}, help = "Block time in sec. (0.1 - 20)", defaultValue = "1") double blockTime, @ShellOption(value = {"-e", "--epoch-length"}, help = "No of slots in an epoch", defaultValue = "600") int epochLength, @ShellOption(value = {"--genesis-profile"}, defaultValue = ShellOption.NULL, help = "Use a pre-defined genesis profile (Options: zero_fee, zero_min_utxo_value, zero_fee_and_min_utxo_value)") GenesisProfile genesisProfile, - @ShellOption(value = {"--enable-yaci-store"}, defaultValue = "false", help = "Enable Yaci Store. This will also enable Ogmios for Tx Evaluation") boolean enableYaciStore, + @ShellOption(value = {"--enable-yaci-store"}, defaultValue = "false", help = "Enable Yaci Store") boolean enableYaciStore, @ShellOption(value = {"--enable-kupomios"}, defaultValue = "false", help= "Enable Ogmios and Kupo") boolean enableKupomios, @ShellOption(value = {"--interactive"}, defaultValue="false", help="To start in interactive mode when 'up' command is passed as an arg to yaci-cli") boolean interactive, @ShellOption(value = {"--tail"}, defaultValue="false", help="To tail the network when 'up' command is passed as an arg to yaci-cli. Only works in non-interactive mode.") boolean tail, @@ -104,7 +109,7 @@ public void downloadAndStart( if (enableYaciStore) { applicationConfig.setYaciStoreEnabled(true); - applicationConfig.setOgmiosEnabled(true); + applicationConfig.setOgmiosEnabled(false); //TODO -- Temporarily till Ogmios is ready } else if (enableKupomios){ applicationConfig.setOgmiosEnabled(true); applicationConfig.setKupoEnabled(true); diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterInfo.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterInfo.java index 76c389e7..bfc97ab5 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterInfo.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterInfo.java @@ -49,7 +49,7 @@ public class ClusterInfo { @Builder.Default private int yanoServerPort = 14447; @Builder.Default - private int yanoHttpPort = 6666; + private int yanoHttpPort = 6060; private boolean localMultiNodeEnabled; private int localMultiNodeStakeRatioFactor; 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 3e923498..c1c1f83f 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 @@ -175,16 +175,7 @@ public RunStatus startCluster(ClusterInfo clusterInfo, Path clusterFolder, Consu writer.accept(error("Node process could not be started.")); return new RunStatus(false, firstRun); } - - Process submitApiProcess = startSubmitApi(clusterInfo, clusterFolder, writer); - if (submitApiProcess == null) { - writer.accept(error("Submit API process could not be started.")); - return new RunStatus(false, firstRun); - } - processes.add(nodeProcess); - if (submitApiProcess != null) - processes.add(submitApiProcess); // Companion mode handover: sync as relay, stop Yano, restart as block producer if (companionBootstrapDone) { @@ -220,6 +211,14 @@ public RunStatus startCluster(ClusterInfo clusterInfo, Path clusterFolder, Consu } } + Process submitApiProcess = startSubmitApi(clusterInfo, clusterFolder, writer); + if (submitApiProcess == null) { + writer.accept(error("Submit API process could not be started.")); + return new RunStatus(false, firstRun); + } + + processes.add(submitApiProcess); + // In companion mode, still report firstRun=true so FirstRunDone fires // (topup + cost model governance proposals run against the Haskell node). // The Haskell node syncs Yano's epoch history, so it starts at a later epoch diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/commands/TxnCommands.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/commands/TxnCommands.java index ac3f0667..ee0c473e 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/commands/TxnCommands.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/commands/TxnCommands.java @@ -31,8 +31,8 @@ public class TxnCommands { @ShellMethod(value = "Topup account", key = "topup") @ShellMethodAvailability("localClusterCmdAvailability") - public void mintToken(@ShellOption(value = {"-a", "--address"}, help = "Receiver address") String address, - @ShellOption(value = {"-v", "--value"}, help = "Ada value") double adaValue) { + public void topup(@ShellOption(value = {"-a", "--address"}, help = "Receiver address") String address, + @ShellOption(value = {"-v", "--value"}, help = "Ada value") double adaValue) { String clusterName = CommandContext.INSTANCE.getProperty(CLUSTER_NAME); Era era = CommandContext.INSTANCE.getEra(); @@ -82,9 +82,9 @@ public void getUtxos(@ShellOption(value = {"-a", "--address"}, help = "Address") @ShellMethod(value = "Mint tokens using faucet account with default policy", key = "mint") @ShellMethodAvailability("localClusterCmdAvailability") - public void mintToken(@ShellOption(value = {"-n", "--asset-name"}, help = "Asset Name") String assetName, - @ShellOption(value = {"-q", "--quantity"}, help = "Quantity") BigInteger quantity, - @ShellOption(value = {"-r", "--receiver"}, help = "Reciever Address") String receiver + public void topup(@ShellOption(value = {"-n", "--asset-name"}, help = "Asset Name") String assetName, + @ShellOption(value = {"-q", "--quantity"}, help = "Quantity") BigInteger quantity, + @ShellOption(value = {"-r", "--receiver"}, help = "Reciever Address") String receiver ) { String clusterName = CommandContext.INSTANCE.getProperty(CLUSTER_NAME); Era era = CommandContext.INSTANCE.getEra(); diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ogmios/OgmiosService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ogmios/OgmiosService.java index 9bc5e51b..6b968242 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ogmios/OgmiosService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ogmios/OgmiosService.java @@ -14,6 +14,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import java.io.File; @@ -41,11 +42,13 @@ public class OgmiosService { private final ProcessUtil processUtil; private List processes = new ArrayList<>(); + private Process ogmiosProcess; private Queue ogmiosLogs = EvictingQueue.create(300); private Queue kupoLogs = EvictingQueue.create(300); @EventListener + @Order(0) public void handleClusterStarted(ClusterStarted clusterStarted) { String clusterName = clusterStarted.getClusterName(); @@ -80,8 +83,10 @@ public boolean start(String clusterName, Consumer writer) { return false; Process process = startOgmios(clusterName, clusterInfo); - if (process != null) + if (process != null) { + ogmiosProcess = process; processes.add(process); + } } if (appConfig.isKupoEnabled()) { @@ -99,6 +104,10 @@ public boolean start(String clusterName, Consumer writer) { return true; } + public boolean isOgmiosRunning() { + return ogmiosProcess != null && ogmiosProcess.isAlive(); + } + private static boolean ogmiosPortAvailabilityCheck(ClusterInfo clusterInfo, Consumer writer) { boolean ogmiosPortAvailable = PortUtil.isPortAvailable(clusterInfo.getOgmiosPort()); if (!ogmiosPortAvailable) { @@ -217,6 +226,7 @@ public boolean stop(Consumer writer) { //clean pid files processUtil.deletePidFile(OGMIOS_PROCESS_NAME); processUtil.deletePidFile(KUPO_PROCESS_NAME); + ogmiosProcess = null; } ogmiosLogs.clear(); diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yacistore/YaciStoreConfigBuilder.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yacistore/YaciStoreConfigBuilder.java index 2300348f..55e1fc42 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yacistore/YaciStoreConfigBuilder.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yacistore/YaciStoreConfigBuilder.java @@ -12,7 +12,6 @@ import java.nio.file.Path; import java.util.LinkedHashMap; import java.util.Map; -import java.util.Properties; import static com.bloxbean.cardano.yacicli.util.ConsoleWriter.error; @@ -26,11 +25,11 @@ public class YaciStoreConfigBuilder { private final ClusterConfig clusterConfig; - public boolean build(ClusterInfo clusterInfo) { + public boolean build(ClusterInfo clusterInfo, String txEvaluatorMode) { Path nodeSocketPath = Path.of(clusterInfo.getSocketPath()); String nodeFolder = nodeSocketPath.getParent().toFile().getAbsolutePath(); - Map storeProperties = new LinkedHashMap(); + Map storeProperties = new LinkedHashMap<>(); storeProperties.put("server.port", String.valueOf(clusterInfo.getYaciStorePort())); boolean yanoOnly = NodeMode.YANO_ONLY == clusterInfo.getNodeMode(); @@ -52,6 +51,7 @@ public boolean build(ClusterInfo clusterInfo) { storeProperties.put("store.cardano.submit-api-url", "http://localhost:" + clusterInfo.getSubmitApiPort() + "/api/submit/tx"); } storeProperties.put("store.cardano.ogmios-url", "http://localhost:" + clusterInfo.getOgmiosPort()); + storeProperties.put("store.submit.tx-evaluator-mode", txEvaluatorMode); storeProperties.put("spring.datasource.url", "jdbc:h2:file:" + nodeFolder + "/yaci_store/storedb;MV_STORE=TRUE;AUTO_SERVER=TRUE;AUTO_RECONNECT=TRUE;LOCK_TIMEOUT=120000"); storeProperties.put("spring.datasource.username", "sa"); storeProperties.put("spring.datasource.password", "password"); @@ -125,6 +125,8 @@ public boolean build(ClusterInfo clusterInfo) { try (BufferedWriter writer = Files.newBufferedWriter(n2cOverridePath)) { writer.write("store.epoch.endpoints.epoch.local.enabled=false"); writer.newLine(); + writer.write("store.submit.tx-evaluator-mode=" + txEvaluatorMode); + writer.newLine(); } catch (IOException e) { e.printStackTrace(); error("Error creating N2C override config: " + e.getMessage()); diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yacistore/YaciStoreService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yacistore/YaciStoreService.java index 3f110f0a..3c4651e2 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yacistore/YaciStoreService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yacistore/YaciStoreService.java @@ -16,6 +16,7 @@ import com.bloxbean.cardano.yacicli.localcluster.events.ClusterStarted; import com.bloxbean.cardano.yacicli.localcluster.events.ClusterStopped; import com.bloxbean.cardano.yacicli.localcluster.events.RollbackDone; +import com.bloxbean.cardano.yacicli.localcluster.ogmios.OgmiosService; import com.bloxbean.cardano.yacicli.localcluster.service.ClusterUtilService; import com.bloxbean.cardano.yacicli.util.PortUtil; import com.bloxbean.cardano.yacicli.util.ProcessStream; @@ -27,6 +28,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClientException; @@ -49,6 +51,9 @@ @Slf4j public class YaciStoreService { private static final String STORE_PROCESS_NAME = "yaci-store"; + private static final String TX_EVALUATOR_MODE_OGMIOS = "ogmios"; + private static final String TX_EVALUATOR_MODE_SCALUS = "scalus"; + private final ApplicationConfig appConfig; private final ClusterService clusterService; private final ClusterStartService clusterStartService; @@ -58,6 +63,7 @@ public class YaciStoreService { private final YaciStoreConfigBuilder yaciStoreConfigBuilder; private final YaciStoreCustomDbHelper customDBHelper; private final ProcessUtil processUtil; + private final OgmiosService ogmiosService; private List processes = new ArrayList<>(); @@ -67,6 +73,7 @@ public class YaciStoreService { private Queue logs = EvictingQueue.create(300); @EventListener + @Order(1) public void handleClusterStarted(ClusterStarted clusterStarted) { String clusterName = clusterStarted.getClusterName(); @@ -98,7 +105,7 @@ public boolean start(String clusterName, Consumer writer) { return false; Era era = clusterInfo.getEra(); - StoreStartResult result = startStoreApp(clusterInfo, era); + StoreStartResult result = startStoreApp(clusterInfo, era, writer); // Always track a live process so it can be cleaned up by stop(), even when // the boot log wasn't observed (the process may still recover). if (result.process() != null) @@ -217,11 +224,14 @@ private Long fetchIndexerHeight(RestTemplate restTemplate, ObjectMapper mapper, } } - private StoreStartResult startStoreApp(ClusterInfo clusterInfo, Era era) throws IOException, InterruptedException, ExecutionException, TimeoutException { + private StoreStartResult startStoreApp(ClusterInfo clusterInfo, Era era, Consumer writer) throws IOException, InterruptedException, ExecutionException, TimeoutException { ProcessBuilder builder = new ProcessBuilder(); builder.directory(new File(clusterConfig.getYaciStoreBinPath())); boolean yanoOnly = NodeMode.YANO_ONLY == clusterInfo.getNodeMode(); + String txEvaluatorMode = resolveTxEvaluatorMode(writer); + builder.environment().put("STORE_SUBMIT_TX_EVALUATOR_MODE", txEvaluatorMode); + writer.accept(info("Yaci Store tx evaluator mode: " + txEvaluatorMode)); // In yano-only mode, force Java mode if jar is available. // The native binary bakes in the n2c profile at compile time, making it impossible @@ -250,7 +260,7 @@ private StoreStartResult startStoreApp(ClusterInfo clusterInfo, Era era) throws } if (!appConfig.isDocker()) { - yaciStoreConfigBuilder.build(clusterInfo); + yaciStoreConfigBuilder.build(clusterInfo, txEvaluatorMode); } if (effectiveMode != null && effectiveMode.equals("native")) { @@ -348,6 +358,17 @@ private StoreStartResult startStoreApp(ClusterInfo clusterInfo, Era era) throws return new StoreStartResult(process, started.get()); } + private String resolveTxEvaluatorMode(Consumer writer) { + if (appConfig.isOgmiosEnabled() && ogmiosService.isOgmiosRunning()) + return TX_EVALUATOR_MODE_OGMIOS; + + if (appConfig.isOgmiosEnabled()) { + writer.accept(warn("Ogmios is enabled but not running. Using Scalus tx evaluator for Yaci Store.")); + } + + return TX_EVALUATOR_MODE_SCALUS; + } + private Process startViewerApp(String cluster) throws IOException, InterruptedException, ExecutionException, TimeoutException { ProcessBuilder builder = new ProcessBuilder(); diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoCompanionService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoCompanionService.java index c384af7e..a3a4ba15 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoCompanionService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoCompanionService.java @@ -1,6 +1,7 @@ package com.bloxbean.cardano.yacicli.localcluster.yano; import com.bloxbean.cardano.yacicli.common.Tuple; +import com.bloxbean.cardano.yacicli.localcluster.ClusterConfig; import com.bloxbean.cardano.yacicli.localcluster.ClusterInfo; import com.bloxbean.cardano.yacicli.localcluster.service.ClusterUtilService; import com.bloxbean.cardano.yaci.core.protocol.chainsync.messages.Point; @@ -46,6 +47,7 @@ public class YanoCompanionService { private final YanoService yanoService; private final YanoBootstrapService yanoBootstrapService; private final YanoGovernanceService yanoGovernanceService; + private final ClusterConfig clusterConfig; // ObjectProvider to break the constructor cycle: // ClusterService -> ClusterStartService -> YanoCompanionService -> ClusterUtilService -> ClusterService. // The bean is resolved lazily at call time (see performHandover). diff --git a/applications/cli/src/main/resources/META-INF/native-image/yaci-cli/native-image.properties b/applications/cli/src/main/resources/META-INF/native-image/yaci-cli/native-image.properties index 9994ee87..175187d5 100644 --- a/applications/cli/src/main/resources/META-INF/native-image/yaci-cli/native-image.properties +++ b/applications/cli/src/main/resources/META-INF/native-image/yaci-cli/native-image.properties @@ -10,5 +10,6 @@ Args = -H:+StaticExecutableWithDynamicLibC -march=compatibility --initialize-at- --initialize-at-run-time=io.netty.channel.unix.IovArray \ --initialize-at-run-time=io.netty.channel.unix.Limits \ --initialize-at-run-time=io.netty.util.internal.logging.Log4JLogger \ +--initialize-at-run-time=io.netty.util.internal.shaded.org.jctools \ --initialize-at-run-time=io.netty.channel.kqueue.KQueue \ --initialize-at-run-time=io.netty.handler.ssl.BouncyCastleAlpnSslUtils diff --git a/applications/cli/src/main/resources/META-INF/native-image/yaci-cli/native-image.properties.ci b/applications/cli/src/main/resources/META-INF/native-image/yaci-cli/native-image.properties.ci index 46c401d8..9c74f226 100644 --- a/applications/cli/src/main/resources/META-INF/native-image/yaci-cli/native-image.properties.ci +++ b/applications/cli/src/main/resources/META-INF/native-image/yaci-cli/native-image.properties.ci @@ -10,5 +10,6 @@ Args = --static --libc=musl -march=compatibility --initialize-at-run-time=io.net --initialize-at-run-time=io.netty.channel.unix.IovArray \ --initialize-at-run-time=io.netty.channel.unix.Limits \ --initialize-at-run-time=io.netty.util.internal.logging.Log4JLogger \ +--initialize-at-run-time=io.netty.util.internal.shaded.org.jctools \ --initialize-at-run-time=io.netty.channel.kqueue.KQueue \ --initialize-at-run-time=io.netty.handler.ssl.BouncyCastleAlpnSslUtils diff --git a/applications/cli/src/main/resources/META-INF/native-image/yaci-cli/reflect-config.json b/applications/cli/src/main/resources/META-INF/native-image/yaci-cli/reflect-config.json index 6151640b..06c804b7 100644 --- a/applications/cli/src/main/resources/META-INF/native-image/yaci-cli/reflect-config.json +++ b/applications/cli/src/main/resources/META-INF/native-image/yaci-cli/reflect-config.json @@ -17,6 +17,99 @@ "allPublicMethods" : true, "allPublicFields" : true }, + { + "name":"io.netty.util.internal.shaded.org.jctools.queues.unpadded.MpscUnpaddedArrayQueueProducerIndexField", + "allDeclaredFields":true + }, + { + "name":"io.netty.util.internal.shaded.org.jctools.queues.unpadded.MpscUnpaddedArrayQueueProducerLimitField", + "allDeclaredFields":true + }, + { + "name":"io.netty.util.internal.shaded.org.jctools.queues.unpadded.MpscUnpaddedArrayQueueConsumerIndexField", + "allDeclaredFields":true + }, + { + "name":"com.bloxbean.cardano.yacicli.localcluster.config.GenesisConfig", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true, + "allPublicConstructors":true, + "allPublicMethods":true, + "allPublicFields":true + }, + { + "name":"com.bloxbean.cardano.yacicli.localcluster.config.GenesisConfig$Pool", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true, + "allPublicConstructors":true, + "allPublicMethods":true, + "allPublicFields":true + }, + { + "name":"com.bloxbean.cardano.yacicli.localcluster.config.GenesisConfig$Delegator", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true, + "allPublicConstructors":true, + "allPublicMethods":true, + "allPublicFields":true + }, + { + "name":"com.bloxbean.cardano.yacicli.localcluster.config.GenesisConfig$MapItem", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true, + "allPublicConstructors":true, + "allPublicMethods":true, + "allPublicFields":true + }, + { + "name":"com.bloxbean.cardano.yacicli.localcluster.config.GenesisConfig$HeavyDelegation", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true, + "allPublicConstructors":true, + "allPublicMethods":true, + "allPublicFields":true + }, + { + "name":"com.bloxbean.cardano.yacicli.localcluster.config.GenesisConfig$GenesisDeleg", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true, + "allPublicConstructors":true, + "allPublicMethods":true, + "allPublicFields":true + }, + { + "name":"com.bloxbean.cardano.yacicli.localcluster.config.GenesisConfig$NonAvvmBalances", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true, + "allPublicConstructors":true, + "allPublicMethods":true, + "allPublicFields":true + }, + { + "name":"com.bloxbean.cardano.yacicli.localcluster.config.GenesisConfig$InitialAddress", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true, + "allPublicConstructors":true, + "allPublicMethods":true, + "allPublicFields":true + }, + { + "name":"com.bloxbean.cardano.yacicli.localcluster.config.GenesisConfig$CCMember", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true, + "allPublicConstructors":true, + "allPublicMethods":true, + "allPublicFields":true + }, { "name":"com.bloxbean.cardano.client.crypto.SecretKey", "allDeclaredFields":true, @@ -124,4 +217,3 @@ "name":"org.postgresql.util.PGobject" } ] - diff --git a/applications/cli/src/main/resources/logback-spring.xml b/applications/cli/src/main/resources/logback-spring.xml index c0baa8a7..74d12124 100644 --- a/applications/cli/src/main/resources/logback-spring.xml +++ b/applications/cli/src/main/resources/logback-spring.xml @@ -37,6 +37,9 @@ + + + diff --git a/config/env b/config/env index 93251162..5a3b53f6 100644 --- a/config/env +++ b/config/env @@ -1,5 +1,5 @@ yaci_store_enabled=true -ogmios_enabled=true +ogmios_enabled=false kupo_enabled=false node=node1 @@ -17,6 +17,8 @@ HOST_CLUSTER_API_PORT=10000 HOST_SUBMIT_API_PORT=8090 HOST_OGMIOS_PORT=1337 HOST_KUPO_PORT=1442 +HOST_YANO_HTTP_PORT=6060 +HOST_YANO_N2N_PORT=14447 ####################################################### # Viewer Config - DON'T CHANGE diff --git a/config/node.properties b/config/node.properties index ceb1b3c6..55a73d7f 100644 --- a/config/node.properties +++ b/config/node.properties @@ -1,3 +1,9 @@ +# Node mode: yano-primary (Yano BP + Haskell relay, supports rollback), +# companion (Yano bootstraps + Haskell takes over), +# yano-only (Yano only, fastest startup), +# haskell-only (legacy, Haskell node only) +nodeMode=companion + ################################################################################################################ ## Following genesis configurations can be changed for the devnet diff --git a/e2e-tests/evolution-sdk/devnet.ts b/e2e-tests/evolution-sdk/devnet.ts new file mode 100644 index 00000000..43d3c6c2 --- /dev/null +++ b/e2e-tests/evolution-sdk/devnet.ts @@ -0,0 +1,88 @@ +import * as Evolution from "@evolution-sdk/evolution"; + +const DEVKIT_ADMIN_URL = "http://localhost:10000/local-cluster/api/admin/devnet"; + +/** + * Build an Evolution SDK `Chain` descriptor by fetching the live shelley genesis + * from Yaci DevKit's admin API. This ensures `zeroTime`/`slotLength`/`networkMagic` + * always match the running devnet (which resets each time the cluster restarts). + * + * See: https://devkit.yaci.xyz/tutorials/lucid-evolution/overview + */ +export async function fetchDevnetChain(): Promise { + const shelley = await fetch(`${DEVKIT_ADMIN_URL}/genesis/shelley`).then((r) => r.json()); + + const zeroTime = BigInt(new Date(shelley.systemStart).getTime()); + const slotLength = shelley.slotLength * 1000; // seconds → milliseconds + + return { + id: 0, + name: "Yaci DevKit", + networkMagic: shelley.networkMagic, + epochLength: shelley.epochLength, + slotConfig: { + zeroTime, + zeroSlot: 0n, + slotLength, + }, + }; +} + +/** + * Install a global fetch shim that normalizes a few Yaci Store responses so + * they parse against Evolution SDK's stricter Blockfrost schemas. + * + * Known divergences from upstream Blockfrost handled here: + * - `/epochs/latest/parameters`: `drep_deposit` and `gov_action_deposit` come + * back as numbers; SDK expects strings. + * - `/addresses/{addr}/utxos`: the SDK requires `tx_index` and `block` on + * every UTxO row; Yaci Store emits `output_index`/`block_number` instead. + * + * Without these patches, `client.newTx().build()` and `client.getUtxos()` fail + * with schema parse errors. + */ +export function installYaciStoreFetchShim(): void { + const originalFetch = globalThis.fetch; + if ((originalFetch as any).__yaciShimInstalled) return; + + const shimmed: typeof fetch = async (input, init) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; + const response = await originalFetch(input, init); + if (!response.ok) return response; + + let body: any; + if (url.includes("/epochs/latest/parameters")) { + body = await response.json(); + for (const k of ["drep_deposit", "gov_action_deposit"] as const) { + if (typeof body[k] === "number") body[k] = String(body[k]); + } + } else if (/\/addresses\/[^/]+\/utxos/.test(url)) { + body = await response.json(); + if (Array.isArray(body)) { + for (const u of body) { + if (u.tx_index === undefined && typeof u.output_index === "number") { + u.tx_index = u.output_index; + } + if (typeof u.block !== "string") { + u.block = typeof u.block_hash === "string" + ? u.block_hash + : typeof u.block_number === "number" + ? String(u.block_number) + : ""; + } + } + } + } else { + return response; + } + + return new Response(JSON.stringify(body), { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }); + }; + + (shimmed as any).__yaciShimInstalled = true; + globalThis.fetch = shimmed; +} diff --git a/scripts/docker-compose.yml b/scripts/docker-compose.yml index 14d28d34..efc0f2ec 100644 --- a/scripts/docker-compose.yml +++ b/scripts/docker-compose.yml @@ -10,6 +10,8 @@ services: - "${HOST_CLUSTER_API_PORT}:10000" - "${HOST_OGMIOS_PORT}:1337" - "${HOST_KUPO_PORT}:1442" + - "${HOST_YANO_HTTP_PORT}:6060" + - "${HOST_YANO_N2N_PORT}:14447" volumes: - cluster-data:/clusters - ../config/node.properties:/app/config/node.properties From 683c6af1e6d70d1fa1ffa2cef1c9f35b866bc8b1 Mon Sep 17 00:00:00 2001 From: Satya Date: Sat, 16 May 2026 23:58:40 +0800 Subject: [PATCH 15/17] chore: evoluation sdk e2e test --- config/plutus-costmodels-v10.json | 646 +++++++++++++++++++++++++++ e2e-tests/evolution-sdk/README.md | 84 ++++ e2e-tests/evolution-sdk/package.json | 14 + e2e-tests/evolution-sdk/payment.ts | 30 ++ e2e-tests/evolution-sdk/plutus_v2.ts | 89 ++++ e2e-tests/evolution-sdk/plutus_v3.ts | 91 ++++ 6 files changed, 954 insertions(+) create mode 100644 config/plutus-costmodels-v10.json create mode 100644 e2e-tests/evolution-sdk/README.md create mode 100644 e2e-tests/evolution-sdk/package.json create mode 100644 e2e-tests/evolution-sdk/payment.ts create mode 100644 e2e-tests/evolution-sdk/plutus_v2.ts create mode 100644 e2e-tests/evolution-sdk/plutus_v3.ts diff --git a/config/plutus-costmodels-v10.json b/config/plutus-costmodels-v10.json new file mode 100644 index 00000000..f07d0a17 --- /dev/null +++ b/config/plutus-costmodels-v10.json @@ -0,0 +1,646 @@ +{ + "PlutusV1": [ + 100788, + 420, + 1, + 1, + 1000, + 173, + 0, + 1, + 1000, + 59957, + 4, + 1, + 11183, + 32, + 201305, + 8356, + 4, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 100, + 100, + 16000, + 100, + 94375, + 32, + 132994, + 32, + 61462, + 4, + 72010, + 178, + 0, + 1, + 22151, + 32, + 91189, + 769, + 4, + 2, + 85848, + 228465, + 122, + 0, + 1, + 1, + 1000, + 42921, + 4, + 2, + 24548, + 29498, + 38, + 1, + 898148, + 27279, + 1, + 51775, + 558, + 1, + 39184, + 1000, + 60594, + 1, + 141895, + 32, + 83150, + 32, + 15299, + 32, + 76049, + 1, + 13169, + 4, + 22100, + 10, + 28999, + 74, + 1, + 28999, + 74, + 1, + 43285, + 552, + 1, + 44749, + 541, + 1, + 33852, + 32, + 68246, + 32, + 72362, + 32, + 7243, + 32, + 7391, + 32, + 11546, + 32, + 85848, + 228465, + 122, + 0, + 1, + 1, + 90434, + 519, + 0, + 1, + 74433, + 32, + 85848, + 228465, + 122, + 0, + 1, + 1, + 85848, + 228465, + 122, + 0, + 1, + 1, + 270652, + 22588, + 4, + 1457325, + 64566, + 4, + 20467, + 1, + 4, + 0, + 141992, + 32, + 100788, + 420, + 1, + 1, + 81663, + 32, + 59498, + 32, + 20142, + 32, + 24588, + 32, + 20744, + 32, + 25933, + 32, + 24623, + 32, + 53384111, + 14333, + 10 + ], + "PlutusV2": [ + 100788, + 420, + 1, + 1, + 1000, + 173, + 0, + 1, + 1000, + 59957, + 4, + 1, + 11183, + 32, + 201305, + 8356, + 4, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 100, + 100, + 16000, + 100, + 94375, + 32, + 132994, + 32, + 61462, + 4, + 72010, + 178, + 0, + 1, + 22151, + 32, + 91189, + 769, + 4, + 2, + 85848, + 228465, + 122, + 0, + 1, + 1, + 1000, + 42921, + 4, + 2, + 24548, + 29498, + 38, + 1, + 898148, + 27279, + 1, + 51775, + 558, + 1, + 39184, + 1000, + 60594, + 1, + 141895, + 32, + 83150, + 32, + 15299, + 32, + 76049, + 1, + 13169, + 4, + 22100, + 10, + 28999, + 74, + 1, + 28999, + 74, + 1, + 43285, + 552, + 1, + 44749, + 541, + 1, + 33852, + 32, + 68246, + 32, + 72362, + 32, + 7243, + 32, + 7391, + 32, + 11546, + 32, + 85848, + 228465, + 122, + 0, + 1, + 1, + 90434, + 519, + 0, + 1, + 74433, + 32, + 85848, + 228465, + 122, + 0, + 1, + 1, + 85848, + 228465, + 122, + 0, + 1, + 1, + 955506, + 213312, + 0, + 2, + 270652, + 22588, + 4, + 1457325, + 64566, + 4, + 20467, + 1, + 4, + 0, + 141992, + 32, + 100788, + 420, + 1, + 1, + 81663, + 32, + 59498, + 32, + 20142, + 32, + 24588, + 32, + 20744, + 32, + 25933, + 32, + 24623, + 32, + 43053543, + 10, + 53384111, + 14333, + 10, + 43574283, + 26308, + 10 + ], + "PlutusV3": [ + 100788, + 420, + 1, + 1, + 1000, + 173, + 0, + 1, + 1000, + 59957, + 4, + 1, + 11183, + 32, + 201305, + 8356, + 4, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 16000, + 100, + 100, + 100, + 16000, + 100, + 94375, + 32, + 132994, + 32, + 61462, + 4, + 72010, + 178, + 0, + 1, + 22151, + 32, + 91189, + 769, + 4, + 2, + 85848, + 123203, + 7305, + -900, + 1716, + 549, + 57, + 85848, + 0, + 1, + 1, + 1000, + 42921, + 4, + 2, + 24548, + 29498, + 38, + 1, + 898148, + 27279, + 1, + 51775, + 558, + 1, + 39184, + 1000, + 60594, + 1, + 141895, + 32, + 83150, + 32, + 15299, + 32, + 76049, + 1, + 13169, + 4, + 22100, + 10, + 28999, + 74, + 1, + 28999, + 74, + 1, + 43285, + 552, + 1, + 44749, + 541, + 1, + 33852, + 32, + 68246, + 32, + 72362, + 32, + 7243, + 32, + 7391, + 32, + 11546, + 32, + 85848, + 123203, + 7305, + -900, + 1716, + 549, + 57, + 85848, + 0, + 1, + 90434, + 519, + 0, + 1, + 74433, + 32, + 85848, + 123203, + 7305, + -900, + 1716, + 549, + 57, + 85848, + 0, + 1, + 1, + 85848, + 123203, + 7305, + -900, + 1716, + 549, + 57, + 85848, + 0, + 1, + 955506, + 213312, + 0, + 2, + 270652, + 22588, + 4, + 1457325, + 64566, + 4, + 20467, + 1, + 4, + 0, + 141992, + 32, + 100788, + 420, + 1, + 1, + 81663, + 32, + 59498, + 32, + 20142, + 32, + 24588, + 32, + 20744, + 32, + 25933, + 32, + 24623, + 32, + 43053543, + 10, + 53384111, + 14333, + 10, + 43574283, + 26308, + 10, + 16000, + 100, + 16000, + 100, + 962335, + 18, + 2780678, + 6, + 442008, + 1, + 52538055, + 3756, + 18, + 267929, + 18, + 76433006, + 8868, + 18, + 52948122, + 18, + 1995836, + 36, + 3227919, + 12, + 901022, + 1, + 166917843, + 4307, + 36, + 284546, + 36, + 158221314, + 26549, + 36, + 74698472, + 36, + 333849714, + 1, + 254006273, + 72, + 2174038, + 72, + 2261318, + 64571, + 4, + 207616, + 8310, + 4, + 1293828, + 28716, + 63, + 0, + 1, + 1006041, + 43623, + 251, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 100181, + 726, + 719, + 0, + 1, + 107878, + 680, + 0, + 1, + 95336, + 1, + 281145, + 18848, + 0, + 1, + 180194, + 159, + 1, + 1, + 158519, + 8942, + 0, + 1, + 159378, + 8813, + 0, + 1, + 107490, + 3298, + 1, + 106057, + 655, + 1, + 1964219, + 24520, + 3 + ] +} diff --git a/e2e-tests/evolution-sdk/README.md b/e2e-tests/evolution-sdk/README.md new file mode 100644 index 00000000..049aaf69 --- /dev/null +++ b/e2e-tests/evolution-sdk/README.md @@ -0,0 +1,84 @@ +# Evolution SDK Compatibility Tests + +Examples that exercise Yaci DevKit's devnet with the +[Evolution SDK](https://www.npmjs.com/package/@evolution-sdk/evolution) v0.5.8. + +> **Note**: The Evolution SDK is a different package family from +> `@lucid-evolution/lucid` (used in the sibling `lucid-evo/` folder). Its API is +> namespaced under `import * as Evolution from "@evolution-sdk/evolution"` and +> follows an Effect-based, lower-level design — there is no `Lucid` facade. + +## Install Bun + +``` +curl -fsSL https://bun.sh/install | bash +``` + +## Install Dependencies + +```shell +bun install +``` + +## Run Tests + +```shell +bun payment.ts # ADA payment — works end-to-end +bun plutus_v2.ts # Plutus V2 always-succeeds — lock works, spend currently blocked (see below) +bun plutus_v3.ts # Plutus V3 always-succeeds — same as v2 +``` + +## How chain config is wired up + +The examples fetch the live shelley genesis from Yaci DevKit's admin endpoint +(`http://localhost:10000/local-cluster/api/admin/devnet/genesis/shelley`) and +build an `Evolution.Chain` descriptor from `systemStart`, `slotLength`, +`networkMagic`, and `epochLength`. See `devnet.ts`. The same approach is +documented for `@lucid-evolution/lucid` at +. + +## Yaci Store / Evolution SDK schema gaps + +`devnet.ts` installs a small `fetch` shim that normalizes two Yaci Store +Blockfrost-compatible responses so Evolution SDK's stricter schemas accept +them. Without the shim, every request fails with an Effect Schema `ParseError`. + +| Endpoint | Field | Yaci Store sends | Evolution SDK expects | +|---|---|---|---| +| `/epochs/latest/parameters` | `drep_deposit` | number (`500000000`) | string (`"500000000"`) | +| `/epochs/latest/parameters` | `gov_action_deposit` | number | string | +| `/addresses/{addr}/utxos` | `tx_index` | absent | required `Schema.Number` | +| `/addresses/{addr}/utxos` | `block` | absent (sends `block_number`) | required `Schema.String` | + +These are reasonable fixes to make upstream in Yaci Store — once those land, +the shim can be removed. + +## Plutus spend-phase blocker + +`payment.ts` and the lock phase of `plutus_v{2,3}.ts` complete successfully +against a default devnet. The spend phase fails at build time because +Evolution SDK calls `POST /utils/txs/evaluate/utxos` on the Blockfrost provider +to evaluate script execution units, and Yaci Store's implementation of that +endpoint returns `500 Internal Server Error` (empty body). For example: + +``` +$ curl -X POST -H 'Content-Type: application/json' \ + -d '{"cbor":"...","additionalUtxoSet":[]}' \ + http://localhost:8080/api/v1/utils/txs/evaluate/utxos +{"status_code":500,"error":"Internal Server Error","message":"Error evaluating transaction"} +``` + +Workarounds attempted: + +- `BuildOptions.evaluator: createAikenEvaluator` (from `@evolution-sdk/aiken-uplc`) + bypasses the remote endpoint, but currently fails on the Lucid-style + CBOR-wrapped always-succeeds script bytes with `cannot evaluate an open term: + Term i_2`. Likely a CBOR unwrap-depth mismatch between how the SDK stores + PlutusV2 bytes and how the Aiken evaluator decodes them — needs upstream + investigation in `@evolution-sdk/aiken-uplc`. +- Kupmios (`Ogmios` + `Kupo`) provider works in principle, but neither service + is started by `create-node` by default in this devnet. + +For now, `payment.ts` is the proven end-to-end path; the Plutus examples will +start running once either Yaci Store's `evaluateTx` endpoint is fixed or a +working local evaluator path is wired up. diff --git a/e2e-tests/evolution-sdk/package.json b/e2e-tests/evolution-sdk/package.json new file mode 100644 index 00000000..c513d48b --- /dev/null +++ b/e2e-tests/evolution-sdk/package.json @@ -0,0 +1,14 @@ +{ + "name": "evolution-sdk-example", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "@evolution-sdk/evolution": "^0.5.8" + } +} diff --git a/e2e-tests/evolution-sdk/payment.ts b/e2e-tests/evolution-sdk/payment.ts new file mode 100644 index 00000000..a8b42c29 --- /dev/null +++ b/e2e-tests/evolution-sdk/payment.ts @@ -0,0 +1,30 @@ +import * as Evolution from "@evolution-sdk/evolution"; +import { fetchDevnetChain, installYaciStoreFetchShim } from "./devnet"; + +installYaciStoreFetchShim(); +const devnet = await fetchDevnetChain(); + +const seedPhrase = + "test test test test test test test test test test test test test test test test test test test test test test test sauce"; + +const client = Evolution.Client.make(devnet) + .withBlockfrost({ baseUrl: "http://localhost:8080/api/v1", projectId: "Dummy Key" }) + .withSeed({ mnemonic: seedPhrase }); + +const address = await client.address(); +console.log(Evolution.Address.toBech32(address)); + +const receiver = Evolution.Address.fromBech32( + "addr_test1qqm87edtdxc7vu2u34dpf9jzzny4qhk3wqezv6ejpx3vgrwt46dz4zq7vqll88fkaxrm4nac0m5cq50jytzlu0hax5xqwlraql", +); + +const built = await client + .newTx() + .payToAddress({ + address: receiver, + assets: Evolution.Assets.fromLovelace(5_000_000n), + }) + .build(); + +const txHash = await built.signAndSubmit(); +console.log(Evolution.TransactionHash.toHex(txHash)); diff --git a/e2e-tests/evolution-sdk/plutus_v2.ts b/e2e-tests/evolution-sdk/plutus_v2.ts new file mode 100644 index 00000000..300b4b60 --- /dev/null +++ b/e2e-tests/evolution-sdk/plutus_v2.ts @@ -0,0 +1,89 @@ +import * as Evolution from "@evolution-sdk/evolution"; +import { fetchDevnetChain, installYaciStoreFetchShim } from "./devnet"; + +installYaciStoreFetchShim(); +const devnet = await fetchDevnetChain(); + +const seedPhrase = + "test test test test test test test test test test test test test test test test test test test test test test test sauce"; + +const client = Evolution.Client.make(devnet) + .withBlockfrost({ baseUrl: "http://localhost:8080/api/v1", projectId: "Dummy Key" }) + .withSeed({ mnemonic: seedPhrase }); + +const ownerAddress = await client.address(); +const ownerBech32 = Evolution.Address.toBech32(ownerAddress); +console.log(ownerBech32); + +const ownerDetails = Evolution.Address.getAddressDetails(ownerBech32); +if (!ownerDetails || ownerDetails.paymentCredential._tag !== "KeyHash") { + throw new Error("Owner address must have a key-hash payment credential"); +} +const ownerKeyHash = ownerDetails.paymentCredential; +const publicKeyHash = Evolution.KeyHash.toHex(ownerKeyHash); + +// Always-succeeds Plutus V2 script. +const spendScript = new Evolution.PlutusV2.PlutusV2({ + bytes: new Uint8Array(Buffer.from("49480100002221200101", "hex")), +}); + +const scriptHash = Evolution.ScriptHash.fromScript(spendScript); +const scriptAddress = new Evolution.Address.Address({ + networkId: devnet.id, + paymentCredential: scriptHash, +}); +console.log(Evolution.Address.toBech32(scriptAddress)); + +// Datum: Constr 0 [publicKeyHash] +const datumData = Evolution.Data.constr(0n, [ + new Uint8Array(Buffer.from(publicKeyHash, "hex")), +]); +const datum = new Evolution.InlineDatum.InlineDatum({ data: datumData }); +console.log(Evolution.Data.toCBORHex(datumData)); + +const lockTx = await client + .newTx() + .payToAddress({ + address: scriptAddress, + assets: Evolution.Assets.fromLovelace(10_000_000n), + datum, + }) + .build(); + +const lockHash = await lockTx.signAndSubmit(); +console.log(Evolution.TransactionHash.toHex(lockHash)); + +// Wait briefly for the indexer to see the new UTxO. +await new Promise((resolve) => setTimeout(resolve, 3000)); + +console.log("Spend script UTxO ----"); + +const scriptUtxos = await client.getUtxos(scriptAddress); +const ownerUtxo = scriptUtxos.find((utxo) => { + if (utxo.datumOption && utxo.datumOption._tag === "InlineDatum") { + const data = utxo.datumOption.data; + if (Evolution.Data.isConstr(data) && data.index === 0n) { + const owner = data.fields[0]; + if (owner instanceof Uint8Array) { + return Buffer.from(owner).toString("hex") === publicKeyHash; + } + } + } + return false; +}); + +if (!ownerUtxo) throw new Error("Could not find locked UTxO at script address"); + +const redeemerData = Evolution.Data.constr(0n, [ + new Uint8Array(Buffer.from("Hello, World!", "utf8")), +]); + +const spendTx = await client + .newTx() + .collectFrom({ inputs: [ownerUtxo], redeemer: redeemerData }) + .attachScript({ script: spendScript }) + .addSigner({ keyHash: ownerKeyHash }) + .build(); + +const spendHash = await spendTx.signAndSubmit(); +console.log(Evolution.TransactionHash.toHex(spendHash)); diff --git a/e2e-tests/evolution-sdk/plutus_v3.ts b/e2e-tests/evolution-sdk/plutus_v3.ts new file mode 100644 index 00000000..784a7c1c --- /dev/null +++ b/e2e-tests/evolution-sdk/plutus_v3.ts @@ -0,0 +1,91 @@ +import * as Evolution from "@evolution-sdk/evolution"; +import { fetchDevnetChain, installYaciStoreFetchShim } from "./devnet"; + +installYaciStoreFetchShim(); +const devnet = await fetchDevnetChain(); + +const seedPhrase = + "test test test test test test test test test test test test test test test test test test test test test test test sauce"; + +const client = Evolution.Client.make(devnet) + .withBlockfrost({ baseUrl: "http://localhost:8080/api/v1", projectId: "Dummy Key" }) + .withSeed({ mnemonic: seedPhrase }); + +const ownerAddress = await client.address(); +const ownerBech32 = Evolution.Address.toBech32(ownerAddress); +console.log(ownerBech32); + +const ownerDetails = Evolution.Address.getAddressDetails(ownerBech32); +if (!ownerDetails || ownerDetails.paymentCredential._tag !== "KeyHash") { + throw new Error("Owner address must have a key-hash payment credential"); +} +const ownerKeyHash = ownerDetails.paymentCredential; +const publicKeyHash = Evolution.KeyHash.toHex(ownerKeyHash); + +// Compiled Plutus V3 spending validator from the Lucid Evolution example. +const spendScript = new Evolution.PlutusV3.PlutusV3({ + bytes: new Uint8Array( + Buffer.from( + "5857010000323232323225333002323232323253330073370e900118041baa00113232324a26018601a004601600260126ea800458c024c028008c020004c020008c018004c010dd50008a4c26cacae6955ceaab9e5742ae89", + "hex", + ), + ), +}); + +const scriptHash = Evolution.ScriptHash.fromScript(spendScript); +const scriptAddress = new Evolution.Address.Address({ + networkId: devnet.id, + paymentCredential: scriptHash, +}); +console.log(Evolution.Address.toBech32(scriptAddress)); + +const datumData = Evolution.Data.constr(0n, [ + new Uint8Array(Buffer.from(publicKeyHash, "hex")), +]); +const datum = new Evolution.InlineDatum.InlineDatum({ data: datumData }); + +const lockTx = await client + .newTx() + .payToAddress({ + address: scriptAddress, + assets: Evolution.Assets.fromLovelace(10_000_000n), + datum, + }) + .build(); + +const lockHash = await lockTx.signAndSubmit(); +console.log(Evolution.TransactionHash.toHex(lockHash)); + +await new Promise((resolve) => setTimeout(resolve, 3000)); + +console.log("Spend script UTxO ----"); + +const scriptUtxos = await client.getUtxos(scriptAddress); +const ownerUtxo = scriptUtxos.find((utxo) => { + if (utxo.datumOption && utxo.datumOption._tag === "InlineDatum") { + const data = utxo.datumOption.data; + if (Evolution.Data.isConstr(data) && data.index === 0n) { + const owner = data.fields[0]; + if (owner instanceof Uint8Array) { + return Buffer.from(owner).toString("hex") === publicKeyHash; + } + } + } + return false; +}); + +if (!ownerUtxo) throw new Error("Could not find locked UTxO at script address"); + +const redeemerData = Evolution.Data.constr(0n, [ + new Uint8Array(Buffer.from("Hello, World!", "utf8")), +]); + +const spendTx = await client + .newTx() + .collectFrom({ inputs: [ownerUtxo], redeemer: redeemerData }) + .attachScript({ script: spendScript }) + .addSigner({ keyHash: ownerKeyHash }) + .build(); + +const spendHash = await spendTx.signAndSubmit(); +console.log(Evolution.TransactionHash.toHex(spendHash)); From d9c5c9421a0570349e000a836c8d442c796b708c Mon Sep 17 00:00:00 2001 From: Satya Date: Sun, 17 May 2026 00:00:28 +0800 Subject: [PATCH 16/17] chore: yano build related changes --- Earthfile | 5 +++- applications/yano-build/Earthfile | 38 +++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 applications/yano-build/Earthfile diff --git a/Earthfile b/Earthfile index c05524d7..16086b72 100644 --- a/Earthfile +++ b/Earthfile @@ -5,6 +5,7 @@ ARG --global tag="dev" ARG --global local="true" ARG --global REGISTRY_ORG = "bloxbean" ARG --global build_type="native" +ARG --global YANO_BRANCH="fix/devkit_fix" build: LOCALLY @@ -22,7 +23,7 @@ cli-docker: ARG EARTHLY_TARGET_NAME ARG EARTHLY_GIT_SHORT_HASH - BUILD ./applications/cli+docker-build --BUILD_TYPE=${build_type} --REGISTRY_ORG=${REGISTRY_ORG} --APP_VERSION=${tag} --COMMIT_ID=${EARTHLY_GIT_SHORT_HASH} + BUILD ./applications/cli+docker-build --BUILD_TYPE=${build_type} --REGISTRY_ORG=${REGISTRY_ORG} --APP_VERSION=${tag} --COMMIT_ID=${EARTHLY_GIT_SHORT_HASH} --YANO_BRANCH=${YANO_BRANCH} viewer: ARG EARTHLY_TARGET_NAME @@ -44,6 +45,8 @@ zip: RUN echo "revision=${EARTHLY_GIT_SHORT_HASH}" >> /app/yaci-devkit-${tag}/config/version COPY config/env /app/yaci-devkit-${tag}/config/ COPY config/node.properties /app/yaci-devkit-${tag}/config/ + COPY config/plutus-costmodels-v10.json /app/yaci-devkit-${tag}/config/ + COPY config/plutus-costmodels-v11.json /app/yaci-devkit-${tag}/config/ COPY bin/devkit.sh /app/yaci-devkit-${tag}/bin/ diff --git a/applications/yano-build/Earthfile b/applications/yano-build/Earthfile new file mode 100644 index 00000000..2c7ed574 --- /dev/null +++ b/applications/yano-build/Earthfile @@ -0,0 +1,38 @@ +VERSION 0.8 + +yano-native: + LOCALLY + ARG YANO_BRANCH + ARG APP_VERSION + + WORKDIR . + RUN rm -rf tmp + RUN mkdir tmp + WORKDIR tmp + RUN git clone --depth 1 --branch ${YANO_BRANCH} https://github.com/bloxbean/yano.git yano + + RUN git --git-dir yano/.git rev-parse --short HEAD > yano/yano.git.properties + + FROM ghcr.io/graalvm/graalvm-community:25 + + COPY tmp/yano yano + + WORKDIR yano + + # Build native binary with Quarkus + GraalVM Community 25. + # The --initialize-at-run-time flags defer aiken-java-binding's JNA classes: + # the linux-aarch64 .so isn't published, and even on linux-amd64 we don't want + # build-time class init to load a native lib. Default tx evaluator is Scalus + # so the deferred classes are never touched at runtime in normal use. + RUN --mount=type=cache,target=/root/.gradle ./gradlew --no-daemon -i \ + -Pversion=${APP_VERSION} \ + :app:quarkusBuild \ + -Dquarkus.native.enabled=true \ + -Dquarkus.package.jar.enabled=false \ + -PskipSigning=true \ + -Dquarkus.native.additional-build-args="--initialize-at-run-time=com.bloxbean.cardano.aiken,--initialize-at-run-time=com.bloxbean.cardano.yano.ledgerrules.impl.AikenTxEvaluator" + + # Save artifacts + SAVE ARTIFACT app/build/yano yano + SAVE ARTIFACT app/config config + SAVE ARTIFACT yano.git.properties yano.git.properties From 4fbac93e7f831cae641874eb388b2b1044517273 Mon Sep 17 00:00:00 2001 From: Satya Date: Sun, 17 May 2026 00:24:18 +0800 Subject: [PATCH 17/17] Get node arm version from official distribution --- applications/cli/Earthfile | 19 +++++++++++++++---- applications/cli/docker/download-arm64.sh | 23 +++++++++-------------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/applications/cli/Earthfile b/applications/cli/Earthfile index 6efa85f1..c75b7e05 100644 --- a/applications/cli/Earthfile +++ b/applications/cli/Earthfile @@ -60,8 +60,9 @@ java-setup: docker-build: FROM ubuntu:22.04 ENV JAVA_HOME=/opt/java/openjdk - ENV STORE_VERSION=2.0.0 - ENV STORE_NATIVE_BRANCH=release/2.0.x + ENV STORE_VERSION=2.0.1-rc1 + ENV STORE_NATIVE_BRANCH=v2.0.1-rc1 + ENV YANO_BRANCH=fix/devkit_fix ARG TARGETOS ARG TARGETARCH @@ -69,6 +70,7 @@ docker-build: ARG APP_VERSION ARG REGISTRY_ORG ARG COMMIT_ID + ARG YANO_BRANCH COPY --dir +java-setup/openjdk /opt/java/ ENV PATH="${JAVA_HOME}/bin:${PATH}" @@ -115,6 +117,13 @@ docker-build: COPY (../store-build/+store-native/yaci-store* --APP_VERSION=${APP_VERSION} --STORE_BRANCH=${STORE_NATIVE_BRANCH}) /app/store/ END + # Yano native binary (always included; Yano is native-only in docker) + RUN mkdir -p /app/yano + COPY (../yano-build/+yano-native/yano --APP_VERSION=${APP_VERSION} --YANO_BRANCH=${YANO_BRANCH}) /app/yano/yano + COPY (../yano-build/+yano-native/config --APP_VERSION=${APP_VERSION} --YANO_BRANCH=${YANO_BRANCH}) /app/yano/config + COPY (../yano-build/+yano-native/yano.git.properties --APP_VERSION=${APP_VERSION} --YANO_BRANCH=${YANO_BRANCH}) /app/yano/yano.git.properties + RUN chmod +x /app/yano/yano + RUN echo "Build type: $BUILD_TYPE" IF [ "$BUILD_TYPE" = "native" ] COPY (+cli-native/yaci-cli* --APP_VERSION=${APP_VERSION}) /app/ @@ -129,8 +138,8 @@ docker-build: RUN mkdir -p /app/config COPY docker/application.properties /app/config/ - COPY docker/plutus-costmodels-v10.json /app/config/ - COPY docker/plutus-costmodels-v11.json /app/config/ + COPY config/plutus-costmodels-v10.json /app/config/ + COPY config/plutus-costmodels-v11.json /app/config/ ENV PATH="$PATH:/app/cardano-bin" ENV CARDANO_NODE_SOCKET_PATH=/clusters/nodes/default/node/node.sock @@ -143,6 +152,8 @@ docker-build: EXPOSE 8080 EXPOSE 1337 EXPOSE 1442 + EXPOSE 6060 + EXPOSE 14447 ENTRYPOINT ["sh", "/app/yaci-cli.sh"] diff --git a/applications/cli/docker/download-arm64.sh b/applications/cli/docker/download-arm64.sh index 1be0adb5..81020997 100755 --- a/applications/cli/docker/download-arm64.sh +++ b/applications/cli/docker/download-arm64.sh @@ -1,16 +1,11 @@ -file=cardano-11_0_1-aarch64-static-musl-ghc_9122.tar.zst -dir=cardano-11_0_1-aarch64-static-musl-ghc_9122 -wget https://github.com/armada-alliance/cardano-node-binaries/raw/main/static-binaries/$file?raw=true -O - | tar -I zstd -xv +file=cardano-node-11.0.1-linux-arm64.tar.gz +wget https://github.com/IntersectMBO/cardano-node/releases/download/11.0.1/cardano-node-11.0.1-linux-arm64.tar.gz -#unzip $file -mv $dir cardano-node -mv cardano-node /app/cardano-bin/ -#rm $file +mkdir /app/cardano-bin -#submit_api_file=cardano-submit-api-3_2_2.zip -#wget https://github.com/armada-alliance/cardano-node-binaries/raw/main/static-binaries/cardano-submit-api/$submit_api_file -# -#unzip $submit_api_file -#mv cardano-submit-api /app/cardano-bin/ -# -#rm $submit_api_file +tar zxvf $file -C /app/cardano-bin + +mv /app/cardano-bin/bin/* /app/cardano-bin/ +rm -rf /app/cardano-bin/bin + +rm $file