Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions .github/workflows/manual-big-blocks.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
name: Manual big blocks
# Hyperliquid's `evmUserModify` action for the deployer, dispatched by hand: it
# signs `{"type": "evmUserModify", "usingBigBlocks": <flag>}` with the
# deployment key and POSTs it to HyperCore's exchange endpoint, steering the
# deployer's HyperEVM transactions into big blocks — or back out of them. The
# flag is persistent per address on HyperCore, nothing expires it, which is why
# ONE workflow both sets and unsets: opt in for a sizeable deploy, opt back out
# after it, because small blocks are where ordinary transactions confirm fast.
#
# HyperCore accepts the action only from an address it already knows — one
# holding a Core asset. `Manual credit hypercore` is how the deployer became
# that address; it runs first, once, and this runs after, as often as the
# toggle is needed.
#
# The signing scheme is Hyperliquid's L1-action scheme — msgpack of the action
# and nonce, keccak, an EIP-712 "phantom agent" — and is deliberately NOT
# hand-rolled here: `tools/hyperliquid-big-blocks` is a thin caller of the
# official Hyperliquid Rust SDK, pinned to an exact git rev. That crate's
# manifest says why the Rust SDK and why a rev rather than a crates.io
# version; `rainix.yaml` builds and tests it on every push, so the first
# dispatch is not the first compile.
#
# Deliberately `workflow_dispatch` only, like the deploy and the credit:
# signing with the deployment key is key custody, and no merge, tag or
# schedule may be given a path to it.
on:
workflow_dispatch:
inputs:
using-big-blocks:
type: choice
required: true
options:
- "true"
- "false"
description: |
"true": the deployer's HyperEVM transactions land in big blocks —
the slow, high-gas-cap blocks a sizeable deployment needs — until
this workflow is dispatched again with "false". The flag is
persistent per address on HyperCore, so "false" is not a no-op —
it is the explicit way back OUT of big blocks, and the state after
this run is whatever is selected here, regardless of what any
earlier run set. A choice with no default rather than a checkbox:
GitHub treats an unticked boolean as false, which made a hasty
dispatch a live "turn big blocks off" — this forces the choice to
be made deliberately.
# The default token scoped down to the checkout inside nix-cachix-setup. The
# admin gate's permission lookup needs only metadata read, which every token
# carries implicitly.
permissions:
contents: read
env:
# The rainix commit whose `#rust-shell` runs cargo, pinned exactly as the
# rainix reusables pin it — the same SHA `Manual credit hypercore` and the
# `big-blocks-tool` CI job carry, so the dispatch signs on the same
# toolchain CI builds and tests the tool with.
RAINIX_SHA: c4cf22d9b76600a4ad33b5552f4083f80d9b83de
jobs:
big-blocks:
# One run at a time, never cancelled mid-run: two racing dispatches would
# otherwise leave the final persistent state to exchange-side request
# ordering, and the input description promises the state after a run is
# what its box said. `cancel-in-progress: false` so a run that may
# already have submitted is never killed; a run still QUEUED behind
# another is superseded by a newer queued dispatch (GitHub keeps only the
# newest pending run), which is the toggle's own semantics — the latest
# dispatch decides.
concurrency:
group: manual-big-blocks
cancel-in-progress: false
runs-on: ubuntu-latest
Comment thread
coderabbitai[bot] marked this conversation as resolved.
steps:
# GitHub's own gate on `workflow_dispatch` is write access and nothing
# finer, so admin-only is enforced here, first, before any other step
# runs. Both the original dispatcher (`github.actor`) and whoever
# pressed re-run (`github.triggering_actor`) have to hold `admin`: a
# re-run reuses the original run's inputs, so a re-run by a non-admin
# would be a dispatch by a non-admin. Fail closed: any answer that is
# not exactly `admin` — including no answer, because a failed API call
# fails the step under `set -euo pipefail` — refuses the run. The
# `permission` field is read rather than `role_name` because it
# collapses `maintain` to `write`, so a maintainer is refused too.
- name: Fail unless every actor is an admin
env:
GH_TOKEN: ${{ github.token }}
# Actor names travel as env rather than inline `${{ }}` in the
# script body, so they are data to the shell, never syntax.
DISPATCHER: ${{ github.actor }}
RERUNNER: ${{ github.triggering_actor }}
run: |
set -euo pipefail
for actor in $(printf '%s\n%s\n' "$DISPATCHER" "$RERUNNER" | sort -u); do
permission="$(gh api "repos/${GITHUB_REPOSITORY}/collaborators/${actor}/permission" --jq .permission)"
echo "${actor} holds '${permission}' on ${GITHUB_REPOSITORY}."
if [[ "$permission" != "admin" ]]; then
echo "::error::${actor} is not an admin of ${GITHUB_REPOSITORY}; refusing to run."
exit 1
fi
done
# Shared nix + cachix CI preamble (checkout, nix-quick-install, Cachix,
# cache-nix-action) — pinned action SHAs live in the composite.
- uses: rainlanguage/rainix/.github/actions/nix-cachix-setup@main
with:
cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }}
# Cargo registry/git caches so the dispatch does not re-download the
# world. The `Cargo.lock` in the tree, enforced by `--locked`, decides
# what is built either way.
- uses: rainlanguage/rainix/.github/actions/rust-cache@main
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# The submit itself. The tool exits nonzero unless the exchange answers
# `"status": "ok"`, and prints the full parsed response either way.
# There is no info-endpoint query that reads `usingBigBlocks` back —
# nothing in the Hyperliquid docs, and the obvious candidate type names
# are refused by the API — so the logged response IS the record of what
# was set, and the flag's real observable afterwards is which blocks the
# deployer's next transactions land in. The key travels as env, never an
# input and never echoed; the tool prints only the address derived from
# it.
- name: Sign and submit evmUserModify
run: nix develop github:rainlanguage/rainix/${{ env.RAINIX_SHA }}#rust-shell -c cargo run --locked --manifest-path tools/hyperliquid-big-blocks/Cargo.toml
env:
DEPLOYMENT_KEY: ${{ secrets.PRIVATE_KEY }}
USING_BIG_BLOCKS: ${{ inputs.using-big-blocks }}
19 changes: 19 additions & 0 deletions .github/workflows/rainix.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,22 @@ jobs:
rainix:
uses: rainlanguage/rainix/.github/workflows/rainix-sol.yaml@main
secrets: inherit
# `tools/hyperliquid-big-blocks` is Rust, which none of the sol reusables
# compile, and its only real execution is the key-custody dispatch of
# `Manual big blocks` — so this job is what keeps it building: fmt, clippy
# and tests on every push, `--locked` so what builds is exactly the
# committed `Cargo.lock`, on the same pinned rainix toolchain the dispatch
# runs on. The same commands `rainix-rs-static` + `rainix-rs-test` run,
# inlined because those reusables assume the crate sits at the repo root.
big-blocks-tool:
runs-on: ubuntu-latest
env:
RAINIX_SHA: c4cf22d9b76600a4ad33b5552f4083f80d9b83de
steps:
# Shared nix + cachix CI preamble (checkout, nix-quick-install, Cachix,
# cache-nix-action) — pinned action SHAs live in the composite.
- uses: rainlanguage/rainix/.github/actions/nix-cachix-setup@main
with:
cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }}
- uses: rainlanguage/rainix/.github/actions/rust-cache@main
- run: nix develop github:rainlanguage/rainix/${{ env.RAINIX_SHA }}#rust-shell -c bash -c 'cd tools/hyperliquid-big-blocks && cargo fmt --all -- --check && cargo clippy --all-targets --all-features --locked -- -D warnings -D clippy::all && cargo test --locked'
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ out
.env
.pre-commit-config.yaml
fixture-lib
target
12 changes: 6 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ absent (rainlanguage/rainix#298). This is the repo's ONE agent file: no
relocated.

- **Broadcasting is key custody and real money.** Nothing automatic ever
broadcasts: `Manual sol artifacts` and `Manual credit hypercore` are
`workflow_dispatch` only, and no merge, tag or schedule may be given a path
to either.
- **A `sol-v*` tag is the sole release trigger.**
`[external.package].version` in `foundry.toml` is the version of the LAST
Soldeer publish, not a next-version slot, so an ordinary PR does not bump it.
broadcasts or signs: `Manual sol artifacts`, `Manual credit hypercore` and
`Manual big blocks` are `workflow_dispatch` only, and no merge, tag or
schedule may be given a path to any of them.
- **A `sol-v*` tag is the sole release trigger.** `[external.package].version`
in `foundry.toml` is the version of the LAST Soldeer publish, not a
next-version slot, so an ordinary PR does not bump it.
- **`src/generated/<tag>/` is an append-only record.** `cutRelease()` writes a
tag directory once. A cut tag can never be un-cut and consumers pin what it
holds, so a frozen record is never edited, renamed or deleted.
Expand Down
80 changes: 64 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,20 +423,20 @@ credits itself.

The canonical way to run it is the
[`Manual credit hypercore`](.github/workflows/manual-credit-hypercore.yaml)
workflow. The amount is its `credit-wei` input, typed under its own name —
which is why this is not a `Manual sol artifacts` dispatch: that workflow
exports `DEPLOYMENT_SUITE`, `DEPLOYMENT_NETWORK` and `DEPLOYMENT_KEY` and
nothing else, so an amount of real money could only travel through it under a
name that means something else. Every dispatch executes the dry run;
broadcasting takes the `broadcast` input flipped to true as well, and even then
the dry run still runs and refuses first. Dispatching is admin-gated: GitHub
itself gates `workflow_dispatch` at write access and nothing finer, so the
workflow's first step asks GitHub what permission the dispatching actor holds
on this repo and fails the run unless the answer is `admin` — who dispatched
and what they held is in the run log either way.

The fallback, for when the workflow itself is what is broken, is the same
script by hand:
workflow. The amount is its `credit-wei` input, typed under its own name — which
is why this is not a `Manual sol artifacts` dispatch: that workflow exports
`DEPLOYMENT_SUITE`, `DEPLOYMENT_NETWORK` and `DEPLOYMENT_KEY` and nothing else,
so an amount of real money could only travel through it under a name that means
something else. Every dispatch executes the dry run; broadcasting takes the
`broadcast` input flipped to true as well, and even then the dry run still runs
and refuses first. Dispatching is admin-gated: GitHub itself gates
`workflow_dispatch` at write access and nothing finer, so the workflow's first
step asks GitHub what permission the dispatching actor holds on this repo and
fails the run unless the answer is `admin` — who dispatched and what they held
is in the run log either way.

The fallback, for when the workflow itself is what is broken, is the same script
by hand:

```sh
read -rs DEPLOYMENT_KEY && export DEPLOYMENT_KEY
Expand Down Expand Up @@ -474,6 +474,50 @@ However it is run, it is run once per deployer address and never again: a
HyperCore user does not stop being one, so a second run is more money for no
further effect.

## Toggling big blocks

Crediting made the deployer a HyperCore user; opting into big blocks is a
separate action on that user, and the one the credit exists to make possible.
`{"type": "evmUserModify", "usingBigBlocks": true}`, signed by the deployer key
and POSTed to HyperCore's exchange endpoint, sends the deployer's HyperEVM
transactions to big blocks from then on. The flag is persistent per address:
nothing expires it, so the same action with `false` is how the deployer gets
back OUT of big blocks once the sizeable deploy is done — which is why one
workflow serves both directions and its `using-big-blocks` input is required
rather than defaulted.

The canonical way to toggle it is the
[`Manual big blocks`](.github/workflows/manual-big-blocks.yaml) workflow,
admin-gated exactly as `Manual credit hypercore` is, and `workflow_dispatch`
only for the same reason: signing with the deployment key is key custody. The
signing scheme — msgpack of the action and nonce, keccak, an EIP-712 "phantom
agent" — is the official Hyperliquid Rust SDK's, called by the small
[`tools/hyperliquid-big-blocks`](tools/hyperliquid-big-blocks/) crate and
deliberately not hand-rolled: an encoding wrong by one byte still yields a
signature that recovers as some OTHER address, which is not a failure mode to
meet live. The SDK is pinned to an exact git rev in the crate's manifest, which
also says why a rev rather than a crates.io version, and the `big-blocks-tool`
CI job builds and tests the crate on every push, so the first dispatch is not
the first compile.

The run fails unless the exchange answers `"status": "ok"`, and the full
response is in the run log. That log line is the record: Hyperliquid has no
info-endpoint query that reads `usingBigBlocks` back, so the only observable of
the flag afterwards is which blocks the deployer's next transactions land in.

The fallback, for when the workflow itself is what is broken, is the same crate
by hand — the key read rather than written into the command, for the same
shell-history reason as the credit:

```sh
read -rs DEPLOYMENT_KEY && export DEPLOYMENT_KEY
USING_BIG_BLOCKS=true nix develop github:rainlanguage/rainix#rust-shell \
-c cargo run --locked --manifest-path tools/hyperliquid-big-blocks/Cargo.toml
```

`rust-shell` comes from rainix directly because this repo's own dev shell is the
slim `sol-shell`, which carries no cargo.

## Install

Via [soldeer](https://soldeer.xyz):
Expand Down Expand Up @@ -515,13 +559,17 @@ forge soldeer install # install deps declared in foundry.toml
forge test
```

The three CI jobs are rainix reusable workflows, not commands in the shell. What
each of them runs, which is what reproduces it locally:
Three of the CI jobs are rainix reusable workflows, not commands in the shell,
and the fourth is repo-local. What each of them runs, which is what reproduces
it locally:

- `rainix-sol-test` — `forge test -vvv`
- `rainix-sol-legal` — `reuse lint`
- `rainix-sol-static` — `slither .`, `forge fmt --check`, then
`rainix-sol-single-contract`
- `big-blocks-tool` — `cargo fmt --all -- --check`, `cargo clippy` and
`cargo test --locked` in `tools/hyperliquid-big-blocks/`, in rainix's
`rust-shell`

Use the nix-pinned `forge` for all development.

Expand Down
2 changes: 2 additions & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ path = [
"slither.config.json",
"REUSE.toml",
"soldeer.lock",
"tools/hyperliquid-big-blocks/Cargo.lock",
"tools/hyperliquid-big-blocks/Cargo.toml",
]
SPDX-FileCopyrightText = "Copyright (c) 2020 Rain Open Source Software Ltd"
SPDX-License-Identifier = "LicenseRef-DCL-1.0"
Loading
Loading