diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index 7f29b21212..817f6466c4 100644 --- a/.github/workflows/client.yml +++ b/.github/workflows/client.yml @@ -310,6 +310,55 @@ jobs: install-go: false checks: "-SA1019" + # Compiles and unit-tests the interactive FROST + ROAST retry coordinator path, + # which lives behind the `frost_roast_retry` (and `frost_native`) build tags. The + # default untagged build the other jobs run compiles only the `!frost_roast_retry` + # no-op stubs, so without this job the entire ROAST retry state machine (liveness + + # evidence/blame) and the ~30 `frost_native` unit tests never compile or run in CI. + # + # This is the mock-FFI path: it exercises the Go coordinator flow with cgo DISABLED, + # so it needs neither libfrost_tbtc (the Rust signer lib) nor Docker. The real-crypto + # cgo variant of these tags is gated separately by frost-cgo-integration.yml. + client-frost-roast-retry: + needs: client-detect-changes + if: | + github.event_name == 'push' + || needs.client-detect-changes.outputs.path-filter == 'true' + runs-on: ubuntu-latest + env: + CGO_ENABLED: "0" + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + + - name: Build the ROAST retry coordinator path (mock FFI, no cgo) + run: | + # `frost_roast_retry` alone compiles the coordinator registry / evidence / + # blame flow against mock producers; adding `frost_native` also compiles the + # native-engine <-> retry integration files (mock FFI engine, still no lib). + go build -tags "frost_roast_retry" ./... + go build -tags "frost_native frost_roast_retry" ./... + + - name: Unit-test the tagged path (mock FFI, no cgo) + run: | + # These three tag sets cover every non-cgo tagged test file in pkg/frost and + # pkg/tbtc: + # frost_native -> frost_native (and && !frost_roast_retry) + # frost_roast_retry -> frost_roast_retry (and && !frost_native) + # frost_native frost_roast_retry -> the intersection integration tests + # The `frost_native frost_tbtc_signer cgo` real-crypto tests are NOT run here; + # they are covered by frost-cgo-integration.yml. + for tags in \ + "frost_native" \ + "frost_roast_retry" \ + "frost_native frost_roast_retry"; do + echo "::group::go test -tags \"$tags\"" + go test -tags "$tags" -count=1 -timeout 25m ./pkg/frost/... ./pkg/tbtc/... + echo "::endgroup::" + done + client-integration-test: needs: [client-detect-changes, electrum-integration-detect-changes, client-build-test-publish] if: | diff --git a/.github/workflows/frost-cgo-integration.yml b/.github/workflows/frost-cgo-integration.yml index ce4a11ef74..5faa59cdbc 100644 --- a/.github/workflows/frost-cgo-integration.yml +++ b/.github/workflows/frost-cgo-integration.yml @@ -9,6 +9,12 @@ name: FROST cgo integration # against it with skips FORBIDDEN (KEEP_CORE_FROST_REQUIRE_CGO=true), so a stale/missing/ # unloadable lib fails the job instead of quietly dropping coverage. # +# The tag set also includes `frost_roast_retry`, so the ROAST retry coordinator flow is +# compiled and tested against real crypto here (the mock-FFI variant runs in client.yml), +# and the job smoke-builds the full activation artifact via `make build-frost`. The +# non-cgo tagged build/test coverage lives in the client.yml `client-frost-roast-retry` +# job; together they cover the tag matrix that the default untagged CI never touches. +# # The Go interactive code and the signer crate currently live on separate branches, so # this checks the pinned mirror commit out into a side path and builds from there. After # the branches merge, replace the cross-branch checkout with an in-tree cargo build and @@ -22,6 +28,7 @@ on: - "pkg/net/**" - "go.mod" - "go.sum" + - "Makefile" - "ci/frost-signer-pin.env" - ".github/workflows/frost-cgo-integration.yml" workflow_dispatch: {} @@ -104,10 +111,28 @@ jobs: # references its symbols only via dlsym (no direct references), so the loader # makes them resolvable at runtime; rpath lets the test binary find the .so. export CGO_LDFLAGS="-L${FROST_LIB_DIR} -Wl,--no-as-needed -lfrost_tbtc -Wl,-rpath,${FROST_LIB_DIR}" - # The real-crypto suite (exercises the engine + the ABI preflight against the - # linked lib) plus the ABI fail-closed test (proves a present-but-incompatible - # lib refuses to fall back to legacy signing; uses the seam, so it does not need - # an actually-incompatible lib). This gate is the only CI that builds these tags. - go test -tags "frost_native frost_tbtc_signer" -count=1 \ - -run 'TestRealCgoInteractiveSigning|TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath_ABIIncompatible_DoesNotFallback' \ + # Run the WHOLE tagged pkg/frost/signing suite against the linked lib, not a + # narrow -run subset. `frost_roast_retry` is added to the tag set so the ROAST + # retry coordinator flow (liveness + evidence/blame) is compiled and tested + # against real crypto here too, not only in the mock-FFI job (client.yml). + # With KEEP_CORE_FROST_REQUIRE_CGO=true a lib-unavailable would-be skip becomes + # a hard failure, so a stale/missing symbol fails the gate instead of silently + # dropping coverage. This gate is the only CI that builds the cgo tag set. + go test -tags "frost_native frost_tbtc_signer frost_roast_retry" -count=1 \ + -timeout 20m \ ./pkg/frost/signing/ + + - name: Smoke-build the FROST activation artifact (make build-frost) + env: + CGO_ENABLED: "1" + run: | + set -euo pipefail + # Proves the release/activation binary - the keep-client built with + # `frost_native frost_tbtc_signer frost_roast_retry` and linked against + # libfrost_tbtc - actually compiles and links. The default `make build` the + # release/Docker path runs ships the `!frost_roast_retry` no-op stubs; this is + # the target that produces the ROAST-retry-enabled artifact once the readiness + # manifest flips. Reuses the same lib built above (frost_lib_dir=FROST_LIB_DIR). + make build-frost frost_lib_dir="${FROST_LIB_DIR}" + test -x ./keep-client || { echo "keep-client activation binary was not produced"; exit 1; } + ./keep-client --version diff --git a/Makefile b/Makefile index 1fca3ddc46..3945045c33 100644 --- a/Makefile +++ b/Makefile @@ -129,6 +129,39 @@ build: $(info Building Go code) $(call go_build_cmd,.,$(app_name)) +# FROST activation build. +# +# Produces the keep-client binary with the interactive FROST + ROAST retry +# coordinator path compiled in (build tags `frost_native frost_tbtc_signer +# frost_roast_retry`). The default `build` target above compiles the +# `!frost_roast_retry` no-op stubs, so the released image ships with the ROAST +# retry coordinator disabled; this target is the artifact that actually enables +# it once the readiness manifest flips (see +# docs/development/frost-roast-retry-rollout.adoc). +# +# Unlike `build` this is a cgo build that links libfrost_tbtc (the Rust signer +# lib built from the tbtc-signer crate). Point `frost_lib_dir` at the directory +# holding the built `libfrost_tbtc.{so,dylib}`. The CGO_LDFLAGS mirror the +# frost-cgo-integration CI workflow: `--no-as-needed` keeps the DT_NEEDED entry +# even though the cgo bridge resolves the frost_tbtc_* symbols via dlsym (no +# direct references), and the rpath lets the binary find the lib at runtime. +# These are GNU-ld flags; the target is meant for the Linux CI/release toolchain. +frost_build_tags := frost_native frost_tbtc_signer frost_roast_retry + +ifndef frost_lib_dir +override frost_lib_dir = $(CURDIR)/_frost-target/debug +endif + +build-frost: + $(info Building Go code with the FROST activation tags [$(frost_build_tags)]) + CGO_ENABLED=1 \ + CGO_LDFLAGS="-L$(frost_lib_dir) -Wl,--no-as-needed -lfrost_tbtc -Wl,-rpath,$(frost_lib_dir)" \ + go build \ + -tags "$(frost_build_tags)" \ + -ldflags "-X github.com/keep-network/keep-core/build.Version=$(version) -X github.com/keep-network/keep-core/build.Revision=$(revision) -checklinkname=0" \ + -o $(app_name) \ + . + platforms := linux/amd64 \ darwin/amd64 @@ -150,4 +183,4 @@ cmd-help: build @echo '$$ $(app_name) start --help' > docs/resources/client-start-help ./$(app_name) start --help >> docs/resources/client-start-help -.PHONY: all development sepolia download_artifacts generate gen_proto build cmd-help release build_multi +.PHONY: all development sepolia download_artifacts generate gen_proto build build-frost cmd-help release build_multi diff --git a/docs/development/frost-roast-retry-rollout.adoc b/docs/development/frost-roast-retry-rollout.adoc index 213700dee4..378baa7beb 100644 --- a/docs/development/frost-roast-retry-rollout.adoc +++ b/docs/development/frost-roast-retry-rollout.adoc @@ -88,10 +88,32 @@ RFC-21 design and is enforced in == Production rollout sequencing -. *Build the binary with the tag.* Internal builds and CI - pipelines already exercise the tag via - `go test -tags 'frost_roast_retry' ./pkg/frost/... ./pkg/tbtc/...`. - Production binaries adopt the tag once the readiness manifest at +. *Build the binary with the tag.* CI exercises the tagged path on + every pull request that touches Go, so the ROAST retry flow cannot + silently rot: ++ +-- +* The `client-frost-roast-retry` job in `.github/workflows/client.yml` + compiles the coordinator flow with cgo disabled + (`go build -tags "frost_roast_retry" ./...` and + `go build -tags "frost_native frost_roast_retry" ./...`) and runs the + tagged unit tests under the three non-cgo tag sets that cover the + matrix — `frost_native`, `frost_roast_retry`, and + `frost_native frost_roast_retry` — via + `go test -tags "" ./pkg/frost/... ./pkg/tbtc/...` against the + mock FFI (no `libfrost_tbtc` required). +* `.github/workflows/frost-cgo-integration.yml` adds `frost_roast_retry` + to its real-crypto cgo tag set, runs the whole tagged + `./pkg/frost/signing/` suite against the linked `libfrost_tbtc` with + skips forbidden, and smoke-builds the activation artifact via + `make build-frost`. +-- ++ +Production binaries are produced with `make build-frost` (tags + `frost_native frost_tbtc_signer frost_roast_retry`, cgo-linked against + `libfrost_tbtc`); the default `make build` still ships the + `!frost_roast_retry` no-op stubs. Production adopts the tagged build + once the readiness manifest at `docs/development/frost-readiness-manifest.adoc` flips to `present`. (The manifest was originally planned for the tBTC monorepo's `docs/operations/` directory; the monorepo signer is diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go index 22468e1a96..91323ad524 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go @@ -44,6 +44,29 @@ func TestRegisterBuildTaggedTBTCSignerEngine(t *testing.T) { ) } + // The fail-closed contract asserted below - every engine operation returns + // ErrNativeCryptographyUnavailable - only holds when libfrost_tbtc is NOT linked: + // the cgo bridge is compiled (this file's build tag) but the frost_tbtc_* symbols + // are not resolvable via dlsym. Under the frost-cgo-integration gate the lib IS + // linked, so native crypto is available and StartSignRound instead reaches the real + // signer (and its provenance gate); asserting an unavailable error there would be a + // false failure. Probe the linked lib with the same check the ABI preflight uses - + // it keeps ErrNativeCryptographyUnavailable in the chain iff the lib is absent - and + // skip the fail-closed assertions with a reason when the lib is present. The + // registration wiring above still runs under both builds, and the linked-lib crypto + // path is covered by TestBuildTaggedTBTCSignerInteractiveFROSTBridge_WithLinkedSigner + // and the TestRealCgoInteractiveSigning* suite. + if abiErr := assertTBTCSignerABICompatible(); !errors.Is( + abiErr, ErrNativeCryptographyUnavailable, + ) { + t.Skipf( + "libfrost_tbtc linked (native crypto available; ABI probe: %v); the "+ + "fail-closed-unavailable path this test asserts is not exercisable with "+ + "the lib present", + abiErr, + ) + } + _, err = engine.StartSignRound( "session-1", 1,