Skip to content

CLI: governance improvements#458

Open
Bridgerz wants to merge 5 commits intoupgrade-test-infrastructurefrom
cli-governance-improvements
Open

CLI: governance improvements#458
Bridgerz wants to merge 5 commits intoupgrade-test-infrastructurefrom
cli-governance-improvements

Conversation

@Bridgerz
Copy link
Copy Markdown
Contributor

Summary

A batch of CLI UX/ergonomics improvements for the governance (proposal) flow,
plus one correctness fix for on-chain state scraping and a small localnet
helper to make this stuff actually drivable against a local dev network.

Stacked on top of #456 — set base back to main once that lands.

Commits

  1. Fix CLI on-chain state init for Bag-backed dynamic fields. The
    scraper was reading child_object.contents for every dynamic field,
    but that's only populated for ObjectBag (DynamicObject) kinds. For
    plain Bag entries (proposals), the real payload is on the
    DynamicField itself via value_type + value. Without this,
    every governance CLI command errored with unable to parse type ""
    once the proposals bag had any entries.
  2. Friendlier prompt + print proposal ID on create. (y/N)
    Press enter to X, or Ctrl+C to cancel. After proposal create ...,
    print the new Proposal ID: 0x… so it can be copy/pasted into the
    follow-up vote / view / execute commands.
  3. Show votes + quorum progress in proposal view. One extra
    list_dynamic_fields call pulls the full Proposal<T> envelope and
    displays creator, voter addresses, voted-vs-threshold weight, and
    whether quorum is reached. Scoped to the CLI — no new fields on
    onchain::types::Proposal or changes to the watcher.
  4. --execute flag on proposal vote. Casts the vote and, if
    that vote pushes the proposal over quorum, chains an immediate
    proposal::<module>::execute in the same invocation. Skips silently
    (with an info message) when quorum isn't reached. Upgrade proposals
    are rejected — they need the dedicated upgrade flow.
  5. localnet: dump validator operator keys to disk. Writes each
    validator's operator_private_key to
    <data-dir>/validators/validator_<N>.pem (0600) at hashi-localnet start time. Without this, the keys only live in-memory on the
    running validator processes, so there's no way to point the CLI at a
    committee-member key for proposal create / vote / execute on
    localnet.

Test plan

Tested end-to-end against a 4-validator localnet:

  • proposal create update-config bitcoin_confirmation_threshold u64:3
    prints the new proposal ID and uses the press-enter prompt.
  • proposal view <id> shows Creator, Votes: N voter(s) — <w> of total weight <total>, Threshold: <bps> (<pct>%), and the voter list.
  • proposal vote <id> --execute from the second validator prints
    "Quorum not reached yet (…); skipping --execute."
  • proposal vote <id> --execute from the third validator prints
    "Quorum reached (…); executing…" and submits both the vote and the
    execute transactions.
  • On-chain bitcoin_confirmation_threshold updated to 3 after
    the auto-execute.

`scrape_proposals` was reading `child_object.contents` from every
`list_dynamic_fields` entry, but `child_object` is only populated for
DynamicObject kinds (i.e. `ObjectBag`). Proposals live in a regular
`Bag` (DynamicField kind, value stored inline), so the proposal type
and BCS have to be read from the top-level `DynamicField.value_type`
and `DynamicField.value` fields instead. Previously this surfaced as
`unable to parse type ""` on any fullnode that strictly followed the
field-mask contract, blocking every CLI governance command.

Also make the treasury scrape skip (with a warning) any dynamic field
whose child object comes back without a type, instead of hard-failing
init — treasury caps aren't needed for the governance flows so partial
state is fine there.
Two small UX fixes on the proposal commands:

- Replace the `(y/N)` confirm prompt with `Press enter to X, or
  Ctrl+C to cancel`. Most of the time you want to proceed; typing `y`
  for every governance action is just noise. Ctrl+C still bails.

- After `proposal create ...` executes successfully, print the new
  proposal's ID so it can be copy/pasted into the follow-up `vote`
  and `execute` commands without digging through the tx digest.

`execute_or_simulate` now returns `Option<ExecuteTransactionResponse>`
(Some on a real execute, None on dry-run or when no keypair is
configured) so the create handlers can pull the `ProposalCreatedEvent`
out of effects to get the proposal ID.
`proposal view <id>` now displays vote progress alongside the basic
details so you can tell at a glance whether a proposal is ready to
execute:

  Creator:    <creator_address>
  Votes:      N voter(s) — <voted_weight> of total weight <total> — QUORUM REACHED
              (or <voted>/<threshold> weight, <remaining> more needed)
  Threshold:  <bps> (<percent>%)
  Voters:     <addr>...
  Metadata:   <k>: <v>

Scoped deliberately to the CLI side — no new fields on
`onchain::types::Proposal` and no watcher changes, so the validator's
in-memory state stays unchanged. The CLI pays a single extra
`list_dynamic_fields` call (only on `proposal view`, not `proposal list`)
to pull the full `Proposal<T>` envelope and read votes + threshold.

New `HashiClient::fetch_current_committee` exposes the cached committee
(with weights) for the quorum arithmetic; new `fetch_proposal_details`
returns a `ProposalDetails` struct with votes, quorum threshold,
creator, and metadata.
`hashi proposal vote <id> --execute` (or `-e`) now casts the vote and,
if that vote pushes the proposal over its quorum threshold, chains an
immediate `proposal::<module>::execute` call in the same CLI invocation.
Saves the extra round-trip of running `proposal execute <id>` manually
once the last validator votes.

Semantics:
- If quorum is not reached after the vote, print an info message and
  skip the execute step. No failure.
- Upgrade proposals are rejected — they need the dedicated upgrade
  flow (execute + publish + finalize in one PTB), which a generic
  `<module>::execute` call can't express.
- Dry-run and no-keypair paths skip the execute naturally since
  `execute_or_simulate` returns `None` in those cases.

Uses the existing `HashiClient::build_execute_proposal_transaction`
helper. The quorum check re-fetches live vote state via
`fetch_proposal_details` (added in the previous commit) so it reflects
the vote we just submitted.
Writes each validator's `operator_private_key` to
`<data-dir>/validators/validator_<N>.pem` (0600) at `hashi-localnet
start` time, and adds a `Validator Keys:` line to the connection
details output.

Without this, localnet-resident operator keys only live in-memory on
the running validator processes, so there's no way to point the main
`hashi` CLI at a committee-member key for `proposal create` / `vote` /
`execute`. Dumping them makes the whole governance flow drivable end
to end against localnet with:

    hashi proposal \
        -c .hashi/localnet/hashi-cli.toml \
        --keypair .hashi/localnet/validators/validator_<N>.pem \
        <subcommand>

Factors the existing funded-key dump through a shared `write_pem_to_disk`
helper so both writes share the restrictive-permissions logic.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant