diff --git a/.gitignore b/.gitignore index 03a4fb14..6a007e21 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ 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/ +/e2e-tests/evolution-sdk/node_modules/ +/chainstate/ 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/README.md b/README.md index 1395002c..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/.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..c75b7e05 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,9 @@ 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.1-rc1 + ENV STORE_NATIVE_BRANCH=v2.0.1-rc1 + ENV YANO_BRANCH=fix/devkit_fix ARG TARGETOS ARG TARGETARCH @@ -58,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}" @@ -94,7 +107,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 @@ -102,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/ @@ -116,6 +138,8 @@ docker-build: RUN mkdir -p /app/config COPY docker/application.properties /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 @@ -128,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/build.gradle b/applications/cli/build.gradle index 511a74e4..a151c284 100644 --- a/applications/cli/build.gradle +++ b/applications/cli/build.gradle @@ -34,16 +34,16 @@ 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' - 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' @@ -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..3c7dffb6 100644 --- a/applications/cli/config/application.properties +++ b/applications/cli/config/application.properties @@ -7,9 +7,13 @@ 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 +yano.enabled=false yaci.store.mode=native @@ -21,6 +25,8 @@ bp.create.enabled=true #yaci.store.port=8080 #socat.port=3333 #prometheus.port=12798 +#yano.server.port=14447 +#yano.http.port=6060 ###################################################### diff --git a/applications/cli/config/download.properties b/applications/cli/config/download.properties index 63830bd9..01c6ea1d 100644 --- a/applications/cli/config/download.properties +++ b/applications/cli/config/download.properties @@ -1,14 +1,18 @@ #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=11.0.1 +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.1-rc1 +yaci.store.version=2.0.1-rc1 +yaci.store.jar.version=2.0.1-rc1 + +yano.tag=v0.1.0-pre3 +yano.version=0.1.0-pre3 #node.url= #ogmios.url= #kupo.url= #yaci.store.url= #yaci.store.jar.url= +#yano.url= diff --git a/applications/cli/config/node.properties b/applications/cli/config/node.properties index d5a3d7c4..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 @@ -66,6 +72,8 @@ #dvtTreasuryWithdrawal=0.51f #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..a3d556d1 --- /dev/null +++ b/applications/cli/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 + ] +} diff --git a/applications/cli/docker/application.properties b/applications/cli/docker/application.properties index 9e80581f..5319999a 100644 --- a/applications/cli/docker/application.properties +++ b/applications/cli/docker/application.properties @@ -2,10 +2,11 @@ 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 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..1ca0ce47 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-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 0d81adef..81020997 100755 --- a/applications/cli/docker/download-arm64.sh +++ b/applications/cli/docker/download-arm64.sh @@ -1,16 +1,11 @@ -file=cardano-10_5_0-aarch64-static-musl-ghc_9101.tar.zst -dir=cardano-10_5_0-aarch64-static-musl-ghc_9101 -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 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/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/commands/common/DownloadService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/commands/common/DownloadService.java index 1c13f7ad..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; @@ -483,10 +567,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; } @@ -608,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/ClusterCommands.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterCommands.java index 63ce2613..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; @@ -168,6 +173,7 @@ else if (era.equalsIgnoreCase("conway")) .yaciStorePort(yaciStorePort) .socatPort(socatPort) .prometheusPort(prometheusPort) + .nodeMode(genesisConfig.getNodeMode()) .localMultiNodeEnabled(enableMultiNode) .localMultiNodeStakeRatioFactor(stakeRatioFactor) .build(); @@ -234,6 +240,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/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 8f1cc14c..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 @@ -43,6 +43,14 @@ public class ClusterInfo { @Builder.Default private int prometheusPort=12798; + private int protocolMajorVer; + private NodeMode nodeMode; + + @Builder.Default + private int yanoServerPort = 14447; + @Builder.Default + private int yanoHttpPort = 6060; + 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..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,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()))); + 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/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/ClusterStartService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterStartService.java index ef9763fc..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 @@ -8,8 +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; @@ -47,6 +52,10 @@ public class ClusterStartService { private final GenesisConfig genesisConfig; 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<>(); @@ -69,11 +78,88 @@ public RunStatus startCluster(ClusterInfo clusterInfo, Path clusterFolder, Consu try { boolean firstRun = checkIfFirstRun(clusterFolder); + boolean companionMode = NodeMode.COMPANION == clusterInfo.getNodeMode(); + 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); - if (clusterInfo.isLocalMultiNodeEnabled()) { - String clusterName = CommandContext.INSTANCE.getProperty(ClusterConfig.CLUSTER_NAME); + // 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: 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() && !companionBootstrapDone) { if (firstRun) { localPeerService.handleFirstRun(new FirstRunDone(clusterName)); } @@ -81,11 +167,49 @@ 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); } + processes.add(nodeProcess); + + // 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); + + 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.")); + } + } Process submitApiProcess = startSubmitApi(clusterInfo, clusterFolder, writer); if (submitApiProcess == null) { @@ -93,10 +217,12 @@ public RunStatus startCluster(ClusterInfo clusterInfo, Path clusterFolder, Consu return new RunStatus(false, firstRun); } - processes.add(nodeProcess); - if (submitApiProcess != null) - processes.add(submitApiProcess); + 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 + // and governance enactment happens faster. return new RunStatus(true, firstRun); } catch (IOException e) { throw new RuntimeException(e); @@ -204,10 +330,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 +455,81 @@ 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); + } + + //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"); @@ -339,6 +547,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/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/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 4d20b6fd..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 @@ -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; @@ -42,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; @@ -108,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; @@ -145,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; @@ -247,6 +248,16 @@ public class GenesisConfig { ); + //Dijkstra + private long maxRefScriptSizePerBlock = 1048576; + private long maxRefScriptSizePerTx = 204800; + private long refScriptCostStride = 25600; + private double refScriptCostMultiplier = 1.2; + + // 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 NodeMode nodeMode = NodeMode.HASKELL_ONLY; + //Introduced for the issue https://github.com/bloxbean/yaci-devkit/issues/65 private int conwayHardForkAtEpoch = 0; private boolean shiftStartTimeBehind = false; @@ -437,6 +448,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; } @@ -526,9 +543,16 @@ public GenesisConfig copy() { genesisConfig.setGenesisDelegs(new ArrayList<>(genesisDelegs)); genesisConfig.setNonAvvmBalances(new ArrayList<>(nonAvvmBalances)); + genesisConfig.setNodeMode(nodeMode); genesisConfig.setConwayHardForkAtEpoch(conwayHardForkAtEpoch); genesisConfig.setShiftStartTimeBehind(shiftStartTimeBehind); + //Dijkstra + genesisConfig.setMaxRefScriptSizePerBlock(maxRefScriptSizePerBlock); + genesisConfig.setMaxRefScriptSizePerTx(maxRefScriptSizePerTx); + genesisConfig.setRefScriptCostStride(refScriptCostStride); + genesisConfig.setRefScriptCostMultiplier(refScriptCostMultiplier); + return genesisConfig; } @@ -635,10 +659,22 @@ 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 = 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()) 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/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..25d55217 --- /dev/null +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/events/listeners/FirstRunPlutusV3CostModelUpdate.java @@ -0,0 +1,76 @@ +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.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; +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; +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; + private final YanoGovernanceService yanoGovernanceService; + + @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; + } + + NodeMode nodeMode = clusterInfo != null ? clusterInfo.getNodeMode() : null; + 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) { + 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/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/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/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/service/AccountService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/AccountService.java index 6a340d47..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,13 +5,17 @@ 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.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,14 +25,45 @@ 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 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); @@ -53,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); @@ -74,7 +113,35 @@ 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) { + if (isYanoOnlyMode(clusterName)) { + return yanoHttpNodeService.getFundsAtGenesisKeys(clusterName); + } + Level orgLevel = rootLogService.getLogLevel(); if (!rootLogService.isDebugLevel()) rootLogService.setLogLevel(Level.OFF); @@ -99,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/LocalNodeService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/service/LocalNodeService.java index cac8b407..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 @@ -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; @@ -36,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; @@ -95,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() { @@ -264,6 +273,195 @@ 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 { + return GenesisUtil.loadCostModels(costModelsFile); + } + 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/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..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 @@ -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; @@ -11,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; @@ -25,20 +25,33 @@ 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(); + 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("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"); @@ -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,31 @@ 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(); + writer.write("store.submit.tx-evaluator-mode=" + txEvaluatorMode); + 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..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 @@ -1,26 +1,38 @@ 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; 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; 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; 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.core.annotation.Order; +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; @@ -39,14 +51,19 @@ @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; private final ClusterConfig clusterConfig; + private final ClusterUtilService clusterUtilService; private final JreResolver jreResolver; private final YaciStoreConfigBuilder yaciStoreConfigBuilder; private final YaciStoreCustomDbHelper customDBHelper; private final ProcessUtil processUtil; + private final OgmiosService ogmiosService; private List processes = new ArrayList<>(); @@ -56,6 +73,7 @@ public class YaciStoreService { private Queue logs = EvictingQueue.create(300); @EventListener + @Order(1) public void handleClusterStarted(ClusterStarted clusterStarted) { String clusterName = clusterStarted.getClusterName(); @@ -87,9 +105,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, 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) + 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); @@ -100,6 +126,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) { @@ -112,29 +142,128 @@ 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, Consumer writer) throws IOException, InterruptedException, ExecutionException, TimeoutException { ProcessBuilder builder = new ProcessBuilder(); builder.directory(new File(clusterConfig.getYaciStoreBinPath())); - if (yaciStoreMode == null || yaciStoreMode.equals("java")) { + 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 + // 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; + return new StoreStartResult(null, false); } - } 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())); - return null; + return new StoreStartResult(null, false); } } if (!appConfig.isDocker()) { - yaciStoreConfigBuilder.build(clusterInfo); + yaciStoreConfigBuilder.build(clusterInfo, txEvaluatorMode); } - 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 +274,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)); } @@ -199,7 +335,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. " + @@ -219,7 +355,18 @@ private Process startStoreApp(ClusterInfo clusterInfo, Era era) throws IOExcepti processUtil.createProcessId(STORE_PROCESS_NAME, process); - return process; + 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 { 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..d75993ca --- /dev/null +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoBootstrapService.java @@ -0,0 +1,292 @@ +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 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 { + 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 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 { + 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..a3a4ba15 --- /dev/null +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoCompanionService.java @@ -0,0 +1,328 @@ +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; +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.beans.factory.ObjectProvider; +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.time.Duration; +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 static final String TOPOLOGY_BEFORE_YANO = "topology-before-yano.json"; + + 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). + private final ObjectProvider clusterUtilServiceProvider; + 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 { + syncYanoGenesisToHaskellNode(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 until Yano has an actual tip past the bootstrap boundary. + // In multi-node mode Yano skips slots where node-1 is not leader, so + // catch-up can scan to wall-clock while the produced tip is still sparse. + if (clusterInfo.isLocalMultiNodeEnabled()) { + if (!yanoBootstrapService.waitForTipEpoch(httpPort, clusterInfo.getEpochLength(), + BOOTSTRAP_EPOCH_SHIFT, Duration.ofSeconds(60), writer)) { + writer.accept(error("Yano did not produce a bootstrap-boundary block. Falling back to haskell-only mode.")); + yanoService.stop(); + return false; + } + } else { + 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 { + updateTopologyForYanoPeering(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. + */ + 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"); + + // 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. + */ + 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_BEFORE_YANO); + + 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 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...")); + 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.")); + } + } + + 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_BEFORE_YANO); + + 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..5fb3491f --- /dev/null +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoConfigBuilder.java @@ -0,0 +1,94 @@ +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("yano.remote.protocol-magic", String.valueOf(clusterInfo.getProtocolMagic())); + props.put("yano.server.port", String.valueOf(clusterInfo.getYanoServerPort())); + + // Genesis files + 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("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("yano.storage.path", yanoDataDir.toAbsolutePath().toString()); + + // 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 + 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..67e7b85d --- /dev/null +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoGovernanceService.java @@ -0,0 +1,321 @@ +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.time.Duration; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +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(); + private final Set successfulGovernanceClusters = ConcurrentHashMap.newKeySet(); + + 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) { + 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()); + 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.")); + successfulGovernanceClusters.add(clusterName); + 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; + } + } + + 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. + */ + 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..12898d7e --- /dev/null +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yano/YanoService.java @@ -0,0 +1,395 @@ +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.ClusterDeleted; +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("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("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("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()); + + 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(); + } + + @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()) + 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/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/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/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}} +} 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/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/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 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/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/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 + ] +} 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/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/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/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)); diff --git a/scripts/docker-compose.yml b/scripts/docker-compose.yml index f63b7f99..efc0f2ec 100644 --- a/scripts/docker-compose.yml +++ b/scripts/docker-compose.yml @@ -10,9 +10,13 @@ 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 + - ../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 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