From dd68283c215771ffc2eac2f0efc6c062204b43a1 Mon Sep 17 00:00:00 2001 From: Emmanuel Jacquier Date: Fri, 27 Mar 2026 16:16:28 -0400 Subject: [PATCH] New template: Verfiable builds for ts --- starter-templates/README.md | 5 +- starter-templates/verifiable-build/README.md | 15 +++ .../verifiable-build-ts/.gitattributes | 3 + .../verifiable-build-ts/README.md | 100 ++++++++++++++++++ .../verifiable-build-ts/project.yaml | 44 ++++++++ .../verifiable-build-ts/secrets.yaml | 1 + .../workflow/.dockerignore | 3 + .../verifiable-build-ts/workflow/.gitignore | 2 + .../verifiable-build-ts/workflow/Dockerfile | 19 ++++ .../workflow/Dockerfile.lock | 11 ++ .../verifiable-build-ts/workflow/Makefile | 24 +++++ .../workflow/config.production.json | 3 + .../workflow/config.staging.json | 3 + .../verifiable-build-ts/workflow/main.test.ts | 42 ++++++++ .../verifiable-build-ts/workflow/main.ts | 28 +++++ .../verifiable-build-ts/workflow/package.json | 16 +++ .../workflow/tsconfig.json | 16 +++ .../workflow/workflow.yaml | 14 +++ 18 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 starter-templates/verifiable-build/README.md create mode 100644 starter-templates/verifiable-build/verifiable-build-ts/.gitattributes create mode 100644 starter-templates/verifiable-build/verifiable-build-ts/README.md create mode 100644 starter-templates/verifiable-build/verifiable-build-ts/project.yaml create mode 100644 starter-templates/verifiable-build/verifiable-build-ts/secrets.yaml create mode 100644 starter-templates/verifiable-build/verifiable-build-ts/workflow/.dockerignore create mode 100644 starter-templates/verifiable-build/verifiable-build-ts/workflow/.gitignore create mode 100644 starter-templates/verifiable-build/verifiable-build-ts/workflow/Dockerfile create mode 100644 starter-templates/verifiable-build/verifiable-build-ts/workflow/Dockerfile.lock create mode 100644 starter-templates/verifiable-build/verifiable-build-ts/workflow/Makefile create mode 100644 starter-templates/verifiable-build/verifiable-build-ts/workflow/config.production.json create mode 100644 starter-templates/verifiable-build/verifiable-build-ts/workflow/config.staging.json create mode 100644 starter-templates/verifiable-build/verifiable-build-ts/workflow/main.test.ts create mode 100644 starter-templates/verifiable-build/verifiable-build-ts/workflow/main.ts create mode 100644 starter-templates/verifiable-build/verifiable-build-ts/workflow/package.json create mode 100644 starter-templates/verifiable-build/verifiable-build-ts/workflow/tsconfig.json create mode 100644 starter-templates/verifiable-build/verifiable-build-ts/workflow/workflow.yaml diff --git a/starter-templates/README.md b/starter-templates/README.md index b5683c69..172107f1 100644 --- a/starter-templates/README.md +++ b/starter-templates/README.md @@ -60,7 +60,10 @@ They are more comprehensive than **building-blocks**, and can be adapted into yo 8. **Prediction Market** — [`./prediction-market`](./prediction-market) Full prediction market lifecycle example with 3 workflows: market creation, resolution using Chainlink BTC/USD Data Feed, and dispute management via LogTrigger. - + +9. **Verifiable Build** — [`./verifiable-build`](./verifiable-build) + Reproducible Docker-based builds for TypeScript workflows, enabling third-party verification that deployed workflows match their source code. + > Each subdirectory includes its own README with template-specific steps and example logs. ## License diff --git a/starter-templates/verifiable-build/README.md b/starter-templates/verifiable-build/README.md new file mode 100644 index 00000000..737bfd54 --- /dev/null +++ b/starter-templates/verifiable-build/README.md @@ -0,0 +1,15 @@ +# Verifiable Build + +Reproducible Docker-based builds for CRE TypeScript workflows, enabling third-party verification of deployed workflows. + +Go workflows are verifiable by default and do not require this template. + +## Available Languages + +| Language | Directory | +|----------|-----------| +| TypeScript | [verifiable-build-ts](./verifiable-build-ts) | + +## Learn More + +- [Verifying Workflows](https://docs.chain.link/cre/guides/operations/verifying-workflows) diff --git a/starter-templates/verifiable-build/verifiable-build-ts/.gitattributes b/starter-templates/verifiable-build/verifiable-build-ts/.gitattributes new file mode 100644 index 00000000..bdf3f198 --- /dev/null +++ b/starter-templates/verifiable-build/verifiable-build-ts/.gitattributes @@ -0,0 +1,3 @@ +# Enforce LF line endings for all text files to ensure reproducible builds. +# CRLF on Windows would change config file hashes and break verification. +* text=auto eol=lf diff --git a/starter-templates/verifiable-build/verifiable-build-ts/README.md b/starter-templates/verifiable-build/verifiable-build-ts/README.md new file mode 100644 index 00000000..4ffc4ace --- /dev/null +++ b/starter-templates/verifiable-build/verifiable-build-ts/README.md @@ -0,0 +1,100 @@ +# Verifiable Workflow Template (TypeScript) + +A TypeScript workflow template with reproducible builds that work on both Windows and macOS. This template enables third-party verification of deployed workflows using the CRE CLI. + +> **Note:** Go workflows are verifiable by default and do not require this template. This template is for TypeScript workflows only. For more details, see the [Verifying Workflows](https://docs.chain.link/cre/guides/operations/verifying-workflows) guide. + +## Prerequisites + +- [CRE CLI](https://github.com/smartcontractkit/cre-cli/releases) installed +- [Docker Desktop](https://www.docker.com/products/docker-desktop/) running + +## Project Structure + +``` +. +├── project.yaml +├── secrets.yaml +└── workflow/ + ├── Dockerfile + ├── Makefile + ├── bun.lock + ├── config.production.json + ├── config.staging.json + ├── main.ts + ├── main.test.ts + ├── package.json + ├── tsconfig.json + └── workflow.yaml +``` + +## Getting Started + +### Simulate the workflow + +Run from the **project root directory**: + +```bash +cre workflow simulate workflow --target=staging-settings +``` + +## Verifying a Workflow Build + +Workflow verification lets anyone independently confirm that a deployed workflow matches its source code. The `cre workflow hash` command computes the workflow hash locally and compares it against the onchain workflow ID. + +### Computing the workflow hash + +From the **project root directory**, run: + +```bash +cre workflow hash workflow --public_key --target production-settings +``` + +Replace `` with the deployer's public address (e.g. `0xb0f2D38245dD6d397ebBDB5A814b753D56c30715`). + +Example output: + +``` +Compiling workflow... +✓ Workflow compiled + Binary hash: 03c77e16354e5555f9a74e787f9a6aa0d939e9b8e4ddff06542b7867499c58ea + Config hash: 3bdaebcc2f639d77cb248242c1d01c8651f540cdbf423d26fe3128516fd225b6 + Workflow hash: 001de36f9d689b57f2e4f1eaeda1db5e79f7991402e3611e13a5c930599c2297 +``` + +The **Workflow hash** is the onchain workflow ID. If it matches the workflow ID observed onchain, the deployed workflow matches this source code. + +### For verifiers (third-party auditors) + +1. Install the [CRE CLI](https://github.com/smartcontractkit/cre-cli/releases). No login or deploy access is required. +2. Clone or unzip the shared workflow repository. +3. Run `cre workflow hash` as shown above, using the deployer's public address. +4. Compare the `Workflow hash` output with the onchain workflow ID. A match confirms the deployed workflow is built from this source. + +### Generating the lockfile + +If `bun.lock` is missing, generate it before building: + +```bash +cd workflow +make lock +``` + +This runs the lockfile generation inside Docker to ensure consistency across platforms. + +## How Reproducible Builds Work + +The build process uses Docker to ensure identical output on any machine: + +1. `make build` on the host starts a Docker build (`linux/amd64`) +2. Inside the container, `bun install --frozen-lockfile` installs exact dependencies from `bun.lock` +3. The workflow is compiled to a WASM binary (`workflow.wasm`) +4. The binary is exported back to the host + +Because the build runs inside a pinned Docker image with a locked dependency tree, the same source always produces the same binary hash. + +## Learn More + +- [Verifying Workflows](https://docs.chain.link/cre/guides/operations/verifying-workflows) - Full verification guide +- [Deploying Workflows](https://docs.chain.link/cre/guides/operations/deploying-workflows) +- [Building Consumer Contracts](https://docs.chain.link/cre/guides/workflow/using-evm-client/onchain-write/building-consumer-contracts) diff --git a/starter-templates/verifiable-build/verifiable-build-ts/project.yaml b/starter-templates/verifiable-build/verifiable-build-ts/project.yaml new file mode 100644 index 00000000..3b25aacf --- /dev/null +++ b/starter-templates/verifiable-build/verifiable-build-ts/project.yaml @@ -0,0 +1,44 @@ +# ========================================================================== +# CRE PROJECT SETTINGS FILE +# ========================================================================== +# Project-specific settings for CRE CLI targets. +# Each target defines cre-cli, account, and rpcs groups. +# +# Example custom target: +# my-target: +# account: +# workflow-owner-address: "0x123..." # Optional: Owner wallet/MSIG address (used for --unsigned transactions) +# rpcs: +# - chain-name: ethereum-testnet-sepolia # Required if your workflow interacts with this chain +# url: "" +# +# RPC URLs support ${VAR_NAME} syntax to reference environment variables. +# This keeps secrets out of project.yaml (which is committed to git). +# Variables are resolved from your .env file or exported shell variables. +# Example: +# - chain-name: ethereum-testnet-sepolia +# url: https://rpc.example.com/${CRE_SECRET_RPC_SEPOLIA} +# +# Experimental chains (automatically used by the simulator when present): +# Use this for chains not yet in official chain-selectors (e.g., hackathons, new chain integrations). +# In your workflow, reference the chain as evm:ChainSelector:@1.0.0 +# +# experimental-chains: +# - chain-selector: 12345 # The chain selector value +# rpc-url: "https://rpc.example.com" # RPC endpoint URL +# forwarder: "0x..." # Forwarder contract address on the chain + +# ========================================================================== +staging-settings: + rpcs: + - chain-name: ethereum-testnet-sepolia + url: https://ethereum-sepolia-rpc.publicnode.com + +# ========================================================================== +production-settings: + rpcs: + - chain-name: ethereum-testnet-sepolia + url: https://ethereum-sepolia-rpc.publicnode.com + diff --git a/starter-templates/verifiable-build/verifiable-build-ts/secrets.yaml b/starter-templates/verifiable-build/verifiable-build-ts/secrets.yaml new file mode 100644 index 00000000..7b85d864 --- /dev/null +++ b/starter-templates/verifiable-build/verifiable-build-ts/secrets.yaml @@ -0,0 +1 @@ +secretsNames: diff --git a/starter-templates/verifiable-build/verifiable-build-ts/workflow/.dockerignore b/starter-templates/verifiable-build/verifiable-build-ts/workflow/.dockerignore new file mode 100644 index 00000000..c1eba061 --- /dev/null +++ b/starter-templates/verifiable-build/verifiable-build-ts/workflow/.dockerignore @@ -0,0 +1,3 @@ +node_modules +wasm +*.wasm diff --git a/starter-templates/verifiable-build/verifiable-build-ts/workflow/.gitignore b/starter-templates/verifiable-build/verifiable-build-ts/workflow/.gitignore new file mode 100644 index 00000000..48127633 --- /dev/null +++ b/starter-templates/verifiable-build/verifiable-build-ts/workflow/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +wasm/ diff --git a/starter-templates/verifiable-build/verifiable-build-ts/workflow/Dockerfile b/starter-templates/verifiable-build/verifiable-build-ts/workflow/Dockerfile new file mode 100644 index 00000000..f38a7a90 --- /dev/null +++ b/starter-templates/verifiable-build/verifiable-build-ts/workflow/Dockerfile @@ -0,0 +1,19 @@ +# oven/bun:1.3.5 (x64 / amd64 digest) +FROM oven/bun@sha256:f48ca3e042de3c9da0afb25455414db376c873bf88f79ffb6f26aec82bdb6da2 AS builder + +RUN apt-get update && apt-get install -y make && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +ENV CRE_DOCKER_BUILD_IMAGE=true + +COPY package.json bun.lock ./ +RUN bun install --frozen-lockfile + +COPY . . + +RUN make build + +# --- EXPORT STAGE --- +FROM scratch AS exporter +COPY --from=builder /app/wasm /wasm diff --git a/starter-templates/verifiable-build/verifiable-build-ts/workflow/Dockerfile.lock b/starter-templates/verifiable-build/verifiable-build-ts/workflow/Dockerfile.lock new file mode 100644 index 00000000..ad5a9040 --- /dev/null +++ b/starter-templates/verifiable-build/verifiable-build-ts/workflow/Dockerfile.lock @@ -0,0 +1,11 @@ +# oven/bun:1.3.5 (x64 / amd64 digest) +FROM oven/bun@sha256:f48ca3e042de3c9da0afb25455414db376c873bf88f79ffb6f26aec82bdb6da2 + +WORKDIR /app + +COPY package.json ./ +RUN bun install + +# --- EXPORT STAGE --- +FROM scratch AS exporter +COPY --from=0 /app/bun.lock /bun.lock diff --git a/starter-templates/verifiable-build/verifiable-build-ts/workflow/Makefile b/starter-templates/verifiable-build/verifiable-build-ts/workflow/Makefile new file mode 100644 index 00000000..cbd50eb6 --- /dev/null +++ b/starter-templates/verifiable-build/verifiable-build-ts/workflow/Makefile @@ -0,0 +1,24 @@ +.PHONY: build lock + +build: +ifeq ($(CRE_DOCKER_BUILD_IMAGE),true) + @if [ ! -f bun.lock ]; then \ + echo "ERROR: bun.lock is missing."; \ + echo "If you are verifying this workflow, ask the original author to publish their lockfile —"; \ + echo "generating it yourself will likely produce a different lock and verification will fail."; \ + echo "To generate one anyway, run 'make lock' from $(CURDIR)"; \ + exit 1; \ + fi + mkdir -p wasm + bun cre-compile main.ts wasm/workflow.wasm +else + $(if $(wildcard package.json),,$(error ERROR: package.json not found in workflow/. This file is required for the build)) + $(if $(wildcard bun.lock),,$(error ERROR: bun.lock is missing. If you are verifying this workflow, ask the original author to publish their lockfile — generating it yourself will likely produce a different lock and verification will fail. To generate one anyway, run 'make lock' from $(CURDIR))) + $(if $(wildcard Dockerfile),,$(error ERROR: Dockerfile not found in workflow/. Restore it from the template)) + $(if $(wildcard main.ts),,$(error ERROR: main.ts not found in workflow/. The workflow entry point is missing)) + docker build --platform=linux/amd64 --output type=local,dest="$(CURDIR)" . + @echo "Build complete. workflow.wasm is ready in $(CURDIR)/wasm" +endif + +lock: + docker build --platform=linux/amd64 -f Dockerfile.lock --output type=local,dest="$(CURDIR)" . diff --git a/starter-templates/verifiable-build/verifiable-build-ts/workflow/config.production.json b/starter-templates/verifiable-build/verifiable-build-ts/workflow/config.production.json new file mode 100644 index 00000000..1a360cb3 --- /dev/null +++ b/starter-templates/verifiable-build/verifiable-build-ts/workflow/config.production.json @@ -0,0 +1,3 @@ +{ + "schedule": "*/30 * * * * *" +} diff --git a/starter-templates/verifiable-build/verifiable-build-ts/workflow/config.staging.json b/starter-templates/verifiable-build/verifiable-build-ts/workflow/config.staging.json new file mode 100644 index 00000000..1a360cb3 --- /dev/null +++ b/starter-templates/verifiable-build/verifiable-build-ts/workflow/config.staging.json @@ -0,0 +1,3 @@ +{ + "schedule": "*/30 * * * * *" +} diff --git a/starter-templates/verifiable-build/verifiable-build-ts/workflow/main.test.ts b/starter-templates/verifiable-build/verifiable-build-ts/workflow/main.test.ts new file mode 100644 index 00000000..d108aadb --- /dev/null +++ b/starter-templates/verifiable-build/verifiable-build-ts/workflow/main.test.ts @@ -0,0 +1,42 @@ +import { describe, expect } from "bun:test"; +import { newTestRuntime, test } from "@chainlink/cre-sdk/test"; +import { onCronTrigger, initWorkflow } from "./main"; +import type { Config } from "./main"; + +describe("onCronTrigger", () => { + test("logs message and returns greeting", async () => { + const config: Config = { schedule: "*/5 * * * *" }; + const runtime = newTestRuntime(); + runtime.config = config; + + const result = onCronTrigger(runtime); + + expect(result).toBe("Hello world!"); + const logs = runtime.getLogs(); + expect(logs).toContain("Hello world! Workflow triggered."); + }); +}); + +describe("initWorkflow", () => { + test("returns one handler with correct cron schedule", async () => { + const testSchedule = "0 0 * * *"; + const config: Config = { schedule: testSchedule }; + + const handlers = initWorkflow(config); + + expect(handlers).toBeArray(); + expect(handlers).toHaveLength(1); + expect(handlers[0].trigger.config.schedule).toBe(testSchedule); + }); + + test("handler executes onCronTrigger and returns result", async () => { + const config: Config = { schedule: "*/5 * * * *" }; + const runtime = newTestRuntime(); + runtime.config = config; + const handlers = initWorkflow(config); + + const result = handlers[0].fn(runtime, {}); + + expect(result).toBe(onCronTrigger(runtime)); + }); +}); diff --git a/starter-templates/verifiable-build/verifiable-build-ts/workflow/main.ts b/starter-templates/verifiable-build/verifiable-build-ts/workflow/main.ts new file mode 100644 index 00000000..45f9e071 --- /dev/null +++ b/starter-templates/verifiable-build/verifiable-build-ts/workflow/main.ts @@ -0,0 +1,28 @@ +import { CronCapability, handler, Runner, type Runtime } from "@chainlink/cre-sdk"; + +export type Config = { + schedule: string; +}; + +export const onCronTrigger = (runtime: Runtime): string => { + runtime.log("Hello world! Workflow triggered."); + return "Hello world!"; +}; + +export const initWorkflow = (config: Config) => { + const cron = new CronCapability(); + + return [ + handler( + cron.trigger( + { schedule: config.schedule } + ), + onCronTrigger + ), + ]; +}; + +export async function main() { + const runner = await Runner.newRunner(); + await runner.run(initWorkflow); +} diff --git a/starter-templates/verifiable-build/verifiable-build-ts/workflow/package.json b/starter-templates/verifiable-build/verifiable-build-ts/workflow/package.json new file mode 100644 index 00000000..d3d7aed8 --- /dev/null +++ b/starter-templates/verifiable-build/verifiable-build-ts/workflow/package.json @@ -0,0 +1,16 @@ +{ + "name": "typescript-simple-template", + "version": "1.0.0", + "main": "dist/main.js", + "private": true, + "scripts": { + "postinstall": "bun x cre-setup" + }, + "license": "UNLICENSED", + "dependencies": { + "@chainlink/cre-sdk": "^1.5.0" + }, + "devDependencies": { + "@types/bun": "1.3.5" + } +} diff --git a/starter-templates/verifiable-build/verifiable-build-ts/workflow/tsconfig.json b/starter-templates/verifiable-build/verifiable-build-ts/workflow/tsconfig.json new file mode 100644 index 00000000..840fdc79 --- /dev/null +++ b/starter-templates/verifiable-build/verifiable-build-ts/workflow/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ESNext"], + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "main.ts" + ] +} diff --git a/starter-templates/verifiable-build/verifiable-build-ts/workflow/workflow.yaml b/starter-templates/verifiable-build/verifiable-build-ts/workflow/workflow.yaml new file mode 100644 index 00000000..fefe90f2 --- /dev/null +++ b/starter-templates/verifiable-build/verifiable-build-ts/workflow/workflow.yaml @@ -0,0 +1,14 @@ +production-settings: + user-workflow: + workflow-name: workflow-production + workflow-artifacts: + config-path: ./config.production.json + secrets-path: "" + workflow-path: ./wasm/workflow.wasm +staging-settings: + user-workflow: + workflow-name: workflow-staging + workflow-artifacts: + config-path: ./config.staging.json + secrets-path: "" + workflow-path: ./wasm/workflow.wasm