diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.github/workflows/ict-E2E.yml b/.github/workflows/ict-E2E.yml new file mode 100644 index 0000000..78bf323 --- /dev/null +++ b/.github/workflows/ict-E2E.yml @@ -0,0 +1,213 @@ +name: ictest E2E + +on: + pull_request: + push: + tags: + - "**" + branches: + - "main" + - "master" + +permissions: + contents: read + packages: write + +env: + GO_VERSION: 1.24.5 + TAR_PATH: /tmp/terp-docker-image.tar + IMAGE_NAME: terp-docker-image + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Go ${{ env.GO_VERSION }} + uses: actions/setup-go@v6 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: interchaintest/go.sum + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and export + uses: docker/build-push-action@v6 + with: + context: . + tags: terpnetwork/terp-core:local + outputs: type=docker,dest=${{ env.TAR_PATH }} + + - name: Upload artifact + uses: actions/upload-artifact@v5 + with: + name: ${{ env.IMAGE_NAME }} + path: ${{ env.TAR_PATH }} + + e2e-tests: + needs: build-docker + runs-on: ubuntu-latest + strategy: + matrix: + test: + - "e2e-basic" + - "e2e-pfm" + - "e2e-ibc" + - "e2e-polytone" + # - "e2e-clock" + # - "e2e-upgrade" + # - "e2e-drip" + # - "e2e-cwhooks" + fail-fast: false + + steps: + - name: Set up Go ${{ env.GO_VERSION }} + uses: actions/setup-go@v6 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: interchaintest/go.sum + + - name: checkout chain + uses: actions/checkout@v6 + + - name: Download Tarball Artifact + uses: actions/download-artifact@v6 + with: + name: ${{ env.IMAGE_NAME }} + path: /tmp + + - name: Load Docker Image + run: | + docker image load -i ${{ env.TAR_PATH }} + docker image ls -a + + - name: Run Test + id: run_test + continue-on-error: true + run: make ${{ matrix.test }} + + - name: Retry Failed Test + if: steps.run_test.outcome == 'failure' + run: | + for i in 1 2; do + echo "Retry attempt $i" + if make ${{ matrix.test }}; then + echo "Test passed on retry" + exit 0 + fi + done + echo "Test failed after retries" + exit 1 + + build-ict: + runs-on: ubuntu-latest + steps: + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry and target + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + workspace/ict-rs/target + key: ${{ runner.os }}-cargo-ict-${{ hashFiles('workspace/ict-rs/**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-ict- + + - name: Detect dependency profile + id: deps + run: | + BRANCH="${{ github.head_ref || github.ref_name }}" + if echo "$BRANCH" | grep -qi "zk"; then + echo "cosmos_rust_ref=zk" >> $GITHUB_OUTPUT + echo "tendermint_ref=zk" >> $GITHUB_OUTPUT + echo "cw_orch_ref=zk" >> $GITHUB_OUTPUT + else + echo "cosmos_rust_ref=main" >> $GITHUB_OUTPUT + echo "tendermint_ref=main" >> $GITHUB_OUTPUT + echo "cw_orch_ref=cw3" >> $GITHUB_OUTPUT + fi + + - name: Clone ict-rs and dependencies + run: | + mkdir -p workspace + git clone --depth 1 https://github.com/permissionlessweb/ict-rs.git workspace/ict-rs + git clone --depth 1 -b ${{ steps.deps.outputs.cosmos_rust_ref }} https://github.com/permissionlessweb/cosmos-rust.git workspace/cosmos-rust + git clone --depth 1 -b ${{ steps.deps.outputs.tendermint_ref }} https://github.com/permissionlessweb/tendermint-rs.git workspace/tendermint-rs + git clone --depth 1 -b ${{ steps.deps.outputs.cw_orch_ref }} https://github.com/permissionlessweb/cw-orchestrator.git workspace/cw-orchestrator + + - name: Checkout terp-core for contract wasm + uses: actions/checkout@v6 + with: + path: workspace/terp-core + + - name: Build all ict-rs examples + run: cd workspace/ict-rs && cargo build --examples --features docker + + - name: Package example binaries + run: | + mkdir -p /tmp/ict-binaries + cp workspace/ict-rs/target/debug/examples/state_sync /tmp/ict-binaries/ + cp workspace/ict-rs/target/debug/examples/bootstrap_mainnet /tmp/ict-binaries/ + cp workspace/ict-rs/target/debug/examples/loyalty_rewards /tmp/ict-binaries/ + # Package loyalty-db assets and contract wasm + mkdir -p /tmp/ict-binaries/loyalty-db + cp workspace/ict-rs/examples/loyalty-db/schema.sql /tmp/ict-binaries/loyalty-db/ + cp workspace/ict-rs/examples/loyalty-db/seed.js /tmp/ict-binaries/loyalty-db/ + cp workspace/ict-rs/examples/loyalty-db/package.json /tmp/ict-binaries/loyalty-db/ + cp workspace/terp-core/tests/interchaintest/contracts/loyalty_verifier.wasm /tmp/ict-binaries/ + tar -czf /tmp/ict-binaries.tar.gz -C /tmp/ict-binaries . + + - name: Upload ict-rs binaries + uses: actions/upload-artifact@v5 + with: + name: ict-binaries + path: /tmp/ict-binaries.tar.gz + + ict-tests: + needs: [build-docker, build-ict] + runs-on: ubuntu-latest + strategy: + matrix: + test: + - state_sync + - bootstrap_mainnet + - loyalty_rewards + fail-fast: false + + steps: + - name: Download Docker Tarball + uses: actions/download-artifact@v6 + with: + name: ${{ env.IMAGE_NAME }} + path: /tmp + + - name: Load Docker Image + run: | + docker image load -i ${{ env.TAR_PATH }} + docker image ls -a + + - name: Download ict-rs binaries + uses: actions/download-artifact@v6 + with: + name: ict-binaries + path: /tmp + + - name: Extract and run ict-rs test + env: + TERP_IMAGE_REPO: terpnetwork/terp-core + TERP_IMAGE_VERSION: local + run: | + mkdir -p /tmp/ict-binaries + tar -xzf /tmp/ict-binaries.tar.gz -C /tmp/ict-binaries + chmod +x /tmp/ict-binaries/${{ matrix.test }} + /tmp/ict-binaries/${{ matrix.test }} diff --git a/.github/workflows/interchaintest-E2E.yml b/.github/workflows/interchaintest-E2E.yml deleted file mode 100644 index 2325e65..0000000 --- a/.github/workflows/interchaintest-E2E.yml +++ /dev/null @@ -1,108 +0,0 @@ -name: ictest E2E - -on: - pull_request: - push: - tags: - - "**" - branches: - - "main" - - "master" - -permissions: - contents: read - packages: write - -env: - GO_VERSION: 1.24.5 - TAR_PATH: /tmp/terp-docker-image.tar - IMAGE_NAME: terp-docker-image - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build-docker: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup Go ${{ env.GO_VERSION }} - uses: actions/setup-go@v6 - with: - go-version: ${{ env.GO_VERSION }} - cache-dependency-path: interchaintest/go.sum - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and export - uses: docker/build-push-action@v6 - with: - context: . - tags: terpnetwork/terp-core:local - outputs: type=docker,dest=${{ env.TAR_PATH }} - - - name: Upload artifact - uses: actions/upload-artifact@v5 - with: - name: ${{ env.IMAGE_NAME }} - path: ${{ env.TAR_PATH }} - - e2e-tests: - needs: build-docker - runs-on: ubuntu-latest - strategy: - matrix: - # names of `make` commands to run tests - test: - - "e2e-basic" - - "e2e-pfm" - - "e2e-ibc" - - "e2e-polytone" - # - "e2e-clock" - # - "e2e-upgrade" - # - "e2e-drip" - # - "e2e-cwhooks" - fail-fast: false - - steps: - - name: Set up Go ${{ env.GO_VERSION }} - uses: actions/setup-go@v6 - with: - go-version: ${{ env.GO_VERSION }} - cache-dependency-path: interchaintest/go.sum - - - name: checkout chain - uses: actions/checkout@v6 - - - name: Download Tarball Artifact - uses: actions/download-artifact@v6 - with: - name: ${{ env.IMAGE_NAME }} - path: /tmp - - - name: Load Docker Image - run: | - docker image load -i ${{ env.TAR_PATH }} - docker image ls -a - - - name: Run Test - id: run_test - continue-on-error: true - run: make ${{ matrix.test }} - - - name: Retry Failed Test - if: steps.run_test.outcome == 'failure' - run: | - for i in 1 2; do - echo "Retry attempt $i" - if make ${{ matrix.test }}; then - echo "Test passed on retry" - exit 0 - fi - done - echo "Test failed after retries" - exit 1 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0f8833b..89748ba 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,7 +40,7 @@ jobs: --build-arg GO_VERSION=${{ steps.go-version.outputs.version }} \ --build-arg GIT_VERSION=${VERSION} \ --build-arg GIT_COMMIT=${COMMIT} \ - --build-arg RUNNER_IMAGE=alpine:3.17 \ + --build-arg RUNNER_IMAGE=alpine:3.20 \ --platform linux/${{ matrix.arch }} \ -t terp-core:local-${{ matrix.arch }} \ --load \ diff --git a/.gitignore b/.gitignore index 4443aee..1e25f5d 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,7 @@ dist heighliner* tools-stamp docs/node_modules - +target # Data - ideally these don't exist baseapp/data/* client/lcd/keys/* diff --git a/Dockerfile b/Dockerfile index 9ce2462..df42b78 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,10 @@ ARG GO_VERSION=1.24 ARG RUNNER_IMAGE=alpine:3.17 + +# WASMVM_SOURCE controls where the static wasmvm library comes from: +# "github" (default) — download libwasmvm_muslc from CosmWasm GitHub releases +# "local" — use pre-built lib from build/wasmvm/ (for custom zk-wasmvm) +ARG WASMVM_SOURCE=github FROM golang:${GO_VERSION}-alpine AS go-builder @@ -16,8 +21,13 @@ WORKDIR /code # Pull in the go.mod file *first* so the layer can be cached ADD go.mod go.sum ./ +# Re-declare ARGs after FROM (Docker scoping rule) +ARG WASMVM_SOURCE=github + # --------------------------------------------------------- -# Pull in the exact wasmvm lib that the Cosmos SDK wants +# Pull in the wasmvm static library (github mode only). +# In local mode the lib is staged in build/wasmvm/ and will +# be copied after the full source COPY below. # --------------------------------------------------------- RUN ARCH=$(uname -m) && \ WASMVM_VERSION=$(go list -m github.com/CosmWasm/wasmvm/v3 | awk '{print $2}') && \ @@ -37,12 +47,12 @@ RUN echo "Ensuring binary is statically linked ..." \ && (file /code/build/terpd | grep "statically linked") # --------------------------------------------------------- -# 1️⃣ Runtime image – this is normal terpd binary +# 1. Runtime image — standard (github wasmvm) # --------------------------------------------------------- FROM ${RUNNER_IMAGE} AS runtime -# Minimal set of runtime deps (ca‑certs is enough for HTTPS RPC) -RUN apk add --no-cache ca-certificates +# Copy ca-certificates from builder (works on distroless, Alpine, and nonroot) +COPY --from=go-builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=go-builder /code/build/terpd /usr/local/bin/terpd @@ -55,7 +65,17 @@ EXPOSE 1317 26656 26657 CMD ["/usr/local/bin/terpd"] # --------------------------------------------------------- -# 2️⃣ Localterp bootstrap image – binary + init scripts + tools +# 2. O-Line deployment image — runtime + bootstrap tools +# --------------------------------------------------------- +FROM runtime AS oline +RUN apk add --no-cache \ + bash curl jq openssh openssl \ + coreutils file pv lz4 zstd unzip wget \ + nginx sed gawk +EXPOSE 1317 26656 26657 9090 22 80 + +# --------------------------------------------------------- +# 3. Localterp bootstrap image # --------------------------------------------------------- FROM alpine:3.17 AS localterp RUN apk add --no-cache \ @@ -76,9 +96,15 @@ COPY docker/localterp/bootstrap.sh . COPY docker/localterp/initialize.sh . COPY docker/localterp/start.sh . COPY docker/localterp/faucet/faucet_server.js . - RUN chmod +x *.sh +# localterp key mnemonics +ENV VALIDATOR_MNEMONIC="push certain add next grape invite tobacco bubble text romance again lava crater pill genius vital fresh guard great patch knee series era tonight" +ENV ACCOUNT_A_MNEMONIC="grant rice replace explain federal release fix clever romance raise often wild taxi quarter soccer fiber love must tape steak together observe swap guitar" +ENV ACCOUNT_B_MNEMONIC="jelly shadow frog dirt dragon use armed praise universe win jungle close inmate rain oil canvas beauty pioneer chef soccer icon dizzy thunder meadow" +ENV ACCOUNT_C_MNEMONIC="chair love bleak wonder skirt permit say assist aunt credit roast size obtain minute throw sand usual age smart exact enough room shadow charge" +ENV ACCOUNT_FAUCET_MNEMONIC="word twist toast cloth movie predict advance crumble escape whale sail such angry muffin balcony keen move employ cook valve hurt glimpse breeze brick" + # 1317=LCD proxy, 5000=faucet, 26656=P2P, 26657=RPC, 9090=GRPC EXPOSE 1317 5000 26656 26657 9090 @@ -86,4 +112,4 @@ HEALTHCHECK --interval=5s --timeout=1s --retries=120 \ CMD bash -c 'curl -sfm1 http://localhost:26657/status && \ curl -s http://localhost:26657/status | jq -e "(.result.sync_info.latest_block_height | tonumber) > 0"' -ENTRYPOINT ["/code/bootstrap.sh"] \ No newline at end of file +ENTRYPOINT ["/code/bootstrap.sh"] diff --git a/Makefile b/Makefile index 31ca0c5..ffa124e 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,7 @@ include scripts/makefiles/build.mk include scripts/makefiles/docker.mk include scripts/makefiles/e2e.mk +include scripts/makefiles/ict.mk include scripts/makefiles/format.mk include scripts/makefiles/tsh.mk # include scripts/makefiles/localnet.mk @@ -25,6 +26,7 @@ help: @echo " make deps Show available deps commands" @echo " make docker Show available docker commands" @echo " make e2e Show available e2e commands" + @echo " make ict Show available ict-rs commands" @echo " make lint Show available lint commands" @echo " make proto Show available proto commands" @echo " make release Show available release commands" diff --git a/app/app.go b/app/app.go index b92cd0f..015a98e 100644 --- a/app/app.go +++ b/app/app.go @@ -141,22 +141,16 @@ var ( // of "EnableAllProposals" (takes precedence over ProposalsEnabled) // https://github.com/terpnetwork/terp-core/blob/02a54d33ff2c064f3539ae12d75d027d9c665f05/x/wasm/internal/types/proposal.go#L28-L34 EnableSpecificProposals = "" - // EmptyWasmOpts defines a type alias for a list of wasm options. EmptyWasmOpts []wasmkeeper.Option - - Upgrades = []upgrades.Upgrade{ // v2.Upgrade,v3.Upgrade,v4.Upgrade,v4_1.Upgrade, + Upgrades = []upgrades.Upgrade{ // v2.Upgrade,v3.Upgrade,v4.Upgrade,v4_1.Upgrade, v5.Upgrade, } ) -// These constants are derived from the above variables. -// These are the ones we will want to use in the code, based on -// any overrides above var ( // DefaultNodeHome default home directories for terpd DefaultNodeHome = os.ExpandEnv("$HOME/") + NodeDir - // Bech32PrefixAccAddr defines the Bech32 prefix of an account's address Bech32PrefixAccAddr = Bech32Prefix // Bech32PrefixAccPub defines the Bech32 prefix of an account's public key @@ -304,17 +298,12 @@ func NewTerpApp( clientKeeper := appKeepers.IBCKeeper.ClientKeeper storeProvider := appKeepers.IBCKeeper.ClientKeeper.GetStoreProvider() - - // Add tendermint & ibcWasm light client routes tmLightClientModule := ibctm.NewLightClientModule(appCodec, storeProvider) ibcWasmLightClientModule := ibcwlc.NewLightClientModule(*appKeepers.IBCWasmClientKeeper, storeProvider) clientKeeper.AddRoute(ibctm.ModuleName, &tmLightClientModule) clientKeeper.AddRoute(ibcwlctypes.ModuleName, ibcWasmLightClientModule) - // Setup keepers app.AppKeepers = appKeepers - app.keys = app.GetKVStoreKey() - enabledSignModes := append(authtx.DefaultSignModes, sigtypes.SignMode_SIGN_MODE_TEXTUAL) txConfigOpts := authtx.ConfigOptions{ EnabledSignModes: enabledSignModes, @@ -387,32 +376,20 @@ func NewTerpApp( crisis.NewAppModule(app.CrisisKeeper, skipGenesisInvariants, app.GetSubspace(crisistypes.ModuleName)), // always be last to make sure that it checks for all invariants and not only part of them ) - // Upgrades from v0.50.x onwards happen in pre block - app.mm.SetOrderPreBlockers(upgradetypes.ModuleName, authtypes.ModuleName) - // During begin block slashing happens after distr.BeginBlocker so that // there is nothing left over in the validator fee pool, so as to keep the // CanWithdrawInvariant invariant. - // NOTE: staking module is required if HistoricalEntries param > 0 + app.mm.SetOrderPreBlockers(upgradetypes.ModuleName, authtypes.ModuleName) app.mm.SetOrderBeginBlockers(orderBeginBlockers()...) - app.mm.SetOrderEndBlockers(orderEndBlockers()...) - app.mm.SetOrderInitGenesis(orderInitBlockers()...) - - app.mm.RegisterInvariants(app.CrisisKeeper) - - // upgrade handlers app.configurator = module.NewConfigurator(appCodec, app.MsgServiceRouter(), app.GRPCQueryRouter()) err = app.mm.RegisterServices(app.configurator) if err != nil { panic(err) } - // initialize stores app.MountKVStores(app.keys) app.MountTransientStores(app.GetTransientStoreKey()) - - // register upgrade app.setupUpgradeHandlers(app.configurator) autocliv1.RegisterQueryServer(app.GRPCQueryRouter(), runtimeservices.NewAutoCLIQueryService(app.mm.Modules)) diff --git a/cmd/terpd/cmd/bootstrap.go b/cmd/terpd/cmd/bootstrap.go new file mode 100644 index 0000000..709ff9f --- /dev/null +++ b/cmd/terpd/cmd/bootstrap.go @@ -0,0 +1,834 @@ +package cmd + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "syscall" + "time" + + cmtcfg "github.com/cometbft/cometbft/config" + rpchttp "github.com/cometbft/cometbft/rpc/client/http" + "github.com/cosmos/cosmos-sdk/server" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/terpnetwork/terp-core/v5/app" +) + +// BootstrapConfig holds settings for automated node bootstrapping. +// Persisted in app.toml under [bootstrap]. +type BootstrapConfig struct { + SyncMode string `mapstructure:"sync-mode"` + GenesisURL string `mapstructure:"genesis-url"` + GenesisHash string `mapstructure:"genesis-hash"` + SnapshotURL string `mapstructure:"snapshot-url"` + StateSyncRPCs string `mapstructure:"statesync-rpcs"` + TrustOffset int64 `mapstructure:"trust-offset"` + MaxRetries int `mapstructure:"max-retries"` + Seeds string `mapstructure:"seeds"` + PersistentPeers string `mapstructure:"persistent-peers"` + PrivateMode bool `mapstructure:"private-mode"` + Cosmovisor bool `mapstructure:"cosmovisor"` + Service bool `mapstructure:"service"` + Pruning string `mapstructure:"pruning"` +} + +// networkPreset holds known-good configuration for a specific network. +type networkPreset struct { + ChainID string + GenesisURL string + RPCs string +} + +var networkPresets = map[string]networkPreset{ + "morocco-1": { + ChainID: "morocco-1", + GenesisURL: "https://raw.githubusercontent.com/terpnetwork/networks/refs/heads/main/mainnet/morocco-1/genesis.json", + RPCs: "https://rpc.terp.chaintools.tech:443", + }, + "120u-1": { + ChainID: "120u-1", + GenesisURL: "https://raw.githubusercontent.com/terpnetwork/networks/refs/heads/main/testnet/120u-1/genesis.json", + RPCs: "https://testnet-rpc.terp.network:443", + }, +} + +// DefaultBootstrapConfig returns sensible defaults for mainnet bootstrapping. +func DefaultBootstrapConfig() BootstrapConfig { + return BootstrapConfig{ + SyncMode: "statesync", + GenesisURL: "https://raw.githubusercontent.com/terpnetwork/networks/refs/heads/main/mainnet/morocco-1/genesis.json", + GenesisHash: "", + SnapshotURL: "", + StateSyncRPCs: "https://rpc.terp.network:443,https://rpc.terp.network:443", + TrustOffset: 100, + MaxRetries: 6, + Seeds: "", + PersistentPeers: "", + PrivateMode: true, + Cosmovisor: false, + Service: false, + Pruning: "", + } +} + +// BootstrapConfigTemplate is the TOML template appended to app.toml. +const BootstrapConfigTemplate = ` +############################################################################### +### Bootstrap Configuration ### +############################################################################### + +[bootstrap] + +# Sync mode: "statesync" or "snapshot" +sync-mode = "{{ .Bootstrap.SyncMode }}" + +# Genesis file download URL +genesis-url = "{{ .Bootstrap.GenesisURL }}" + +# Expected SHA256 hash of genesis.json (empty = skip validation) +genesis-hash = "{{ .Bootstrap.GenesisHash }}" + +# Snapshot tarball URL (used when sync-mode = "snapshot") +snapshot-url = "{{ .Bootstrap.SnapshotURL }}" + +# Private mode (default true): disables PEX gossip, rejects inbound peers, +# only connects to configured persistent peers. Ideal for local state-sync +# testing without participating in the network. Use --public to disable. +private-mode = {{ .Bootstrap.PrivateMode }} +` + +// BootstrapCmd automates full node bootstrapping for Docker and manual use. +var BootstrapCmd = &cobra.Command{ + Use: "bootstrap", + Short: "Bootstrap and start a Terp node (init + config + sync + start)", + Long: `Fully automated node bootstrapping for Docker entrypoints and fresh nodes. + +Flow: + 1. Init node if not already initialized + 2. Download and validate genesis (known hash) + 3. Configure peers, seeds, state-sync or snapshot restore + 4. Exec into terpd start + +Settings are read from app.toml [bootstrap] section and can be overridden +with flags or environment variables. + +Docker usage: + ENTRYPOINT ["terpd"] + CMD ["bootstrap"] + +Direct usage: + terpd bootstrap + terpd bootstrap --sync-mode snapshot --snapshot-url https://...`, + RunE: runBootstrap, +} + +func init() { + BootstrapCmd.Flags().String("moniker", "", "node moniker (auto-generated if empty)") + BootstrapCmd.Flags().String("chain-id", "morocco-1", "chain ID") + BootstrapCmd.Flags().String("network", "", "preset network config: morocco-1 (mainnet) or 90u-4 (testnet)") + BootstrapCmd.Flags().String("sync-mode", "", "override sync mode: statesync or snapshot") + BootstrapCmd.Flags().String("genesis-url", "", "override genesis download URL") + BootstrapCmd.Flags().String("genesis-hash", "", "override expected genesis SHA256 hash") + BootstrapCmd.Flags().String("snapshot-url", "", "override snapshot tarball URL") + BootstrapCmd.Flags().String("statesync-rpcs", "", "override state-sync RPC endpoints (comma-separated)") + BootstrapCmd.Flags().Int64("trust-offset", 0, "override trust offset") + BootstrapCmd.Flags().Int("max-retries", 0, "override max retries") + BootstrapCmd.Flags().String("bootstrap-seeds", "", "override seed nodes") + BootstrapCmd.Flags().String("bootstrap-peers", "", "override persistent peers") + BootstrapCmd.Flags().Bool("public", false, "public mode: enable PEX gossip and accept inbound peers (default is private)") + BootstrapCmd.Flags().Bool("cosmovisor", false, "install cosmovisor via 'go install' and initialize it") + BootstrapCmd.Flags().Bool("service", false, "create a systemd service (Linux only, works with --cosmovisor)") + BootstrapCmd.Flags().String("pruning", "", "pruning strategy: default, nothing, or everything") + BootstrapCmd.Flags().Bool("setup-only", false, "perform setup without starting the node") +} + +func runBootstrap(cmd *cobra.Command, args []string) error { + home, _ := cmd.Flags().GetString("home") + if home == "" { + home = app.DefaultNodeHome + } + moniker, _ := cmd.Flags().GetString("moniker") + chainID, _ := cmd.Flags().GetString("chain-id") + + if moniker == "" { + moniker = fmt.Sprintf("terp-node-%d", time.Now().Unix()%10000) + } + + // Load bootstrap config from app.toml [bootstrap] section + bsCfg := DefaultBootstrapConfig() + serverCtx := server.GetServerContextFromCmd(cmd) + if serverCtx != nil && serverCtx.Viper != nil { + _ = serverCtx.Viper.UnmarshalKey("bootstrap", &bsCfg) + } + + // Apply --network preset (overrides defaults before flag overrides) + if network, _ := cmd.Flags().GetString("network"); network != "" { + preset, ok := networkPresets[network] + if !ok { + return fmt.Errorf("unknown network %q (available: morocco-1, 90u-4)", network) + } + chainID = preset.ChainID + bsCfg.GenesisURL = preset.GenesisURL + bsCfg.StateSyncRPCs = preset.RPCs + fmt.Printf("Using network preset: %s\n", network) + } + + // Override with flags (only when explicitly set) + applyBootstrapFlagOverrides(cmd, &bsCfg) + + // --public flag inverts private mode + if isPublic, _ := cmd.Flags().GetBool("public"); isPublic { + bsCfg.PrivateMode = false + } + + modeStr := "private" + if !bsCfg.PrivateMode { + modeStr = "public" + } + + fmt.Println("=== Terp Bootstrap ===") + fmt.Printf(" Home : %s\n", home) + fmt.Printf(" Chain ID : %s\n", chainID) + fmt.Printf(" Moniker : %s\n", moniker) + fmt.Printf(" Sync mode: %s\n", bsCfg.SyncMode) + fmt.Printf(" P2P mode : %s\n\n", modeStr) + + // ──── Step 1: Init if needed ──── + genesisPath := filepath.Join(home, "config", "genesis.json") + if _, err := os.Stat(genesisPath); os.IsNotExist(err) { + fmt.Println("Node not initialized. Running init...") + if err := runInit(home, moniker, chainID); err != nil { + return err + } + fmt.Println("Init complete.") + } else { + fmt.Println("Node already initialized.") + } + + // ──── Step 2: Genesis download + validation ──── + if bsCfg.GenesisURL != "" { + fmt.Printf("Downloading genesis from %s\n", bsCfg.GenesisURL) + if err := downloadFile(bsCfg.GenesisURL, genesisPath); err != nil { + return fmt.Errorf("genesis download failed: %w", err) + } + fmt.Println("Genesis downloaded.") + } + + if bsCfg.GenesisHash != "" { + fmt.Printf("Validating genesis hash...") + if err := validateFileHash(genesisPath, bsCfg.GenesisHash); err != nil { + return fmt.Errorf("genesis validation failed: %w", err) + } + fmt.Println(" OK") + } + + // ──── Step 3: Load and modify CometBFT config ──── + cmtCfg, err := loadCometConfig(home) + if err != nil { + return fmt.Errorf("failed to load config.toml: %w", err) + } + + // Apply seeds and persistent peers from bootstrap config + if bsCfg.Seeds != "" { + cmtCfg.P2P.Seeds = bsCfg.Seeds + } + if bsCfg.PersistentPeers != "" { + cmtCfg.P2P.PersistentPeers = bsCfg.PersistentPeers + } + + switch bsCfg.SyncMode { + case "statesync": + if err := configureStateSyncBootstrap(cmtCfg, bsCfg); err != nil { + return err + } + case "snapshot": + // if err := configureSnapshotBootstrap(home, cmtCfg, bsCfg); err != nil { + // return err + // } + default: + return fmt.Errorf("unknown sync-mode: %q (use 'statesync' or 'snapshot')", bsCfg.SyncMode) + } + + // ──── Step 4: Apply P2P mode ──── + if bsCfg.PrivateMode { + applyPrivateMode(cmtCfg) + fmt.Println("Private mode: PEX disabled, inbound peers rejected, no gossip.") + } + + // Write updated config.toml + configTomlPath := filepath.Join(home, "config", "config.toml") + cmtcfg.WriteConfigFile(configTomlPath, cmtCfg) + fmt.Println("config.toml updated.") + + // ──── Step 5: Apply pruning to app.toml ──── + if bsCfg.Pruning != "" { + if err := applyPruningConfig(home, bsCfg.Pruning); err != nil { + return err + } + } + + // ──── Step 6: Cosmovisor setup ──── + binary, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to resolve executable path: %w", err) + } + + if bsCfg.Cosmovisor { + if err := installCosmovisor(binary, home); err != nil { + return err + } + } + + // ──── Step 7: Systemd service ──── + if bsCfg.Service { + if err := createSystemdService(home, bsCfg.Cosmovisor); err != nil { + return err + } + } + + // ──── Step 8: Exec into terpd start ──── + setupOnly, _ := cmd.Flags().GetBool("setup-only") + if setupOnly { + fmt.Println("Bootstrap complete (setup-only). Node is ready to start.") + return nil + } + + fmt.Println("Bootstrap complete. Starting node...") + + startArgs := []string{"terpd", "start", "--home", home} + if bsCfg.Cosmovisor { + cosmovisorBin, err := exec.LookPath("cosmovisor") + if err != nil { + fmt.Println("Warning: cosmovisor not found on PATH, falling back to terpd start") + } else { + startArgs = []string{"cosmovisor", "run", "start", "--home", home} + binary = cosmovisorBin + } + } + + // syscall.Exec replaces current process — clean for Docker PID 1 + return syscall.Exec(binary, startArgs, os.Environ()) +} + +// ──── State-sync configuration ──── + +func configureStateSyncBootstrap(cmtCfg *cmtcfg.Config, bsCfg BootstrapConfig) error { + rpcs := splitTrimmed(bsCfg.StateSyncRPCs, ",") + if len(rpcs) == 0 { + return fmt.Errorf("no statesync-rpcs configured") + } + + var lastErr error + for attempt := 0; attempt < bsCfg.MaxRetries; attempt++ { + rpc := rpcs[attempt%len(rpcs)] + fmt.Printf("Trying RPC %d/%d: %s\n", attempt+1, bsCfg.MaxRetries, rpc) + + trustHeight, trustHash, err := fetchStateSyncInfo(rpc, bsCfg.TrustOffset) + if err != nil { + fmt.Printf(" Failed: %v\n", err) + lastErr = err + continue + } + + fmt.Printf(" Trust height: %d\n", trustHeight) + fmt.Printf(" Trust hash : %s\n", trustHash) + + cmtCfg.StateSync.Enable = true + // CometBFT requires >=2 RPC servers; use two distinct ones when possible + if len(rpcs) >= 2 { + cmtCfg.StateSync.RPCServers = []string{rpcs[0], rpcs[1]} + } else { + cmtCfg.StateSync.RPCServers = []string{rpc, rpc} + } + cmtCfg.StateSync.TrustHeight = trustHeight + cmtCfg.StateSync.TrustHash = trustHash + cmtCfg.StateSync.TrustPeriod = 168 * time.Hour + + fmt.Println("State-sync configured.") + return nil + } + + return fmt.Errorf("state-sync config failed after %d attempts: %w", bsCfg.MaxRetries, lastErr) +} + +// // ──── Snapshot configuration ──── +// func configureSnapshotBootstrap(home string, cmtCfg *cmtcfg.Config, bsCfg BootstrapConfig) error { +// dataDir := filepath.Join(home, "data") +// rpcs := splitTrimmed(bsCfg.StateSyncRPCs, ",") +// cmtCfg.StateSync.Enable = false + +// if bsCfg.SnapshotURL == "" { +// // No URL — check if data dir already has content (SFTP delivery / pre-populated). +// if entries, err := os.ReadDir(dataDir); err == nil && len(entries) > 0 { +// fmt.Println("Snapshot data already present (SFTP/pre-populated). Skipping download.") +// } else { +// fmt.Println("No snapshot URL and no pre-existing data. Node will sync from genesis via block-sync.") +// } +// } else if isLocalPath(bsCfg.SnapshotURL) { +// // Local file path — extract directly without downloading. +// localPath := strings.TrimPrefix(bsCfg.SnapshotURL, "file://") +// fmt.Printf("Restoring snapshot from local file: %s\n", localPath) +// if _, err := os.Stat(localPath); err != nil { +// return fmt.Errorf("snapshot file not found: %s", localPath) +// } +// inputFmt := detectFormat(localPath) +// if err := extractToHome(localPath, inputFmt, home); err != nil { +// return fmt.Errorf("snapshot restore failed: %w", err) +// } +// fmt.Println("Snapshot restored from local file.") +// } else { +// fmt.Printf("Downloading snapshot from %s\n", bsCfg.SnapshotURL) +// if err := downloadAndExtractTarball(bsCfg.SnapshotURL, dataDir); err != nil { +// return fmt.Errorf("snapshot restore failed: %w", err) +// } +// fmt.Println("Snapshot extracted.") +// } + +// // Auto-detect sentry snapshot: if blockstore.db or state.db are missing +// // but application.db exists, run bootstrap-state to reconstruct CometBFT +// // state via light client verification. +// appDB := filepath.Join(dataDir, "application.db") +// blockDB := filepath.Join(dataDir, "blockstore.db") +// stateDB := filepath.Join(dataDir, "state.db") + +// hasApp := fileExists(appDB) +// hasBlock := fileExists(blockDB) +// hasState := fileExists(stateDB) + +// if hasApp && (!hasBlock || !hasState) { +// fmt.Println("Sentry snapshot detected (missing blockstore/state). Running bootstrap-state...") + +// if len(rpcs) == 0 || rpcs[0] == "" { +// return fmt.Errorf("bootstrap-state requires statesync-rpcs to verify light client headers") +// } + +// // Configure statesync RPC servers in config.toml for bootstrap-state +// if len(rpcs) >= 2 { +// cmtCfg.StateSync.RPCServers = []string{rpcs[0], rpcs[1]} +// } else { +// cmtCfg.StateSync.RPCServers = []string{rpcs[0], rpcs[0]} +// } + +// // Fetch trust height and hash for bootstrap-state +// trustHeight, trustHash, err := fetchStateSyncInfo(rpcs[0], bsCfg.TrustOffset) +// if err != nil { +// return fmt.Errorf("failed to fetch trust info for bootstrap-state: %w", err) +// } +// cmtCfg.StateSync.TrustHeight = trustHeight +// cmtCfg.StateSync.TrustHash = trustHash +// cmtCfg.StateSync.TrustPeriod = 24 * time.Hour + +// // Write config so bootstrap-state can read the RPC servers +// configTomlPath := filepath.Join(home, "config", "config.toml") +// cmtcfg.WriteConfigFile(configTomlPath, cmtCfg) + +// // Run bootstrap-state +// binary, err := os.Executable() +// if err != nil { +// return fmt.Errorf("failed to resolve executable: %w", err) +// } +// bsCmd := exec.Command(binary, "comet", "bootstrap-state", "--home", home) +// bsCmd.Stdout = os.Stdout +// bsCmd.Stderr = os.Stderr +// if err := bsCmd.Run(); err != nil { +// return fmt.Errorf("bootstrap-state failed: %w", err) +// } +// fmt.Println("CometBFT state bootstrapped via light client.") +// } + +// return nil +// } + +// isLocalPath returns true if the string is a local file path (not an HTTP URL). +func isLocalPath(s string) bool { + return strings.HasPrefix(s, "/") || strings.HasPrefix(s, "./") || + strings.HasPrefix(s, "file://") || strings.HasPrefix(s, "~/") +} + +// fileExists returns true if path exists and is a file or directory. +func fileExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +// ──── RPC helpers ──── + +// fetchStateSyncInfo queries an RPC for trust height, hash, and peer addresses. +func fetchStateSyncInfo(rpcAddr string, trustOffset int64) (int64, string, error) { + client, err := rpchttp.New(rpcAddr, "/websocket") + if err != nil { + return 0, "", err + } + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + status, err := client.Status(ctx) + if err != nil { + return 0, "", fmt.Errorf("status: %w", err) + } + + latestHeight := status.SyncInfo.LatestBlockHeight + trustHeight := latestHeight - trustOffset + if trustHeight < 1 { + trustHeight = 1 + } + + block, err := client.Block(ctx, &trustHeight) + if err != nil { + return 0, "", fmt.Errorf("block at %d: %w", trustHeight, err) + } + trustHash := hex.EncodeToString(block.BlockID.Hash) + + return trustHeight, trustHash, nil +} + +// ──── File / download helpers ──── + +func runInit(home, moniker, chainID string) error { + binary, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to resolve executable: %w", err) + } + initCmd := exec.Command(binary, "init", moniker, "--chain-id", chainID, "--home", home) + initCmd.Stdout = os.Stdout + initCmd.Stderr = os.Stderr + if err := initCmd.Run(); err != nil { + return fmt.Errorf("terpd init failed: %w", err) + } + return nil +} + +func downloadFile(url, destPath string) error { + resp, err := http.Get(url) //nolint:gosec // URL comes from operator config + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("HTTP %d from %s", resp.StatusCode, url) + } + + data, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + return os.WriteFile(destPath, data, 0o644) +} + +func validateFileHash(path, expectedHash string) error { + data, err := os.ReadFile(path) + if err != nil { + return err + } + h := sha256.Sum256(data) + actual := hex.EncodeToString(h[:]) + if actual != expectedHash { + return fmt.Errorf("hash mismatch: expected %s, got %s", expectedHash, actual) + } + return nil +} + +func downloadAndExtractTarball(url, destDir string) error { + resp, err := http.Get(url) //nolint:gosec // URL comes from operator config + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("HTTP %d from %s", resp.StatusCode, url) + } + + // Detect compression from URL extension and stream-decompress into tar. + // Supports: .tar.lz4, .tar.zst, .tar.gz, .tar + var tarCmd *exec.Cmd + switch { + case strings.HasSuffix(url, ".tar.lz4"): + // lz4 -dc | tar xf - -C dest + lz4Cmd := exec.Command("lz4", "-dc") + lz4Cmd.Stdin = resp.Body + tarCmd = exec.Command("tar", "xf", "-", "-C", destDir) + pipe, err := lz4Cmd.StdoutPipe() + if err != nil { + return fmt.Errorf("lz4 pipe: %w", err) + } + tarCmd.Stdin = pipe + lz4Cmd.Stderr = os.Stderr + tarCmd.Stdout = os.Stdout + tarCmd.Stderr = os.Stderr + if err := lz4Cmd.Start(); err != nil { + return fmt.Errorf("lz4 start: %w", err) + } + if err := tarCmd.Run(); err != nil { + _ = lz4Cmd.Wait() + return fmt.Errorf("tar extract (lz4): %w", err) + } + return lz4Cmd.Wait() + case strings.HasSuffix(url, ".tar.zst"): + zstdCmd := exec.Command("zstd", "-dc") + zstdCmd.Stdin = resp.Body + tarCmd = exec.Command("tar", "xf", "-", "-C", destDir) + pipe, err := zstdCmd.StdoutPipe() + if err != nil { + return fmt.Errorf("zstd pipe: %w", err) + } + tarCmd.Stdin = pipe + zstdCmd.Stderr = os.Stderr + tarCmd.Stdout = os.Stdout + tarCmd.Stderr = os.Stderr + if err := zstdCmd.Start(); err != nil { + return fmt.Errorf("zstd start: %w", err) + } + if err := tarCmd.Run(); err != nil { + _ = zstdCmd.Wait() + return fmt.Errorf("tar extract (zstd): %w", err) + } + return zstdCmd.Wait() + case strings.HasSuffix(url, ".tar.gz") || strings.HasSuffix(url, ".tgz"): + tarCmd = exec.Command("tar", "-xzf", "-", "-C", destDir) + default: + // Plain tar + tarCmd = exec.Command("tar", "xf", "-", "-C", destDir) + } + tarCmd.Stdin = resp.Body + tarCmd.Stdout = os.Stdout + tarCmd.Stderr = os.Stderr + return tarCmd.Run() +} + +func loadCometConfig(home string) (*cmtcfg.Config, error) { + cfg := cmtcfg.DefaultConfig() + cfg.SetRoot(home) + + configFile := filepath.Join(home, "config", "config.toml") + if _, err := os.Stat(configFile); err != nil { + return cfg, nil // return defaults if config.toml doesn't exist yet + } + + v := viper.New() + v.SetConfigFile(configFile) + if err := v.ReadInConfig(); err != nil { + return nil, fmt.Errorf("reading config.toml: %w", err) + } + if err := v.Unmarshal(cfg); err != nil { + return nil, fmt.Errorf("parsing config.toml: %w", err) + } + return cfg, nil +} + +// ──── P2P mode helpers ──── + +// applyPrivateMode locks down P2P so the node only connects to configured +// persistent peers, rejects all inbound connections, and never gossips. +// Ideal for pulling a local state-sync without participating in the network. +func applyPrivateMode(cfg *cmtcfg.Config) { + cfg.P2P.PexReactor = false // no peer exchange gossip + cfg.P2P.MaxNumInboundPeers = 0 // reject all inbound connections + cfg.P2P.MaxNumOutboundPeers = 10 // only our configured peers + cfg.P2P.AddrBookStrict = false // allow non-routable addrs (local testing) + cfg.P2P.Seeds = "" // no seeds — only persistent peers + cfg.Mempool.Broadcast = false // don't broadcast txs to peers +} + +// ──── Misc helpers ──── + +func applyBootstrapFlagOverrides(cmd *cobra.Command, bsCfg *BootstrapConfig) { + if v, _ := cmd.Flags().GetString("sync-mode"); v != "" { + bsCfg.SyncMode = v + } + if v, _ := cmd.Flags().GetString("genesis-url"); v != "" { + bsCfg.GenesisURL = v + } + if v, _ := cmd.Flags().GetString("genesis-hash"); v != "" { + bsCfg.GenesisHash = v + } + if v, _ := cmd.Flags().GetString("snapshot-url"); v != "" { + bsCfg.SnapshotURL = v + } + if v, _ := cmd.Flags().GetString("statesync-rpcs"); v != "" { + bsCfg.StateSyncRPCs = v + } + if v, _ := cmd.Flags().GetInt64("trust-offset"); v > 0 { + bsCfg.TrustOffset = v + } + if v, _ := cmd.Flags().GetInt("max-retries"); v > 0 { + bsCfg.MaxRetries = v + } + if v, _ := cmd.Flags().GetString("bootstrap-seeds"); v != "" { + bsCfg.Seeds = v + } + if v, _ := cmd.Flags().GetString("bootstrap-peers"); v != "" { + bsCfg.PersistentPeers = v + } + if v, _ := cmd.Flags().GetBool("cosmovisor"); v { + bsCfg.Cosmovisor = true + } + if v, _ := cmd.Flags().GetBool("service"); v { + bsCfg.Service = true + } + if v, _ := cmd.Flags().GetString("pruning"); v != "" { + bsCfg.Pruning = v + } +} + +// ──── Pruning configuration ──── + +func applyPruningConfig(home, pruning string) error { + switch pruning { + case "default", "nothing", "everything": + default: + return fmt.Errorf("unknown pruning strategy %q (use: default, nothing, everything)", pruning) + } + + appTomlPath := filepath.Join(home, "config", "app.toml") + data, err := os.ReadFile(appTomlPath) + if err != nil { + return fmt.Errorf("failed to read app.toml: %w", err) + } + + content := string(data) + // Replace the pruning line in app.toml + lines := strings.Split(content, "\n") + for i, line := range lines { + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(trimmed, "pruning =") { + lines[i] = fmt.Sprintf("pruning = %q", pruning) + break + } + } + + if err := os.WriteFile(appTomlPath, []byte(strings.Join(lines, "\n")), 0o644); err != nil { + return fmt.Errorf("failed to write app.toml: %w", err) + } + fmt.Printf("Pruning strategy set to %q in app.toml\n", pruning) + return nil +} + +// ──── Cosmovisor installation ──── + +func installCosmovisor(terpdBinary, home string) error { + // Check if go is available + goPath, err := exec.LookPath("go") + if err != nil { + return fmt.Errorf("cosmovisor requires Go on PATH: %w", err) + } + fmt.Printf("Found Go at %s\n", goPath) + + // Install cosmovisor + fmt.Println("Installing cosmovisor...") + installCmd := exec.Command(goPath, "install", "cosmossdk.io/tools/cosmovisor/cmd/cosmovisor@latest") + installCmd.Stdout = os.Stdout + installCmd.Stderr = os.Stderr + installCmd.Env = append(os.Environ(), + "DAEMON_NAME=terpd", + fmt.Sprintf("DAEMON_HOME=%s", home), + ) + if err := installCmd.Run(); err != nil { + return fmt.Errorf("cosmovisor install failed: %w", err) + } + + // Initialize cosmovisor + fmt.Println("Initializing cosmovisor...") + cosmovisorBin, err := exec.LookPath("cosmovisor") + if err != nil { + return fmt.Errorf("cosmovisor not found after install: %w", err) + } + + initCmd := exec.Command(cosmovisorBin, "init", terpdBinary) + initCmd.Stdout = os.Stdout + initCmd.Stderr = os.Stderr + initCmd.Env = append(os.Environ(), + "DAEMON_NAME=terpd", + fmt.Sprintf("DAEMON_HOME=%s", home), + ) + if err := initCmd.Run(); err != nil { + return fmt.Errorf("cosmovisor init failed: %w", err) + } + + fmt.Println("Cosmovisor installed and initialized.") + return nil +} + +// ──── Systemd service creation ──── + +func createSystemdService(home string, cosmovisor bool) error { + if runtime.GOOS != "linux" { + fmt.Println("Warning: --service is only supported on Linux, skipping systemd setup.") + return nil + } + + currentUser := os.Getenv("USER") + if currentUser == "" { + currentUser = "root" + } + + var execStart, description string + if cosmovisor { + gopath := os.Getenv("GOPATH") + if gopath == "" { + gopath = filepath.Join(os.Getenv("HOME"), "go") + } + execStart = filepath.Join(gopath, "bin", "cosmovisor") + " run start --home " + home + description = "Terp Node (cosmovisor)" + } else { + binary, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to resolve executable path: %w", err) + } + execStart = binary + " start --home " + home + description = "Terp Node" + } + + unit := fmt.Sprintf(`[Unit] +Description=%s +After=network-online.target +Wants=network-online.target + +[Service] +User=%s +ExecStart=%s +Restart=always +RestartSec=3 +LimitNOFILE=65535 +Environment="DAEMON_NAME=terpd" +Environment="DAEMON_HOME=%s" +Environment="DAEMON_ALLOW_DOWNLOAD_BINARIES=false" +Environment="DAEMON_RESTART_AFTER_UPGRADE=true" + +[Install] +WantedBy=multi-user.target +`, description, currentUser, execStart, home) + + servicePath := "/etc/systemd/system/terpd.service" + if err := os.WriteFile(servicePath, []byte(unit), 0o644); err != nil { + return fmt.Errorf("failed to write systemd service (try running as root): %w", err) + } + + fmt.Printf("Systemd service created at %s\n", servicePath) + fmt.Println("Enable with: sudo systemctl enable terpd && sudo systemctl start terpd") + return nil +} + +func splitTrimmed(s, sep string) []string { + parts := strings.Split(s, sep) + var result []string + for _, p := range parts { + p = strings.TrimSpace(p) + if p != "" { + result = append(result, p) + } + } + return result +} diff --git a/cmd/terpd/cmd/bootstrap_ict_test.go b/cmd/terpd/cmd/bootstrap_ict_test.go new file mode 100644 index 0000000..4825c69 --- /dev/null +++ b/cmd/terpd/cmd/bootstrap_ict_test.go @@ -0,0 +1,164 @@ +//go:build ict + +package cmd + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + cmtcfg "github.com/cometbft/cometbft/config" + rpchttp "github.com/cometbft/cometbft/rpc/client/http" + "github.com/stretchr/testify/require" +) + +// TestProductionRPCConnectivity verifies we can reach the production RPC, +// fetch trust info, and discover peers. Quick sanity check. +func TestProductionRPCConnectivity(t *testing.T) { + bsCfg := DefaultBootstrapConfig() + rpcs := splitTrimmed(bsCfg.StateSyncRPCs, ",") + require.NotEmpty(t, rpcs, "no statesync RPCs configured in defaults") + + trustHeight, trustHash, peers, err := fetchStateSyncInfo(rpcs[0], bsCfg.TrustOffset) + require.NoError(t, err, "failed to connect to production RPC %s", rpcs[0]) + require.True(t, trustHeight > 0, "trust height must be positive") + require.NotEmpty(t, trustHash, "trust hash must not be empty") + require.NotEmpty(t, peers, "must discover at least one peer") + + t.Logf("RPC : %s", rpcs[0]) + t.Logf("Trust height : %d", trustHeight) + t.Logf("Trust hash : %s", trustHash) + t.Logf("Peers found : %d", len(peers)) +} + +// TestBootstrapPrivateModeConfig verifies that private mode correctly locks +// down the CometBFT P2P config. +func TestBootstrapPrivateModeConfig(t *testing.T) { + cfg := cmtcfg.DefaultConfig() + + // Defaults should have PEX on + require.True(t, cfg.P2P.PexReactor) + require.True(t, cfg.Mempool.Broadcast) + require.True(t, cfg.P2P.MaxNumInboundPeers > 0) + + applyPrivateMode(cfg) + + require.False(t, cfg.P2P.PexReactor, "PEX should be disabled") + require.Equal(t, 0, cfg.P2P.MaxNumInboundPeers, "inbound peers should be 0") + require.False(t, cfg.P2P.AddrBookStrict, "addr book strict should be off") + require.Empty(t, cfg.P2P.Seeds, "seeds should be empty") + require.False(t, cfg.Mempool.Broadcast, "mempool broadcast should be off") +} + +// TestBootstrapMainnetSync is the full integration test: builds the binary, +// bootstraps a private node against mainnet, and verifies state-sync completes. +// +// Run with: +// +// go test -tags ict -run TestBootstrapMainnetSync -timeout 10m -v ./cmd/terpd/cmd/ +func TestBootstrapMainnetSync(t *testing.T) { + if testing.Short() { + t.Skip("skipping mainnet sync ICT in short mode") + } + + const ( + rpcPort = "46657" + p2pPort = "46656" + rpcAddr = "http://127.0.0.1:" + rpcPort + ) + + // ──── Phase 1: Build binary ──── + t.Log("Phase 1: Building terpd binary...") + projectRoot, err := filepath.Abs(filepath.Join("..", "..", "..")) + require.NoError(t, err) + + binPath := filepath.Join(t.TempDir(), "terpd-ict") + buildCmd := exec.Command("go", "build", "-o", binPath, "./cmd/terpd") + buildCmd.Dir = projectRoot + out, err := buildCmd.CombinedOutput() + require.NoError(t, err, "build failed:\n%s", string(out)) + t.Logf(" Binary: %s", binPath) + + // ──── Phase 2: Init + custom ports ──── + t.Log("Phase 2: Initializing node with custom ports...") + tmpHome := t.TempDir() + + initProc := exec.Command(binPath, "init", "ict-test", "--chain-id", "morocco-1", "--home", tmpHome) + out, err = initProc.CombinedOutput() + require.NoError(t, err, "init failed:\n%s", string(out)) + + // Set custom ports so we don't conflict with any running node + cfg, err := loadCometConfig(tmpHome) + require.NoError(t, err) + cfg.P2P.ListenAddress = "tcp://0.0.0.0:" + p2pPort + cfg.RPC.ListenAddress = "tcp://127.0.0.1:" + rpcPort + cmtcfg.WriteConfigFile(filepath.Join(tmpHome, "config", "config.toml"), cfg) + t.Logf(" Home: %s (P2P=%s, RPC=%s)", tmpHome, p2pPort, rpcPort) + + // ──── Phase 3: Run bootstrap ──── + t.Log("Phase 3: Running bootstrap (private mode, state-sync to mainnet)...") + + ctx, cancel := context.WithTimeout(context.Background(), 9*time.Minute) + defer cancel() + + bootstrapProc := exec.CommandContext(ctx, binPath, "bootstrap", + "--home", tmpHome, + "--chain-id", "morocco-1", + "--moniker", "ict-test", + ) + bootstrapProc.Stdout = os.Stdout + bootstrapProc.Stderr = os.Stderr + require.NoError(t, bootstrapProc.Start(), "failed to start bootstrap") + + // Track early exits + procDone := make(chan error, 1) + go func() { + procDone <- bootstrapProc.Wait() + }() + defer func() { + if bootstrapProc.Process != nil { + bootstrapProc.Process.Kill() + } + <-procDone + }() + + // ──── Phase 4: Monitor sync ──── + t.Log("Phase 4: Waiting for state-sync (polling every 10s)...") + time.Sleep(20 * time.Second) // let node boot + + rpcClient, err := rpchttp.New(rpcAddr, "/websocket") + require.NoError(t, err) + + deadline := time.After(8 * time.Minute) + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + + for { + select { + case err := <-procDone: + t.Fatalf("bootstrap process exited unexpectedly: %v", err) + + case <-ticker.C: + status, err := rpcClient.Status(context.Background()) + if err != nil { + t.Log(" Node starting up...") + continue + } + h := status.SyncInfo.LatestBlockHeight + if h > 0 { + t.Logf(" SYNCED at height %d", h) + t.Logf(" App hash : %X", status.SyncInfo.LatestAppHash) + t.Logf(" Network : %s", status.NodeInfo.Network) + t.Log("SUCCESS: Bootstrap state-sync to mainnet completed!") + return + } + t.Log(" State-sync in progress...") + + case <-deadline: + t.Fatal("state-sync did not complete within 8 minutes — peers may not have snapshots available") + } + } +} diff --git a/cmd/terpd/cmd/config.go b/cmd/terpd/cmd/config.go index 34e5f86..dca4060 100644 --- a/cmd/terpd/cmd/config.go +++ b/cmd/terpd/cmd/config.go @@ -27,7 +27,6 @@ type TerpCustomClient struct { Fees string `mapstructure:"fees" json:"fees"` FeeGranter string `mapstructure:"fee-granter" json:"fee-granter"` FeePayer string `mapstructure:"fee-payer" json:"fee-payer"` - Note string `mapstructure:"note" json:"note"` } diff --git a/cmd/terpd/cmd/root.go b/cmd/terpd/cmd/root.go index 8eae54c..ca58e53 100644 --- a/cmd/terpd/cmd/root.go +++ b/cmd/terpd/cmd/root.go @@ -26,6 +26,7 @@ import ( "github.com/cometbft/cometbft/crypto" "github.com/cometbft/cometbft/libs/bytes" tmcli "github.com/cometbft/cometbft/libs/cli" + "github.com/cosmos/cosmos-sdk/client/snapshot" simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" "github.com/cosmos/cosmos-sdk/types/module" @@ -43,7 +44,6 @@ import ( "github.com/cosmos/cosmos-sdk/version" authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - "github.com/cosmos/cosmos-sdk/x/crisis" genutilcli "github.com/cosmos/cosmos-sdk/x/genutil/client/cli" "github.com/terpnetwork/terp-core/v5/app" @@ -150,7 +150,8 @@ func initAppConfig() (string, interface{}) { type CustomAppConfig struct { serverconfig.Config - Wasm wasmtypes.NodeConfig `mapstructure:"wasm"` + Wasm wasmtypes.NodeConfig `mapstructure:"wasm"` + Bootstrap BootstrapConfig `mapstructure:"bootstrap"` // SidecarQueryServerConfig sqs.Config `mapstructure:"terp-sqs"` // IndexerConfig indexer.Config `mapstructure:"terp-indexer"` @@ -175,12 +176,14 @@ func initAppConfig() (string, interface{}) { // srvCfg.BaseConfig.IAVLDisableFastNode = true // disable fastnode by default terpAppConfig := CustomAppConfig{ - Config: *srvCfg, - Wasm: wasmtypes.DefaultNodeConfig(), + Config: *srvCfg, + Wasm: wasmtypes.DefaultNodeConfig(), + Bootstrap: DefaultBootstrapConfig(), } customAppTemplate := serverconfig.DefaultConfigTemplate + - wasmtypes.DefaultConfigTemplate() + wasmtypes.DefaultConfigTemplate() + + BootstrapConfigTemplate return customAppTemplate, terpAppConfig } @@ -235,9 +238,12 @@ func initRootCmd(rootCmd *cobra.Command, encodingConfig params.EncodingConfig) { genutilcli.InitCmd(app.ModuleBasics, app.DefaultNodeHome), AddGenesisIcaCmd(app.DefaultNodeHome), tmcli.NewCompletionCmd(rootCmd, true), + StatesyncCmd, + BootstrapCmd, + snapshot.Cmd(ac.newApp), + pruning.Cmd(ac.newApp, app.DefaultNodeHome), DebugCmd(), ConfigCmd(), - pruning.Cmd(ac.newApp, app.DefaultNodeHome), ) server.AddTestnetCreatorCommand(rootCmd, ac.newTestnetApp, addModuleInitFlags) @@ -252,12 +258,9 @@ func initRootCmd(rootCmd *cobra.Command, encodingConfig params.EncodingConfig) { txCommand(), keys.Commands(), ) - // add rosetta - // rootCmd.AddCommand(rosettaCmd.RosettaCommand(encodingConfig.InterfaceRegistry, encodingConfig.Marshaler)) } func addModuleInitFlags(startCmd *cobra.Command) { - crisis.AddModuleInitFlags(startCmd) wasm.AddModuleInitFlags(startCmd) } diff --git a/cmd/terpd/cmd/snapshot_ict_test.go b/cmd/terpd/cmd/snapshot_ict_test.go new file mode 100644 index 0000000..043e799 --- /dev/null +++ b/cmd/terpd/cmd/snapshot_ict_test.go @@ -0,0 +1,298 @@ +//go:build ict + +package cmd + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" + + rpchttp "github.com/cometbft/cometbft/rpc/client/http" + "github.com/stretchr/testify/require" +) + +// TestSnapshotExtractAndRestore is a fully self-contained integration test. +// It creates a local single-validator chain (no external network dependency), +// validates the snapshot extraction/restore cycle, and verifies a second node +// can bootstrap from the snapshot and peer with the original. +// +// The snapshot command is network-agnostic — it operates on any terpd home +// directory regardless of chain-id, denom, or network configuration. This test +// uses a local throwaway chain to prove that. +// +// Run with: +// +// go test -tags ict -run TestSnapshotExtractAndRestore -timeout 5m -v ./cmd/terpd/cmd/ +func TestSnapshotExtractAndRestore(t *testing.T) { + if testing.Short() { + t.Skip("skipping snapshot ICT in short mode") + } + + // ─── Test chain parameters (local throwaway — no external deps) ───── + const ( + chainID = "snapshot-ict-1" + moniker = "snap-validator" + bondDenom = "uthiol" // binary's registered staking denom + bondAmount = "50000000" + bondDenom + genesisAmt = "100000000" + bondDenom + + rpcPortA = "56657" + p2pPortA = "56656" + rpcPortB = "57657" + p2pPortB = "57656" + rpcAddrA = "http://127.0.0.1:" + rpcPortA + rpcAddrB = "http://127.0.0.1:" + rpcPortB + ) + + // ─── Phase 1: Build binary ────────────────────────────────────────── + t.Log("Phase 1: Building terpd binary...") + projectRoot, err := filepath.Abs(filepath.Join("..", "..", "..")) + require.NoError(t, err) + + binPath := filepath.Join(t.TempDir(), "terpd-snapshot-ict") + buildCmd := exec.Command("go", "build", "-o", binPath, "./cmd/terpd") + buildCmd.Dir = projectRoot + out, err := buildCmd.CombinedOutput() + require.NoError(t, err, "build failed:\n%s", string(out)) + t.Logf(" Binary: %s", binPath) + + // ─── Phase 2: Init local chain (node A) ───────────────────────────── + t.Log("Phase 2: Creating local chain (node A)...") + homeA := t.TempDir() + + run(t, binPath, "init", moniker, "--chain-id", chainID, "--home", homeA) + run(t, binPath, "keys", "add", "validator", "--keyring-backend", "test", "--home", homeA) + + valAddr := strings.TrimSpace( + runOutput(t, binPath, "keys", "show", "validator", "-a", "--keyring-backend", "test", "--home", homeA), + ) + t.Logf(" Validator: %s", valAddr) + + run(t, binPath, "genesis", "add-genesis-account", valAddr, genesisAmt, + "--keyring-backend", "test", "--home", homeA) + run(t, binPath, "genesis", "gentx", "validator", bondAmount, + "--chain-id", chainID, "--keyring-backend", "test", "--home", homeA) + run(t, binPath, "genesis", "collect-gentxs", "--home", homeA) + + // Patch ports so we don't collide with anything + patchFile(t, filepath.Join(homeA, "config", "config.toml"), map[string]string{ + "laddr = \"tcp://0.0.0.0:26656\"": fmt.Sprintf("laddr = \"tcp://0.0.0.0:%s\"", p2pPortA), + "laddr = \"tcp://127.0.0.1:26657\"": fmt.Sprintf("laddr = \"tcp://127.0.0.1:%s\"", rpcPortA), + }) + patchTomlValue(t, filepath.Join(homeA, "config", "app.toml"), "pruning", "default") + + // ─── Phase 3: Start node A ────────────────────────────────────────── + t.Log("Phase 3: Starting node A...") + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + nodeA := exec.CommandContext(ctx, binPath, "start", "--home", homeA) + nodeA.Stdout = os.Stdout + nodeA.Stderr = os.Stderr + require.NoError(t, nodeA.Start()) + defer func() { cancel(); nodeA.Wait() }() + + // Wait for blocks + waitForHeight(t, rpcAddrA, 5, 60*time.Second) + + rpcA, err := rpchttp.New(rpcAddrA, "/websocket") + require.NoError(t, err) + statusA, err := rpcA.Status(context.Background()) + require.NoError(t, err) + heightBefore := statusA.SyncInfo.LatestBlockHeight + nodeIDA := string(statusA.NodeInfo.DefaultNodeID) + peerA := fmt.Sprintf("%s@127.0.0.1:%s", nodeIDA, p2pPortA) + t.Logf(" Node A: height=%d peer=%s", heightBefore, peerA) + + // ─── Phase 4: Extract snapshot ────────────────────────────────────── + t.Log("Phase 4: terpd snapshot (freeze → extract → resume)...") + snapshotFile := filepath.Join(t.TempDir(), "snapshot.tar.lz4") + + snapOut, err := exec.Command(binPath, "snapshot", + "--home", homeA, + "-o", snapshotFile, + ).CombinedOutput() + require.NoError(t, err, "snapshot failed:\n%s", string(snapOut)) + + info, err := os.Stat(snapshotFile) + require.NoError(t, err) + require.True(t, info.Size() > 1024, "snapshot too small: %d bytes", info.Size()) + t.Logf(" Snapshot: %.2f MB", float64(info.Size())/(1024*1024)) + + // ─── Phase 5: Verify node A resumed ───────────────────────────────── + t.Log("Phase 5: Verifying node A resumed...") + waitForHeight(t, rpcAddrA, heightBefore+2, 30*time.Second) + statusA2, _ := rpcA.Status(context.Background()) + t.Logf(" Node A: height=%d (resumed from %d)", statusA2.SyncInfo.LatestBlockHeight, heightBefore) + + // ─── Phase 6: Restore snapshot to node B ──────────────────────────── + t.Log("Phase 6: Bootstrapping node B from snapshot...") + homeB := t.TempDir() + + // Init fresh node B (generates its own identity) + run(t, binPath, "init", "node-b", "--chain-id", chainID, "--home", homeB) + + // Copy genesis (same chain, different node) + copyFile(t, + filepath.Join(homeA, "config", "genesis.json"), + filepath.Join(homeB, "config", "genesis.json"), + ) + + // Extract snapshot into B (data/ + wasm/ only — never overwrites config/) + extractOut, err := exec.Command("sh", "-c", + fmt.Sprintf("lz4 -dc %s | tar xf - -C %s", snapshotFile, homeB), + ).CombinedOutput() + require.NoError(t, err, "extract failed:\n%s", string(extractOut)) + + // Verify data dir populated, config untouched (B keeps its own identity) + _, err = os.Stat(filepath.Join(homeB, "data", "application.db")) + if err != nil { + // Might be in a subdirectory depending on DB backend + _, err = os.Stat(filepath.Join(homeB, "data")) + require.NoError(t, err, "data/ missing after extraction") + } + + // Node B's node_key should be different from A (snapshot didn't overwrite) + nodeKeyA, _ := os.ReadFile(filepath.Join(homeA, "config", "node_key.json")) + nodeKeyB, _ := os.ReadFile(filepath.Join(homeB, "config", "node_key.json")) + require.NotEqual(t, string(nodeKeyA), string(nodeKeyB), + "node B has same node_key as A — snapshot overwrote config/!") + + // Patch B's ports + peer with A + patchFile(t, filepath.Join(homeB, "config", "config.toml"), map[string]string{ + "laddr = \"tcp://0.0.0.0:26656\"": fmt.Sprintf("laddr = \"tcp://0.0.0.0:%s\"", p2pPortB), + "laddr = \"tcp://127.0.0.1:26657\"": fmt.Sprintf("laddr = \"tcp://127.0.0.1:%s\"", rpcPortB), + "persistent_peers = \"\"": fmt.Sprintf("persistent_peers = \"%s\"", peerA), + }) + + // ─── Phase 7: Start node B, verify it catches up ──────────────────── + t.Log("Phase 7: Starting node B...") + ctxB, cancelB := context.WithCancel(context.Background()) + defer cancelB() + + nodeB := exec.CommandContext(ctxB, binPath, "start", "--home", homeB) + nodeB.Stdout = os.Stdout + nodeB.Stderr = os.Stderr + require.NoError(t, nodeB.Start()) + defer func() { cancelB(); nodeB.Wait() }() + + waitForHeight(t, rpcAddrB, heightBefore+1, 60*time.Second) + + rpcB, err := rpchttp.New(rpcAddrB, "/websocket") + require.NoError(t, err) + statusB, _ := rpcB.Status(context.Background()) + t.Logf(" Node B: height=%d catching_up=%v", statusB.SyncInfo.LatestBlockHeight, statusB.SyncInfo.CatchingUp) + + // ─── Phase 8: Verify chunk splitting works ────────────────────────── + t.Log("Phase 8: Testing snapshot chunking...") + chunkFile := filepath.Join(t.TempDir(), "chunk-test.tar.lz4") + // Copy the snapshot for chunk test + copyFile(t, snapshotFile, chunkFile) + + splitOut, err := exec.Command("split", "-b", "500K", chunkFile, chunkFile+".part-").CombinedOutput() + if err != nil { + t.Logf(" split not available: %s", string(splitOut)) + } else { + chunks, _ := filepath.Glob(chunkFile + ".part-*") + require.True(t, len(chunks) > 0, "no chunks created") + t.Logf(" Split into %d chunks", len(chunks)) + + // Verify we can reassemble + reassembled := filepath.Join(t.TempDir(), "reassembled.tar.lz4") + catCmd := fmt.Sprintf("cat %s.part-* > %s", chunkFile, reassembled) + _, err = exec.Command("sh", "-c", catCmd).CombinedOutput() + require.NoError(t, err) + + origInfo, _ := os.Stat(chunkFile) + reassInfo, _ := os.Stat(reassembled) + require.Equal(t, origInfo.Size(), reassInfo.Size(), "reassembled size mismatch") + t.Log(" Reassembly verified — sizes match") + } + + t.Log("SUCCESS: Snapshot extract → restore → peer sync → chunking verified!") +} + +// ─── Test helpers ──────────────────────────────────────────────────────── + +func run(t *testing.T, name string, args ...string) { + t.Helper() + cmd := exec.Command(name, args...) + out, err := cmd.CombinedOutput() + require.NoError(t, err, "%s %s failed:\n%s", name, strings.Join(args, " "), string(out)) +} + +func runOutput(t *testing.T, name string, args ...string) string { + t.Helper() + cmd := exec.Command(name, args...) + out, err := cmd.CombinedOutput() + require.NoError(t, err, "%s %s failed:\n%s", name, strings.Join(args, " "), string(out)) + return string(out) +} + +func waitForHeight(t *testing.T, rpcAddr string, target int64, timeout time.Duration) { + t.Helper() + deadline := time.After(timeout) + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + time.Sleep(3 * time.Second) // initial boot delay + + for { + select { + case <-deadline: + t.Fatalf("node at %s did not reach height %d within %s", rpcAddr, target, timeout) + case <-ticker.C: + client, err := rpchttp.New(rpcAddr, "/websocket") + if err != nil { + continue + } + status, err := client.Status(context.Background()) + if err != nil { + continue + } + if status.SyncInfo.LatestBlockHeight >= target { + return + } + } + } +} + +func patchFile(t *testing.T, path string, replacements map[string]string) { + t.Helper() + data, err := os.ReadFile(path) + require.NoError(t, err) + content := string(data) + for old, new := range replacements { + content = strings.Replace(content, old, new, 1) + } + require.NoError(t, os.WriteFile(path, []byte(content), 0644)) +} + +func patchTomlValue(t *testing.T, path string, key string, value string) { + t.Helper() + data, err := os.ReadFile(path) + require.NoError(t, err) + lines := strings.Split(string(data), "\n") + for i, line := range lines { + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(trimmed, "#") { + continue + } + if strings.HasPrefix(trimmed, key+" ") || strings.HasPrefix(trimmed, key+"=") { + lines[i] = fmt.Sprintf("%s = \"%s\"", key, value) + break + } + } + require.NoError(t, os.WriteFile(path, []byte(strings.Join(lines, "\n")), 0644)) +} + +func copyFile(t *testing.T, src, dst string) { + t.Helper() + data, err := os.ReadFile(src) + require.NoError(t, err) + require.NoError(t, os.WriteFile(dst, data, 0644)) +} diff --git a/cmd/terpd/cmd/statesync.go b/cmd/terpd/cmd/statesync.go new file mode 100644 index 0000000..7f73fad --- /dev/null +++ b/cmd/terpd/cmd/statesync.go @@ -0,0 +1,607 @@ +package cmd + +import ( + "context" + "encoding/hex" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + "cosmossdk.io/log" + snapshots "cosmossdk.io/store/snapshots" + snapshottypes "cosmossdk.io/store/snapshots/types" + + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + + abci "github.com/cometbft/cometbft/abci/types" + cmtcfg "github.com/cometbft/cometbft/config" + cmtlog "github.com/cometbft/cometbft/libs/log" + cmtnode "github.com/cometbft/cometbft/node" + "github.com/cometbft/cometbft/p2p" + pvm "github.com/cometbft/cometbft/privval" + "github.com/cometbft/cometbft/proxy" + rpchttp "github.com/cometbft/cometbft/rpc/client/http" + + dbm "github.com/cosmos/cosmos-db" + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/server" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + + "github.com/spf13/cobra" + + "github.com/terpnetwork/terp-core/v5/app" +) + +// StatesyncCmd provides tools to debug and test state-sync snapshots +var StatesyncCmd = &cobra.Command{ + Use: "statesync", + Short: "Debug and test state-sync snapshots (list, info, query, test, fetch)", + Long: `Advanced debugging tool for state-sync snapshots. + +Subcommands: + list List all local snapshots with metadata + info Show detailed info about a snapshot (auto-detects latest) + query Query snapshot metadata via ABCI ListSnapshots (lightweight) + test Dry-run full state-sync restore (OfferSnapshot + ApplySnapshotChunk) + fetch Fetch a snapshot from the production network via P2P state-sync`, + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Help() + }, +} + +func init() { + StatesyncCmd.PersistentFlags().String("home", app.DefaultNodeHome, "directory for config and data") + StatesyncCmd.PersistentFlags().String("db-backend", "goleveldb", "database backend (goleveldb recommended)") + + StatesyncCmd.AddCommand( + listSnapshotsCmd(), + infoSnapshotCmd(), + querySnapshotsCmd(), + testStateSyncCmd(), + fetchSnapshotCmd(), + ) +} + +// getSnapshotStore opens the real snapshot store from your node home. +// Returns the store and the underlying DB (caller should close the DB when done). +func getSnapshotStore(home string) (*snapshots.Store, dbm.DB, error) { + if home == "" { + home = app.DefaultNodeHome + } + + snapshotDir := filepath.Join(home, "data", "snapshots") + + snapshotDB, err := dbm.NewDB("metadata", dbm.GoLevelDBBackend, snapshotDir) + if err != nil { + return nil, nil, fmt.Errorf("failed to open snapshot metadata DB at %s: %w", snapshotDir, err) + } + + store, err := snapshots.NewStore(snapshotDB, snapshotDir) + if err != nil { + snapshotDB.Close() + return nil, nil, fmt.Errorf("failed to create snapshot store: %w", err) + } + + fmt.Printf("Snapshot store opened\n") + fmt.Printf(" Directory : %s\n", snapshotDir) + fmt.Printf(" Metadata : %s/metadata.db\n\n", snapshotDir) + + return store, snapshotDB, nil +} + +// ====================== LIST COMMAND ====================== +func listSnapshotsCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List all local state-sync snapshots", + RunE: func(cmd *cobra.Command, args []string) error { + home, _ := cmd.Flags().GetString("home") + + store, snapshotDB, err := getSnapshotStore(home) + if err != nil { + return err + } + defer snapshotDB.Close() + + snapList, err := store.List() + if err != nil { + return fmt.Errorf("failed to list snapshots: %w", err) + } + + if len(snapList) == 0 { + fmt.Println("No snapshots found.") + return nil + } + + for i, snapshot := range snapList { + fmt.Printf("Snapshot #%d\n", i+1) + fmt.Printf(" Height : %d\n", snapshot.Height) + fmt.Printf(" Format : %d\n", snapshot.Format) + fmt.Printf(" Chunks : %d\n", snapshot.Chunks) + fmt.Printf(" Hash : %X\n", snapshot.Hash) + if len(snapshot.Metadata.ChunkHashes) > 0 { + fmt.Printf(" ChunkHashes: %d entries\n", len(snapshot.Metadata.ChunkHashes)) + } + fmt.Println(" ---") + } + fmt.Printf("\nTotal snapshots found: %d\n", len(snapList)) + return nil + }, + } + return cmd +} + +// ====================== INFO COMMAND ====================== +func infoSnapshotCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "info", + Short: "Show detailed information for a snapshot (auto-detects latest if --height not set)", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + home, _ := cmd.Flags().GetString("home") + height, _ := cmd.Flags().GetUint64("height") + + store, snapshotDB, err := getSnapshotStore(home) + if err != nil { + return err + } + defer snapshotDB.Close() + + var snap *snapshottypes.Snapshot + if height == 0 { + snap, err = store.GetLatest() + if err != nil { + return fmt.Errorf("failed to get latest snapshot: %w", err) + } + if snap == nil { + return fmt.Errorf("no snapshots found; use 'statesync list' to verify") + } + fmt.Printf("Auto-detected latest snapshot at height %d\n\n", snap.Height) + } else { + snap, err = store.Get(height, snapshottypes.CurrentFormat) + if err != nil || snap == nil { + return fmt.Errorf("snapshot at height %d (format %d) not found", height, snapshottypes.CurrentFormat) + } + } + + fmt.Printf("Snapshot at height %d\n", snap.Height) + fmt.Printf(" Format : %d\n", snap.Format) + fmt.Printf(" Chunks : %d\n", snap.Chunks) + fmt.Printf(" Hash : %X\n", snap.Hash) + fmt.Printf(" ChunkHashes: %d entries\n", len(snap.Metadata.ChunkHashes)) + return nil + }, + } + cmd.Flags().Uint64("height", 0, "Snapshot height (0 = auto-detect latest)") + return cmd +} + +// ====================== QUERY (ABCI) COMMAND ====================== +func querySnapshotsCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "query", + Short: "Query snapshot metadata via ABCI ListSnapshots (lightweight, no full node)", + Long: `Calls the ABCI ListSnapshots method exactly as CometBFT peers do when +discovering available snapshots. This is the most lightweight way to verify +what snapshot data your node would advertise to the network. + +No modules are loaded — only the snapshot store is opened and a minimal +BaseApp is created to service the ABCI call.`, + RunE: func(cmd *cobra.Command, args []string) error { + home, _ := cmd.Flags().GetString("home") + + store, snapshotDB, err := getSnapshotStore(home) + if err != nil { + return err + } + defer snapshotDB.Close() + + // Minimal BaseApp — no modules, no full app state + memDB := dbm.NewMemDB() + ba := baseapp.NewBaseApp( + "terpd", + log.NewNopLogger(), + memDB, + nil, + baseapp.SetSnapshot(store, snapshottypes.NewSnapshotOptions(0, 0)), + ) + + resp, err := ba.ListSnapshots(&abci.RequestListSnapshots{}) + if err != nil { + return fmt.Errorf("ABCI ListSnapshots failed: %w", err) + } + + if len(resp.Snapshots) == 0 { + fmt.Println("ABCI ListSnapshots returned 0 snapshots.") + fmt.Println("CometBFT peers would see no snapshots from this node.") + return nil + } + + fmt.Printf("ABCI ListSnapshots: %d snapshot(s) available\n", len(resp.Snapshots)) + fmt.Println("(This is exactly what CometBFT peers see over P2P channel 0x60)") + + for i, s := range resp.Snapshots { + fmt.Printf("Snapshot #%d\n", i+1) + fmt.Printf(" Height : %d\n", s.Height) + fmt.Printf(" Format : %d\n", s.Format) + fmt.Printf(" Chunks : %d\n", s.Chunks) + fmt.Printf(" Hash : %X\n", s.Hash) + if len(s.Metadata) > 0 { + fmt.Printf(" Metadata : %d bytes\n", len(s.Metadata)) + } + fmt.Println(" ---") + } + return nil + }, + } + return cmd +} + +// ====================== TEST (DRY-RUN) COMMAND ====================== +func testStateSyncCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "test", + Short: "Dry-run full state-sync restore (Offer + Apply chunks) - validates app state", + Long: `Simulates exactly what a state-syncing node does. Great for sanity-testing your app. +Auto-detects the latest snapshot unless --height is specified.`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + home, _ := cmd.Flags().GetString("home") + height, _ := cmd.Flags().GetUint64("height") + + logger := log.NewLogger(cmd.OutOrStdout()) + + store, snapshotDB, err := getSnapshotStore(home) + if err != nil { + return err + } + defer snapshotDB.Close() + + var snap *snapshottypes.Snapshot + if height == 0 { + snap, err = store.GetLatest() + if err != nil { + return fmt.Errorf("failed to get latest snapshot: %w", err) + } + if snap == nil { + return fmt.Errorf("no snapshots found; use 'statesync list' to verify") + } + fmt.Printf("Auto-detected latest snapshot at height %d\n\n", snap.Height) + } else { + snap, err = store.Get(height, snapshottypes.CurrentFormat) + if err != nil || snap == nil { + return fmt.Errorf("snapshot at height %d not found", height) + } + } + + fmt.Printf("Starting state-sync dry-run for height %d...\n", snap.Height) + fmt.Printf(" Chunks to apply: %d\n\n", snap.Chunks) + + // Create a minimal BaseApp with snapshot support + memDB := dbm.NewMemDB() + ba := baseapp.NewBaseApp( + "terpd", + logger, + memDB, + nil, // tx decoder not needed + baseapp.SetSnapshot(store, snapshottypes.NewSnapshotOptions(0, 0)), + ) + + // Convert to ABCI snapshot for OfferSnapshot + abciSnap, err := snap.ToABCI() + if err != nil { + return fmt.Errorf("failed to convert snapshot to ABCI: %w", err) + } + + // Step 1: OfferSnapshot + offerResp, err := ba.OfferSnapshot(&abci.RequestOfferSnapshot{ + Snapshot: &abciSnap, + AppHash: []byte{}, + }) + if err != nil { + return fmt.Errorf("OfferSnapshot failed: %w", err) + } + fmt.Printf("OfferSnapshot result: %s\n", offerResp.Result) + + // Step 2: Apply all chunks + for i := uint32(0); i < snap.Chunks; i++ { + chunkReader, err := store.LoadChunk(snap.Height, snap.Format, i) + if err != nil { + return fmt.Errorf("failed to load chunk %d: %w", i, err) + } + chunkBytes, err := io.ReadAll(chunkReader) + chunkReader.Close() + if err != nil { + return fmt.Errorf("failed to read chunk %d: %w", i, err) + } + + applyResp, err := ba.ApplySnapshotChunk(&abci.RequestApplySnapshotChunk{ + Index: i, + Chunk: chunkBytes, + }) + if err != nil { + return fmt.Errorf("ApplySnapshotChunk %d failed: %w", i, err) + } + + fmt.Printf(" Applied chunk %d/%d -> %s\n", i+1, snap.Chunks, applyResp.Result) + if applyResp.Result == abci.ResponseApplySnapshotChunk_RETRY { + fmt.Println(" -> Chunk asked for retry") + } + } + + fmt.Println("\nState-sync dry-run completed successfully!") + fmt.Printf("Final app hash: %X\n", ba.LastCommitID().Hash) + return nil + }, + } + cmd.Flags().Uint64("height", 0, "Snapshot height (0 = auto-detect latest)") + return cmd +} + +// ====================== FETCH (P2P DOWNLOAD) COMMAND ====================== +func fetchSnapshotCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "fetch", + Short: "Fetch a state-sync snapshot from the production network via P2P", + Long: `Bootstraps a temporary CometBFT node in state-sync mode, connects to +production peers via P2P, discovers and downloads a snapshot, then verifies it. + +CometBFT does not expose snapshot listing/fetching via RPC — snapshot exchange +happens exclusively over P2P (channels 0x60/0x61). This command handles the +full P2P bootstrap automatically. + +The fetched data is stored in a temporary directory that you can inspect with +the other statesync subcommands (list, query, test).`, + RunE: func(cmd *cobra.Command, args []string) error { + home, _ := cmd.Flags().GetString("home") + rpcAddr, _ := cmd.Flags().GetString("rpc-addr") + timeout, _ := cmd.Flags().GetDuration("timeout") + trustOffset, _ := cmd.Flags().GetInt64("trust-offset") + + if home == "" { + home = app.DefaultNodeHome + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // ──── Step 1: Connect to production RPC ──── + fmt.Printf("Connecting to production RPC: %s\n", rpcAddr) + + rpcClient, err := rpchttp.New(rpcAddr, "/websocket") + if err != nil { + return fmt.Errorf("failed to connect to RPC: %w", err) + } + + status, err := rpcClient.Status(ctx) + if err != nil { + return fmt.Errorf("failed to get node status: %w", err) + } + + latestHeight := status.SyncInfo.LatestBlockHeight + network := status.NodeInfo.Network + rpcNodeID := string(status.NodeInfo.DefaultNodeID) + + fmt.Printf(" Network : %s\n", network) + fmt.Printf(" Latest height: %d\n", latestHeight) + fmt.Printf(" Node ID : %s\n", rpcNodeID) + + // Trust height / hash + trustHeight := latestHeight - trustOffset + if trustHeight < 1 { + trustHeight = 1 + } + + block, err := rpcClient.Block(ctx, &trustHeight) + if err != nil { + return fmt.Errorf("failed to get block at height %d: %w", trustHeight, err) + } + trustHash := hex.EncodeToString(block.BlockID.Hash) + + fmt.Printf(" Trust height : %d\n", trustHeight) + fmt.Printf(" Trust hash : %s\n", trustHash) + + // Discover peers + netInfo, err := rpcClient.NetInfo(ctx) + if err != nil { + return fmt.Errorf("failed to get net_info: %w", err) + } + + var peers []string + rpcHost := extractHost(rpcAddr) + if rpcHost != "" { + peers = append(peers, fmt.Sprintf("%s@%s:26656", rpcNodeID, rpcHost)) + } + for _, peer := range netInfo.Peers { + addr := peer.RemoteIP + port := "26656" + if parts := strings.Split(peer.NodeInfo.ListenAddr, ":"); len(parts) > 1 { + port = parts[len(parts)-1] + } + peers = append(peers, fmt.Sprintf("%s@%s:%s", peer.NodeInfo.DefaultNodeID, addr, port)) + } + + if len(peers) == 0 { + return fmt.Errorf("no peers discovered from RPC node") + } + fmt.Printf(" Peers found : %d\n\n", len(peers)) + + // ──── Step 2: Create temporary directory ──── + tmpDir, err := os.MkdirTemp("", "terpd-statesync-*") + if err != nil { + return fmt.Errorf("failed to create temp dir: %w", err) + } + fmt.Printf("Temp directory: %s\n", tmpDir) + + configDir := filepath.Join(tmpDir, "config") + dataDir := filepath.Join(tmpDir, "data") + if err := os.MkdirAll(configDir, 0o755); err != nil { + return fmt.Errorf("failed to create config dir: %w", err) + } + if err := os.MkdirAll(dataDir, 0o755); err != nil { + return fmt.Errorf("failed to create data dir: %w", err) + } + + // Copy genesis.json from user's node home + genSrc := filepath.Join(home, "config", "genesis.json") + genDst := filepath.Join(configDir, "genesis.json") + if err := copyFile(genSrc, genDst); err != nil { + return fmt.Errorf("failed to copy genesis.json from %s: %w", genSrc, err) + } + fmt.Println(" Genesis copied") + + // Generate temp node key + nodeKey, err := p2p.LoadOrGenNodeKey(filepath.Join(configDir, "node_key.json")) + if err != nil { + return fmt.Errorf("failed to generate node key: %w", err) + } + fmt.Printf(" Temp node ID: %s\n", nodeKey.ID()) + + // Generate temp priv validator + pvKeyFile := filepath.Join(configDir, "priv_validator_key.json") + pvStateFile := filepath.Join(dataDir, "priv_validator_state.json") + filePV := pvm.GenFilePV(pvKeyFile, pvStateFile) + filePV.Save() + + // ──── Step 3: Build CometBFT config ──── + cmtCfg := cmtcfg.DefaultConfig() + cmtCfg.RootDir = tmpDir + cmtCfg.DBBackend = "goleveldb" + cmtCfg.P2P.ListenAddress = "tcp://0.0.0.0:26658" // avoid conflict with running node + cmtCfg.P2P.PersistentPeers = strings.Join(peers, ",") + cmtCfg.P2P.AllowDuplicateIP = true + cmtCfg.P2P.PexReactor = false // private: no gossip + cmtCfg.P2P.MaxNumInboundPeers = 0 // private: reject inbound + cmtCfg.P2P.AddrBookStrict = false // allow non-routable addrs + cmtCfg.P2P.Seeds = "" // only persistent peers + cmtCfg.Mempool.Broadcast = false // don't broadcast txs + cmtCfg.RPC.ListenAddress = "tcp://127.0.0.1:26659" + cmtCfg.StateSync.Enable = true + cmtCfg.StateSync.RPCServers = []string{rpcAddr, rpcAddr} // same addr twice satisfies >=2 + cmtCfg.StateSync.TrustHeight = trustHeight + cmtCfg.StateSync.TrustHash = trustHash + cmtCfg.StateSync.TrustPeriod = 336 * time.Hour // 14 days + + // ──── Step 4: Create the app ──── + fmt.Println("\nCreating TerpApp for state-sync...") + + appDB, err := dbm.NewDB("application", dbm.GoLevelDBBackend, dataDir) + if err != nil { + return fmt.Errorf("failed to create app DB: %w", err) + } + defer appDB.Close() + + terpApp := app.NewTerpApp( + log.NewNopLogger(), + appDB, + nil, + false, // don't load latest — fresh state-sync + tmpDir, + simtestutil.NewAppOptionsWithFlagHome(tmpDir), + []wasmkeeper.Option{}, + ) + + // ──── Step 5: Create & start CometBFT node ──── + cmtApp := server.NewCometABCIWrapper(terpApp) + tmLogger := cmtlog.NewTMLogger(os.Stdout) + + cmtNode, err := cmtnode.NewNodeWithContext( + ctx, + cmtCfg, + filePV, + nodeKey, + proxy.NewLocalClientCreator(cmtApp), + cmtnode.DefaultGenesisDocProviderFunc(cmtCfg), + cmtcfg.DefaultDBProvider, + cmtnode.DefaultMetricsProvider(cmtCfg.Instrumentation), + tmLogger, + ) + if err != nil { + return fmt.Errorf("failed to create CometBFT node: %w", err) + } + + fmt.Println("Starting temporary CometBFT node for state-sync...") + if err := cmtNode.Start(); err != nil { + return fmt.Errorf("failed to start node: %w", err) + } + defer func() { + if err := cmtNode.Stop(); err != nil { + fmt.Printf("Warning: failed to stop node cleanly: %v\n", err) + } + cmtNode.Wait() + }() + + // ──── Step 6: Monitor state-sync progress ──── + fmt.Println("State-sync in progress (this may take several minutes)...") + + // Wait for temp node's RPC to be ready + time.Sleep(3 * time.Second) + + localClient, err := rpchttp.New("http://127.0.0.1:26659", "/websocket") + if err != nil { + return fmt.Errorf("failed to create local RPC client: %w", err) + } + + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + deadline := time.After(timeout) + + for { + select { + case <-ticker.C: + st, err := localClient.Status(ctx) + if err != nil { + fmt.Print(".") + continue + } + if st.SyncInfo.LatestBlockHeight > 0 { + fmt.Printf("\n\nState-sync completed!\n") + fmt.Printf(" Height : %d\n", st.SyncInfo.LatestBlockHeight) + fmt.Printf(" App Hash : %X\n", st.SyncInfo.LatestAppHash) + fmt.Printf(" Temp dir : %s\n", tmpDir) + fmt.Println("\nVerify with:") + fmt.Printf(" terpd statesync list --home %s\n", tmpDir) + fmt.Printf(" terpd statesync query --home %s\n", tmpDir) + fmt.Printf(" terpd statesync test --home %s\n", tmpDir) + return nil + } + fmt.Print(".") + case <-deadline: + return fmt.Errorf("state-sync timed out after %s — peers may not have snapshots available", timeout) + } + } + }, + } + + cmd.Flags().String("rpc-addr", "https://rpc.terp.chaintools.tech:443", "Production RPC endpoint for trust info + peer discovery") + cmd.Flags().Duration("timeout", 5*time.Minute, "Maximum time to wait for state-sync completion") + cmd.Flags().Int64("trust-offset", 1000, "Blocks behind latest to set trust height") + + return cmd +} + +// ====================== HELPERS ====================== + +func copyFile(src, dst string) error { + data, err := os.ReadFile(src) + if err != nil { + return err + } + return os.WriteFile(dst, data, 0o644) +} + +func extractHost(rpcAddr string) string { + addr := strings.TrimPrefix(rpcAddr, "https://") + addr = strings.TrimPrefix(addr, "http://") + addr = strings.TrimPrefix(addr, "tcp://") + if idx := strings.Index(addr, ":"); idx >= 0 { + addr = addr[:idx] + } + if idx := strings.Index(addr, "/"); idx >= 0 { + addr = addr[:idx] + } + return addr +} diff --git a/docker/README.md b/docker/README.md index 379c8af..a12e3cb 100644 --- a/docker/README.md +++ b/docker/README.md @@ -2,18 +2,28 @@ Here we have a way to spin up a local network in two commands, including a faucet for testing purposes. + +## Build And Run ```sh # builds the local terp binary -docker buildx build --target localterp -t terpnetwork/terp-core:localterp --load . +docker buildx build --target localterp -t ghcr.io/terpnetwork/terp-core:v5.2.0-zk-localterp --load . # One‑liner that pulls the image (if not present) and starts it. -docker run --rm -it -p 26657:26657 -p 1317:1317 -p 8545:8545 -p 5000:5000 -p 9090:9090 terpnetwork/terp-core:localterp +docker run --rm -it -p 26657:26657 -p 1317:1317 -p 8545:8545 -p 5000:5000 -p 9090:9090 ghcr.io/terpnetwork/terp-core:v5.2.0-zk-localterp ``` -> notice that the difference between building the ~400MB docker image (containing the faucet & nodejs dependencies) and building the ~200MB production image is by specification of the `--target` flag.\ +> notice that the difference between building the ~400MB docker image (containing the faucet & nodejs dependencies) and building the ~200MB production image is by specification of the `--target` flag: > > `--target localterp` for local terp\ > `--target runtime` for production images + + +### Overwriting Mnemonics Used +> To bring your own keys to validators and faucets, include the env vars in the docker run command: > +> `-e VALIDATOR_MNEMONIC="validator mnemonic here"`\ +> or\ +> `-e ACCOUNT_FAUCET_MNEMONIC="validator mnemonic here"` + ## Using The Faucet The faucet is exposed by default on `localhost:5000` diff --git a/docker/localterp/bootstrap.sh b/docker/localterp/bootstrap.sh index 048505f..d6a38d7 100644 --- a/docker/localterp/bootstrap.sh +++ b/docker/localterp/bootstrap.sh @@ -39,7 +39,7 @@ if [ ! -e "$file" ]; then sed -E -i 's/timeout_propose = "[0-9]+m?s"/timeout_propose = "500ms"/' ~/.terpd/config/config.toml sed -E -i 's/timeout_prevote = "[0-9]+m?s"/timeout_prevote = "250ms"/' ~/.terpd/config/config.toml sed -E -i 's/timeout_precommit = "[0-9]+m?s"/timeout_precommit = "250ms"/' ~/.terpd/config/config.toml - sed -E -i 's/timeout_commit = "[0-9]+m?s"/timeout_commit = "1s"/' ~/.terpd/config/config.toml + sed -E -i 's/timeout_commit = "[0-9]+m?s"/timeout_commit = "2400ms"/' ~/.terpd/config/config.toml fi if [ ! -e "$CUSTOM_SCRIPT_PATH" ]; then @@ -50,27 +50,27 @@ if [ ! -e "$file" ]; then echo "Done running custom script!" fi - v_mnemonic="push certain add next grape invite tobacco bubble text romance again lava crater pill genius vital fresh guard great patch knee series era tonight" - a_mnemonic="grant rice replace explain federal release fix clever romance raise often wild taxi quarter soccer fiber love must tape steak together observe swap guitar" - b_mnemonic="jelly shadow frog dirt dragon use armed praise universe win jungle close inmate rain oil canvas beauty pioneer chef soccer icon dizzy thunder meadow" - c_mnemonic="chair love bleak wonder skirt permit say assist aunt credit roast size obtain minute throw sand usual age smart exact enough room shadow charge" - d_mnemonic="word twist toast cloth movie predict advance crumble escape whale sail such angry muffin balcony keen move employ cook valve hurt glimpse breeze brick" - - echo "$v_mnemonic" | terpd keys add validator --recover - echo "$a_mnemonic" | terpd keys add a --recover - echo "$b_mnemonic" | terpd keys add b --recover - echo "$c_mnemonic" | terpd keys add c --recover - echo "$d_mnemonic" | terpd keys add d --recover - + # Default mnemonics (used if environment variables are not provided) + echo "=== Setting up keys ===" + v_mnemonic="${VALIDATOR_MNEMONIC:-push certain add next grape invite tobacco bubble text romance again lava crater pill genius vital fresh guard great patch knee series era tonight}" + a_mnemonic="${ACCOUNT_A_MNEMONIC:-grant rice replace explain federal release fix clever romance raise often wild taxi quarter soccer fiber love must tape steak together observe swap guitar}" + b_mnemonic="${ACCOUNT_B_MNEMONIC:-jelly shadow frog dirt dragon use armed praise universe win jungle close inmate rain oil canvas beauty pioneer chef soccer icon dizzy thunder meadow}" + c_mnemonic="${ACCOUNT_C_MNEMONIC:-chair love bleak wonder skirt permit say assist aunt credit roast size obtain minute throw sand usual age smart exact enough room shadow charge}" + faucet_mnemonic="${ACCOUNT_FAUCET_MNEMONIC:-word twist toast cloth movie predict advance crumble escape whale sail such angry muffin balcony keen move employ cook valve hurt glimpse breeze brick}" + + echo "$v_mnemonic" | terpd keys add validator --recover --keyring-backend test || echo "Validator key already exists" + echo "$a_mnemonic" | terpd keys add a --recover --keyring-backend test || echo "Account a already exists" + echo "$b_mnemonic" | terpd keys add b --recover --keyring-backend test || echo "Account b already exists" + echo "$c_mnemonic" | terpd keys add c --recover --keyring-backend test || echo "Account c already exists" + echo "$faucet_mnemonic" | terpd keys add faucet --recover --keyring-backend test || echo "Account faucet already exists" terpd keys list --output json | jq ico=1000000000000000000 - terpd genesis add-genesis-account validator ${ico}uterp,${ico}uthiol terpd genesis add-genesis-account a ${ico}uterp,${ico}uthiol terpd genesis add-genesis-account b ${ico}uterp,${ico}uthiol terpd genesis add-genesis-account c ${ico}uterp,${ico}uthiol - terpd genesis add-genesis-account d ${ico}uterp,${ico}uthiol + terpd genesis add-genesis-account faucet ${ico}uterp,${ico}uthiol terpd genesis gentx validator ${ico::-1}uterp --chain-id "$chain_id" diff --git a/docker/localterp/faucet/faucet_server.js b/docker/localterp/faucet/faucet_server.js index 792f8b7..5749ad7 100644 --- a/docker/localterp/faucet/faucet_server.js +++ b/docker/localterp/faucet/faucet_server.js @@ -2,7 +2,7 @@ const http = require("http"); const querystring = require("querystring"); const exec = require("child_process").exec; -const FAUCET_WALLET_NAME = process.env.FAUCET_WALLET_NAME || "a"; +const FAUCET_WALLET_NAME = process.env.FAUCET_WALLET_NAME || "faucet"; const FAUCET_AMOUNT = process.env.FAUCET_AMOUNT || "1000000000"; const DENOMS = (process.env.DENOMS || "uterp,uthiol").split(","); diff --git a/docker/localterp/start.sh b/docker/localterp/start.sh index fa7740d..d0deb22 100644 --- a/docker/localterp/start.sh +++ b/docker/localterp/start.sh @@ -19,7 +19,7 @@ init_bootstrap() { a_mnemonic="grant rice replace explain federal release fix clever romance raise often wild taxi quarter soccer fiber love must tape steak together observe swap guitar" b_mnemonic="jelly shadow frog dirt dragon use armed praise universe win jungle close inmate rain oil canvas beauty pioneer chef soccer icon dizzy thunder meadow" c_mnemonic="chair love bleak wonder skirt permit say assist aunt credit roast size obtain minute throw sand usual age smart exact enough room shadow charge" - d_mnemonic="word twist toast cloth movie predict advance crumble escape whale sail such angry muffin balcony keen move employ cook valve hurt glimpse breeze brick" + faucet_mnemonic="word twist toast cloth movie predict advance crumble escape whale sail such angry muffin balcony keen move employ cook valve hurt glimpse breeze brick" x_mnemonic="black foot thrive monkey tenant fashion blouse general adult orient grass enact eight tiger color castle rebuild puzzle much gap connect slice print gossip" z_mnemonic="obscure arrest leader echo truth puzzle police evolve robust remain vibrant name firm bulk filter mandate library mention walk can increase absurd aisle token" @@ -27,7 +27,7 @@ init_bootstrap() { echo "$a_mnemonic" | terpd keys add a --recover echo "$b_mnemonic" | terpd keys add b --recover echo "$c_mnemonic" | terpd keys add c --recover - echo "$d_mnemonic" | terpd keys add d --recover + echo "$faucet_mnemonic" | terpd keys add d --recover echo "$z_mnemonic" | terpd keys add z --recover terpd keys list --output json | jq diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..4ce14fd --- /dev/null +++ b/flake.lock @@ -0,0 +1,82 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1776169885, + "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1776350315, + "narHash": "sha256-ijD4bgb5Iyap9F3MX73vLAZF/SYu+q7Gd7Ux4cbfCWw=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "62e3b8aedabc240e5b0cc9fae003bc9edfebbc9b", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..e1cc81a --- /dev/null +++ b/flake.nix @@ -0,0 +1,191 @@ +{ + description = "Terp-Core blockchain dev environment — vanilla & ZK flavors"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { self, nixpkgs, flake-utils, rust-overlay }: + flake-utils.lib.eachDefaultSystem (system: + let + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { inherit system overlays; }; + + # ── Go toolchain ──────────────────────────────────────────────── + # go.mod requires 1.24.3+ (toolchain 1.24.7) + # nixpkgs tracks latest stable; Go >=1.24 satisfies the requirement. + goVersion = pkgs.go; + + # ── Rust toolchain (for zk-wasmvm libwasmvm) ─────────────────── + # zk-wasmvm needs nightly for backtrace feature; vanilla doesn't + # need Rust at all (downloads prebuilt libwasmvm from GitHub). + rustNightly = pkgs.rust-bin.nightly.latest.default.override { + extensions = [ "rust-src" "rust-analyzer" ]; + targets = [ + "x86_64-unknown-linux-musl" + "aarch64-unknown-linux-musl" + ]; + }; + + rustStable = pkgs.rust-bin.stable.latest.default.override { + extensions = [ "rust-src" ]; + }; + + # ── Python (for dep-switch.py, dep-scrape.py, etc.) ──────────── + python = pkgs.python312; + + # ── Common packages for both flavors ─────────────────────────── + commonPkgs = with pkgs; [ + # Go + goVersion + golangci-lint + gofumpt + + # Build essentials + gnumake + gcc + pkg-config + + # Proto generation + buf + + # Python (dep tooling) + python + + # Cargo tools (dep-switch.py uses cargo-sort) + cargo-sort + + # Docker (for reproducible builds) + docker + docker-buildx + + # Dev utilities + jq + curl + wget + git + ]; + + # ── Shared shell hook ────────────────────────────────────────── + commonShellHook = '' + # Project root for dep-switch.py resolution + export TERP_CORE_ROOT="$(pwd)" + export MONOREPO_ROOT="$(cd .. && pwd)" + + # Go workspace — disable to avoid cross-workspace confusion + export GOWORK=off + + # Make wasmvm findable for CGo + export CGO_ENABLED=1 + ''; + + in { + formatter = pkgs.nixfmt; + + devShells = { + # ── Vanilla: standard terpd build ──────────────────────────── + # Uses upstream wasmvm from GitHub releases. + # No Rust toolchain needed — Go-only dev experience. + # + # nix develop .#vanilla + # make build + # + vanilla = pkgs.mkShell { + name = "terp-vanilla"; + buildInputs = commonPkgs; + + shellHook = commonShellHook + '' + export WASMVM_SOURCE=github + export TERP_FLAVOR=vanilla + + echo "╔══════════════════════════════════════════╗" + echo "║ terp-core dev shell (vanilla) ║" + echo "║ go: $(go version | cut -d' ' -f3) ║" + echo "║ make build — build terpd ║" + echo "║ make install — install to GOPATH/bin ║" + echo "╚══════════════════════════════════════════╝" + ''; + }; + + # ── ZK: zk-circuit flavored terpd build ────────────────────── + # Uses local zk-wasmvm fork (../zk-wasmvm) with Halo2 proving. + # Requires Rust nightly for libwasmvm compilation. + # + # nix develop .#zk + # make build + # + zk = pkgs.mkShell { + name = "terp-zk"; + buildInputs = commonPkgs ++ [ + rustNightly + ] ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [ + pkgs.apple-sdk_15 + pkgs.libiconv + ]; + + shellHook = commonShellHook + '' + export WASMVM_SOURCE=local + export TERP_FLAVOR=zk + + # Ensure go.mod points to local zk forks + if ! grep -q '../zk-wasmvm' go.mod 2>/dev/null; then + echo "⚠ go.mod does not replace wasmvm with ../zk-wasmvm" + echo " Run: go mod edit -replace github.com/CosmWasm/wasmvm/v3=../zk-wasmvm" + fi + + echo "╔══════════════════════════════════════════╗" + echo "║ terp-core dev shell (ZK) ║" + echo "║ go: $(go version | cut -d' ' -f3) ║" + echo "║ rustc: $(rustc --version | cut -d' ' -f2) ║" + echo "║ make build — build terpd (zk flavor) ║" + echo "║ WASMVM_SOURCE=local ║" + echo "╚══════════════════════════════════════════╝" + ''; + }; + + # ── Full: everything for monorepo dev + dep tooling ────────── + # Includes Rust (for Cargo workspace ops), Python dep tooling, + # and both flavor capabilities. + # + # nix develop (or nix develop .#default) + # python3 ../_scripts/dep-switch.py --mode zk_local --target all + # + default = pkgs.mkShell { + name = "terp-full"; + buildInputs = commonPkgs ++ [ + rustNightly + ] ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [ + pkgs.apple-sdk_15 + pkgs.libiconv + ]; + + shellHook = commonShellHook + '' + export TERP_FLAVOR=full + + # Default to ZK since it's the superset + export WASMVM_SOURCE=local + + echo "╔══════════════════════════════════════════╗" + echo "║ terp-core dev shell (full) ║" + echo "║ go: $(go version | cut -d' ' -f3) ║" + echo "║ rustc: $(rustc --version | cut -d' ' -f2) ║" + echo "║ python: $(python3 --version | cut -d' ' -f2) ║" + echo "║ ║" + echo "║ Flavors: nix develop .#vanilla ║" + echo "║ nix develop .#zk ║" + echo "║ ║" + echo "║ Dep tooling: ║" + echo "║ dep-switch --mode local --target all ║" + echo "║ dep-switch --mode zk_local --target all║" + echo "╚══════════════════════════════════════════╝" + ''; + }; + }; + } + ); +} diff --git a/proto/buf.yaml b/proto/buf.yaml index cae10f0..4dcc93a 100644 --- a/proto/buf.yaml +++ b/proto/buf.yaml @@ -1,7 +1,7 @@ version: v1 name: buf.build/terpnetwork/terp-core deps: - - buf.build/cosmos/cosmos-sdk:v0.47.0 + - buf.build/cosmos/cosmos-sdk:v0.53.0 - buf.build/cosmos/cosmos-proto - buf.build/cosmos/gogo-proto - buf.build/googleapis/googleapis diff --git a/scripts/makefiles/docker.mk b/scripts/makefiles/docker.mk index 2a3183f..98e24cd 100644 --- a/scripts/makefiles/docker.mk +++ b/scripts/makefiles/docker.mk @@ -6,23 +6,114 @@ RUNNER_BASE_IMAGE_DISTROLESS := gcr.io/distroless/static-debian11 RUNNER_BASE_IMAGE_ALPINE := alpine:3.17 RUNNER_BASE_IMAGE_NONROOT := gcr.io/distroless/static-debian11:nonroot +# --------------------------------------------------------------------------- +# WASMVM_LIB — optional path to a pre-built libwasmvm_muslc .a file. +# +# Standard build (download from CosmWasm GitHub releases): +# make docker-build +# +# Local build (use your own pre-built static lib): +# make docker-build WASMVM_LIB=../zk-wasmvm/internal/api/libwasmvm_muslc.aarch64.a +# +# When WASMVM_LIB is set, the lib + Go source for zk-wasmvm and zk-wasmd +# are staged into build/ so the Dockerfile can COPY them. +# --------------------------------------------------------------------------- + +WASMVM_LIB ?= + +# Upstream version for GitHub download URL (auto-detected from go.mod). +COSMWASM_VERSION ?= $(shell grep 'CosmWasm/wasmvm' go.mod 2>/dev/null | grep -v '=>' | awk '{print $$2}') + +# Derived source mode: local when WASMVM_LIB is set, github otherwise. +_WASMVM_SOURCE = $(if $(WASMVM_LIB),local,github) + +# Sibling repo paths for staging Go source (only used when WASMVM_LIB is set). +ZK_WASMVM_DIR ?= ../zk-wasmvm +ZK_WASMD_DIR ?= ../zk-wasmd + +# Container registry for push targets +DOCKER_REGISTRY ?= ghcr.io/terpnetwork +DOCKER_TAG ?= $(VERSION) +DOCKER_PLATFORMS ?= linux/amd64,linux/arm64 + +.PHONY: docker docker-help docker-build docker-build-distroless docker-build-alpine \ + docker-build-nonroot docker-build-localnet docker-localterp docker-clean \ + build-zk-local build-zk-local-localnet _docker-stage-zk-lib \ + docker-build-zk docker-build-zk-localnet docker-stage-zk docker-clean-zk \ + _docker-stage _docker-stage-zk-multiarch \ + docker-push docker-push-oline docker-push-localterp \ + docker-push-zk docker-push-zk-oline docker-push-zk-localterp \ + docker-push-all docker-push-standard-all docker-push-zk-all + docker-help: @echo "docker subcommands" @echo "" @echo "Usage:" - @echo " make [command]" + @echo " make docker-build # GitHub wasmvm" + @echo " make docker-build WASMVM_LIB=path/to/libwasmvm_muslc.a # local wasmvm" @echo "" @echo "Available Commands:" - @echo " docker-build Build Docker image" - @echo " docker-build-distroless Build distroless Docker image" + @echo " docker-build Build Docker image (distroless runtime)" @echo " docker-build-alpine Build alpine Docker image" @echo " docker-build-nonroot Build nonroot Docker image" + @echo " docker-build-localnet Build localterp dev image" + @echo " docker-localterp Alias for docker-build-localnet" + @echo " build-zk-local Build with ../zk-wasmvm (auto-detect lib)" + @echo " build-zk-local-localnet Build localterp with ../zk-wasmvm" + @echo " docker-clean Remove staged wasmvm dependencies" + @echo "" + @echo " docker-push Build + push multi-arch runtime to registry" + @echo " docker-push-oline Build + push multi-arch oline to registry" + @echo " docker-push-localterp Build + push multi-arch localterp to registry" + @echo " docker-push-zk Build + push multi-arch zk runtime to registry" + @echo " docker-push-zk-oline Build + push multi-arch zk oline to registry" + @echo " docker-push-zk-localterp Build + push multi-arch zk localterp to registry" + @echo "" + @echo " docker-push-standard-all Build + push all standard images (runtime, oline, localterp)" + @echo " docker-push-zk-all Build + push all zk images (runtime, oline, localterp)" + @echo " docker-push-all Build + push ALL images (standard + zk)" + @echo "" + @echo "Current config:" + @echo " WASMVM_LIB = $(or $(WASMVM_LIB),(unset — will download from GitHub))" + @echo " COSMWASM_VERSION = $(COSMWASM_VERSION)" + @echo " WASMVM_SOURCE = $(_WASMVM_SOURCE)" + @echo " DOCKER_REGISTRY = $(DOCKER_REGISTRY)" + @echo " DOCKER_TAG = $(DOCKER_TAG)" + @echo " DOCKER_PLATFORMS = $(DOCKER_PLATFORMS)" docker: docker-help -docker-build-localnet: - docker buildx build --target localterp -t terpnetwork/terp-core:localterp . +# --------------------------------------------------------- +# Stage local wasmvm lib + Go source (no-op when WASMVM_LIB is unset) +# --------------------------------------------------------- + +_docker-stage: +ifdef WASMVM_LIB + @echo "==> Staging local wasmvm lib: $(WASMVM_LIB)" + @mkdir -p build/wasmvm build/zk-deps/zk-wasmvm build/zk-deps/zk-wasmd + @cp $(WASMVM_LIB) build/wasmvm/ + @echo "==> Staging zk-wasmvm Go source (excluding target/ and .git/) ..." + @rsync -a --delete \ + --exclude='libwasmvm/target/' \ + --exclude='.git/' \ + $(ZK_WASMVM_DIR)/ build/zk-deps/zk-wasmvm/ + @echo "==> Staging zk-wasmd Go source ..." + @rsync -a --delete \ + --exclude='.git/' \ + $(ZK_WASMD_DIR)/ build/zk-deps/zk-wasmd/ + @echo "==> Staged:" + @ls -lh build/wasmvm/ +endif + +docker-clean: + @echo "==> Removing staged wasmvm dependencies ..." + rm -rf build/zk-deps build/wasmvm + @echo "Done." -docker-build: +# --------------------------------------------------------- +# Build targets — all respect WASMVM_LIB +# --------------------------------------------------------- + +docker-build: _docker-stage @DOCKER_BUILDKIT=1 docker build \ -t terpnetwork/terp-core:local \ --target runtime \ @@ -30,26 +121,209 @@ docker-build: --build-arg RUNNER_IMAGE=$(RUNNER_BASE_IMAGE_DISTROLESS) \ --build-arg GIT_VERSION=$(VERSION) \ --build-arg GIT_COMMIT=$(COMMIT) \ + --build-arg COSMWASM_VERSION=$(COSMWASM_VERSION) \ + --build-arg WASMVM_SOURCE=$(_WASMVM_SOURCE) \ -f Dockerfile . docker-build-distroless: docker-build -docker-build-alpine: +docker-build-alpine: _docker-stage @DOCKER_BUILDKIT=1 docker build \ -t terpnetwork/terp-core:local-alpine \ - --target runtime \ + --target runtime \ --build-arg GO_VERSION=$(GO_VERSION) \ --build-arg RUNNER_IMAGE=$(RUNNER_BASE_IMAGE_ALPINE) \ --build-arg GIT_VERSION=$(VERSION) \ --build-arg GIT_COMMIT=$(COMMIT) \ + --build-arg COSMWASM_VERSION=$(COSMWASM_VERSION) \ + --build-arg WASMVM_SOURCE=$(_WASMVM_SOURCE) \ -f Dockerfile . -docker-build-nonroot: +docker-build-nonroot: _docker-stage @DOCKER_BUILDKIT=1 docker build \ -t terpnetwork/terp-core:local-nonroot \ - --target runtime \ + --target runtime \ --build-arg GO_VERSION=$(GO_VERSION) \ --build-arg RUNNER_IMAGE=$(RUNNER_BASE_IMAGE_NONROOT) \ --build-arg GIT_VERSION=$(VERSION) \ --build-arg GIT_COMMIT=$(COMMIT) \ - -f Dockerfile . \ No newline at end of file + --build-arg COSMWASM_VERSION=$(COSMWASM_VERSION) \ + --build-arg WASMVM_SOURCE=$(_WASMVM_SOURCE) \ + -f Dockerfile . + +docker-build-localnet: _docker-stage + @DOCKER_BUILDKIT=1 docker buildx build \ + --target localterp \ + --build-arg COSMWASM_VERSION=$(COSMWASM_VERSION) \ + --build-arg WASMVM_SOURCE=$(_WASMVM_SOURCE) \ + -t terpnetwork/terp-core:localterp --load . + +docker-localterp: docker-build-localnet + +# --------------------------------------------------------- +# Local zk-wasmvm convenience targets +# +# Auto-resolve WASMVM_LIB from ../zk-wasmvm so you can just run: +# make build-zk-local +# --------------------------------------------------------- + +# Map macOS arm64 → aarch64 to match CosmWasm lib naming convention. +_HOST_ARCH := $(shell uname -m | sed 's/arm64/aarch64/') + +# Check internal/api first, then libwasmvm/artifacts as fallback. +_ZK_DEFAULT_LIB = $(firstword \ + $(wildcard $(ZK_WASMVM_DIR)/internal/api/libwasmvm_muslc.$(_HOST_ARCH).a) \ + $(wildcard $(ZK_WASMVM_DIR)/libwasmvm/artifacts/libwasmvm_muslc.$(_HOST_ARCH).a)) + +build-zk-local: _docker-stage-zk-lib + @DOCKER_BUILDKIT=1 docker build \ + -t terpnetwork/terp-core:local-zk \ + --target runtime \ + --build-arg GO_VERSION=$(GO_VERSION) \ + --build-arg RUNNER_IMAGE=$(RUNNER_BASE_IMAGE_ALPINE) \ + --build-arg GIT_VERSION=$(VERSION) \ + --build-arg GIT_COMMIT=$(COMMIT) \ + --build-arg WASMVM_SOURCE=local \ + -f Dockerfile . + +build-zk-local-localnet: _docker-stage-zk-lib + @DOCKER_BUILDKIT=1 docker buildx build \ + --target localterp \ + --build-arg WASMVM_SOURCE=local \ + -t terpnetwork/terp-core:localterp-zk --load . + +# Stage the auto-resolved lib + Go source for zk builds. +_docker-stage-zk-lib: + @if [ -z "$(_ZK_DEFAULT_LIB)" ]; then \ + echo "ERROR: libwasmvm_muslc.$(_HOST_ARCH).a not found in $(ZK_WASMVM_DIR)."; \ + echo "Build it first: just build-wasmvm-alpine"; \ + exit 1; \ + fi + $(MAKE) _docker-stage WASMVM_LIB=$(_ZK_DEFAULT_LIB) + +# Backwards-compat aliases +docker-build-zk: build-zk-local +docker-build-zk-localnet: build-zk-local-localnet +docker-stage-zk: _docker-stage +docker-clean-zk: docker-clean + +# --------------------------------------------------------- +# Multi-arch push targets +# +# Build + push to container registry (default: ghcr.io/terpnetwork). +# Override with: +# make docker-push DOCKER_TAG=v5.2.0 DOCKER_REGISTRY=ghcr.io/myorg +# --------------------------------------------------------- + +docker-push: _docker-stage + docker buildx build \ + --platform $(DOCKER_PLATFORMS) \ + --target runtime \ + --build-arg GO_VERSION=$(GO_VERSION) \ + --build-arg RUNNER_IMAGE=$(RUNNER_BASE_IMAGE_ALPINE) \ + --build-arg GIT_VERSION=$(VERSION) \ + --build-arg GIT_COMMIT=$(COMMIT) \ + --build-arg COSMWASM_VERSION=$(COSMWASM_VERSION) \ + --build-arg WASMVM_SOURCE=$(_WASMVM_SOURCE) \ + --tag $(DOCKER_REGISTRY)/terp-core:$(DOCKER_TAG) \ + --push \ + -f Dockerfile . + +docker-push-oline: _docker-stage + docker buildx build \ + --platform $(DOCKER_PLATFORMS) \ + --target oline \ + --build-arg GO_VERSION=$(GO_VERSION) \ + --build-arg RUNNER_IMAGE=$(RUNNER_BASE_IMAGE_ALPINE) \ + --build-arg GIT_VERSION=$(VERSION) \ + --build-arg GIT_COMMIT=$(COMMIT) \ + --build-arg COSMWASM_VERSION=$(COSMWASM_VERSION) \ + --build-arg WASMVM_SOURCE=$(_WASMVM_SOURCE) \ + --tag $(DOCKER_REGISTRY)/terp-core:$(DOCKER_TAG)-oline \ + --push \ + -f Dockerfile . + +docker-push-localterp: _docker-stage + docker buildx build \ + --platform $(DOCKER_PLATFORMS) \ + --target localterp \ + --build-arg GO_VERSION=$(GO_VERSION) \ + --build-arg COSMWASM_VERSION=$(COSMWASM_VERSION) \ + --build-arg WASMVM_SOURCE=$(_WASMVM_SOURCE) \ + --tag $(DOCKER_REGISTRY)/terp-core:$(DOCKER_TAG)-localterp \ + --push \ + -f Dockerfile . + +# Stage both arch libs for multi-arch zk builds. +_docker-stage-zk-multiarch: + @echo "==> Staging zk-wasmvm libs for multi-arch build" + @mkdir -p build/wasmvm build/zk-deps/zk-wasmvm build/zk-deps/zk-wasmd + @for arch in aarch64 x86_64; do \ + lib=$$(find $(ZK_WASMVM_DIR) -name "libwasmvm_muslc.$$arch.a" 2>/dev/null | head -1); \ + if [ -z "$$lib" ]; then \ + echo "ERROR: libwasmvm_muslc.$$arch.a not found in $(ZK_WASMVM_DIR)"; \ + exit 1; \ + fi; \ + echo " Staging $$lib"; \ + cp "$$lib" build/wasmvm/; \ + done + @echo "==> Staging zk-wasmvm Go source ..." + @rsync -a --delete \ + --exclude='libwasmvm/target/' \ + --exclude='.git/' \ + $(ZK_WASMVM_DIR)/ build/zk-deps/zk-wasmvm/ + @echo "==> Staging zk-wasmd Go source ..." + @rsync -a --delete \ + --exclude='.git/' \ + $(ZK_WASMD_DIR)/ build/zk-deps/zk-wasmd/ + @echo "==> Staged:" + @ls -lh build/wasmvm/ + +docker-push-zk: _docker-stage-zk-multiarch + docker buildx build \ + --platform $(DOCKER_PLATFORMS) \ + --target runtime \ + --build-arg GO_VERSION=$(GO_VERSION) \ + --build-arg RUNNER_IMAGE=$(RUNNER_BASE_IMAGE_ALPINE) \ + --build-arg GIT_VERSION=$(VERSION) \ + --build-arg GIT_COMMIT=$(COMMIT) \ + --build-arg WASMVM_SOURCE=local \ + --tag $(DOCKER_REGISTRY)/terp-core:$(DOCKER_TAG) \ + --push \ + -f Dockerfile . + +docker-push-zk-oline: _docker-stage-zk-multiarch + docker buildx build \ + --platform $(DOCKER_PLATFORMS) \ + --target oline \ + --build-arg GO_VERSION=$(GO_VERSION) \ + --build-arg RUNNER_IMAGE=$(RUNNER_BASE_IMAGE_ALPINE) \ + --build-arg GIT_VERSION=$(VERSION) \ + --build-arg GIT_COMMIT=$(COMMIT) \ + --build-arg WASMVM_SOURCE=local \ + --tag $(DOCKER_REGISTRY)/terp-core:$(DOCKER_TAG)-oline \ + --push \ + -f Dockerfile . + +docker-push-zk-localterp: _docker-stage-zk-multiarch + docker buildx build \ + --platform $(DOCKER_PLATFORMS) \ + --target localterp \ + --build-arg GO_VERSION=$(GO_VERSION) \ + --build-arg WASMVM_SOURCE=local \ + --tag $(DOCKER_REGISTRY)/terp-core:$(DOCKER_TAG)-localterp \ + --push \ + -f Dockerfile . + +# --------------------------------------------------------- +# Aggregate push targets +# --------------------------------------------------------- + +docker-push-standard-all: docker-push docker-push-oline docker-push-localterp + @echo "==> All standard images pushed to $(DOCKER_REGISTRY)/terp-core:$(DOCKER_TAG)" + +docker-push-zk-all: docker-push-zk docker-push-zk-oline docker-push-zk-localterp + @echo "==> All zk images pushed to $(DOCKER_REGISTRY)/terp-core:$(DOCKER_TAG)" + +docker-push-all: docker-push-standard-all docker-push-zk-all + @echo "==> All images (standard + zk) pushed to $(DOCKER_REGISTRY)/terp-core:$(DOCKER_TAG)" diff --git a/scripts/makefiles/ict.mk b/scripts/makefiles/ict.mk new file mode 100644 index 0000000..1bcbbe7 --- /dev/null +++ b/scripts/makefiles/ict.mk @@ -0,0 +1,31 @@ +############################################################################### +### ict-rs tests ### +############################################################################### +ICT_RS_DIR ?= $(HOME)/abstract/ict-rs + +ict-help: + @echo "ict-rs subcommands" + @echo "" + @echo "Usage:" + @echo " make ict-[command]" + @echo "" + @echo "Available Commands:" + @echo " ict-state-sync Run state sync test" + @echo " ict-bootstrap-mainnet Run bootstrap mainnet test" + @echo " ict-loyalty-rewards Run loyalty rewards privacy attestation test" + @echo " ict-all Run all ict-rs tests" + +ict: ict-help + +ict-state-sync: + cd $(ICT_RS_DIR) && cargo run --example state_sync --features docker + +ict-bootstrap-mainnet: + cd $(ICT_RS_DIR) && cargo run --example bootstrap_mainnet --features docker + +ict-loyalty-rewards: + cd $(ICT_RS_DIR) && cargo run --example loyalty_rewards --features "docker hashmerchant" + +ict-all: ict-state-sync ict-bootstrap-mainnet ict-loyalty-rewards + +.PHONY: ict-help ict ict-state-sync ict-bootstrap-mainnet ict-loyalty-rewards ict-all