Onboarding
Your own organization: who signs, what needs whose approval, deploying the contract, checking it and handing it to its quorums. Three routes, a bitcd.yaml with the CLI, your own code, or plain contract calls, end with the same contract.
Pick a route
All three routes deploy the same contract and write the same signers and policies. They differ in how much is checked for you.
A · bitcd.yaml and the CLI
One file. The bitcd CLI checks it, deploys, compares the chain with the file and runs later changes. Start here.
B · From your own code
@bitcd/core deploys and seeds from a table you build. For setups driven by your own scripts.
C · By hand
Declare, deploy and seed with sncast or any Starknet tool. No JavaScript, every call visible.
| A · CLI | B · SDK | C · By hand | |
|---|---|---|---|
| Checks the setup before deploying | yes — schema and security checks | no | no |
| Derives the policies each part needs | yes | you list them | you list them |
| Compares the chain with your intent | bitcd diff | you read back | you read back |
| Publishes the organization's definition | publish-manifest | not by itself | not by itself |
| Later changes | propose-changes | proposals from code | proposals by hand |
Executors need route A’s published definition. A deployment made with B or C can adopt the CLI later; bitcd diff reports whether a bitcd.yaml matches the chain.
Before you start
| You need | For | Notes |
|---|---|---|
| Node 22 and pnpm | routes A and B | pnpm through corepack; the repos pin their pnpm version. |
| Side-by-side clones | routes A and B | bitcd-core. Your own code links @bitcd/core from it by path, so the layout matters. |
| Docker | the local rehearsal | The compose file in the bitcd-core checkout runs a Starknet devnet on :5051 and an S3-compatible store on :4566. |
sncast (Starknet Foundry) | route C | With Scarb 2.19 if you declare the contract from source. |
| An S3-compatible bucket | route A, and anything with documents | The chain stores fingerprints; the documents themselves live in the bucket. It doesn't have to be trusted — readers check every document against the chain. |
| One account per signer | every route | Any Starknet account contract that implements SRC-6 signature checks. Plus a funded account to act as the temporary owner during setup. |
$ mkdir bitcdorg && cd bitcdorg
$ git clone git@github.com:bitcdorg/bitcd-core.git
$ npm install -g @bitcd/core starknet @aws-sdk/client-s3 # the bitcd command; the SDK is for the document storeThe bitcd command ships with @bitcd/core; from an unreleased checkout, node bitcd-core/core/bin/bitcd.mjs is the same command. It covers every step here. Keep bitcd.yaml, the keystore and the deployment record in their own directory, acme-org/ beside the clones.
Design the organization
Decide who holds which key, what needs whose approval and how to recover from a lost key. The middle column is where each answer goes in bitcd.yaml.
| Question | In the file | Worth knowing |
|---|---|---|
| Who can approve? The people and agents who hold keys. | cast.signers | Mark people human: true. Only their signed approvals count toward min_humans. |
| Who controls each key? | operator | Keys run by the same person or team share an operator. A max_per_operator below the threshold then stops one operator from filling a quorum alone — but only if this reflects who really holds the keys. |
| What kinds of signer are there? | cast.roles, role | Each signer has one role, and each quorum counts one role. Give executors a role of their own, so their keys can report status but never approve. |
| What will you govern? | values, intent_types, fleets, locks | Each becomes a prefix — app/release, k8s/rbac — with its own policies. Keys, prefixes, roles and schema names are each at most 31 ASCII characters. |
| Who has to agree? | quorum | Four fields per change: threshold, role, min_humans and max_per_operator (0 means no cap). Approvals over an operator's cap don't just go uncounted — they make the commit fail. Keep min_humans at or below threshold, and make sure each quorum can still be met when someone loses a key. |
| Who changes the rules? | manifest.quorum | The quorum over the organization's own definition, and over adding or removing signers. It must include a human. |
| What might you need later? | mutable, reserved_prefixes | Once ownership is given up, policies can only be changed where a prefix has a policy-changing gate, and no new prefix can be added. Mark prefixes mutable and reserve a few spare ones. |
| Where do documents live? | storage | The bucket name, and the name of the environment variable that holds the store's endpoint. |
The example on this page is a small team, acme: three people and a CI bot, one governed value (the approved production release) and one spare prefix for feature flags. A release needs two approvals from different operators, at least one of them human.
Rehearse on a local chain
Start a devnet and the local store, then build a development keystore from the devnet's accounts. The keystore is JSON, {name: {address, pk}}. The entry named genesis is the temporary owner.
$ (cd bitcd-core && docker compose up -d --wait devnet ministack)
$ mkdir acme-org && cd acme-org
$ curl -s localhost:5051 -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"devnet_getPredeployedAccounts"}' |
node -e '
const a = JSON.parse(require("fs").readFileSync(0)).result;
const names = ["genesis", "alice", "bob", "carol", "release-bot"];
const keys = Object.fromEntries(names.map((n, i) => [n, { address: a[i].address, pk: a[i].private_key }]));
require("fs").writeFileSync("keys.json", JSON.stringify(keys, null, 2));'
$ node -p 'require("./keys.json").alice.address' # the addresses to put in your fileThese are public development keys and exist only on your devnet. A deployment record points at one contract on one chain. When you restart the devnet, delete deployment.json too, or the CLI resumes against a contract that no longer exists.
Route A: bitcd.yaml and the CLI
Describe the organization
acme as a file, addresses shortened. Start with renounce: false; ownership is given up in step 6, after the result is checked.
schema: bitcd/v1
org: acme
version: 1
naming: literal
ceremony:
renounce: false # keep the owner until you've checked the result
cast:
roles: [OPERATOR]
signers:
alice: { address: "0x…", human: true, operator: OP_ALICE, role: OPERATOR }
bob: { address: "0x…", human: true, operator: OP_BOB, role: OPERATOR }
carol: { address: "0x…", human: true, operator: OP_CAROL, role: OPERATOR }
release-bot: { address: "0x…", human: false, operator: OP_CI, role: OPERATOR }
storage:
bucket: acme-bitcd
endpoint_env: BITCD_S3_ENDPOINT
values:
release-gate: # the approved production release
prefix: app/release
schema: release/v1
key: app/release/prod
quorum: { threshold: 2, role: OPERATOR, min_humans: 1, max_per_operator: 1 }
revoke: same
escalation: default
mutable: true # its policies can be tightened later
reserved_prefixes: # room to grow after ownership is gone
- prefix: app/flags
set_policy: { threshold: 2, role: OPERATOR, min_humans: 1, max_per_operator: 1 }
manifest:
key: sys/manifest
quorum: { threshold: 2, role: OPERATOR, min_humans: 1, max_per_operator: 1 }The bitcd.yaml reference covers every section, including intent types and fleets.
Validate and plan
Both commands are offline. validate refuses a file whose quorums the cast cannot meet, where an executor could approve, or where a key might not fit. plan lists what setup will write.
$ bitcd validate bitcd.yaml
OK bitcd.yaml: org=acme v1 — 4 signers, 0 connector(s), 0 intent type(s), 1 governed value(s), 9 policies to seed, renounce=false
$ bitcd plan bitcd.yaml -o out/
ceremony for org=acme (renounce=false):
set_signer alice (OPERATOR, human)
…
policy GLOBAL/SET_ROLE=2-of-OPERATOR/h1
policy GLOBAL/SET_POLICY=2-of-OPERATOR/h1
policy sys·SET_VALUE=2-of-OPERATOR/h1
…
policy app/release·SET_POLICY=2-of-OPERATOR/h1
policy app/flags·SET_POLICY=2-of-OPERATOR/h1 (reserved)The compiler adds the GLOBAL and sys gates, for signers and the definition, at the manifest quorum's strength. -o also writes the seed and the compiled manifest as JSON.
Deploy and seed
Declares the contract if needed, deploys the instance with genesis as the temporary owner, writes each signer and policy, and records the instance in deployment.json.
$ bitcd apply-ceremony bitcd.yaml --keys keys.json
class hash : 0x621fbe86…
deployed at: 0x5bf4162b…
wrote ./deployment.json
ok set_signer alice (0x42284fef)
…
owner now: 0x3736286f…If it stops halfway, run it again. It resumes against the recorded instance, skips entries that match and stops on one that differs.
Publish the definition
The compiled definition goes on chain as a governed value at sys/manifest, through the manifest quorum. It is a real change by your real signers, made while the owner can still fix mistakes.
With development keys, the CLI approves and commits with the signers in the keystore:
$ bitcd publish-manifest bitcd.yaml --keys keys.jsonWith real signers, only propose. Each person signs with their own key; then run the command again to put the committed definition in place:
$ bitcd publish-manifest bitcd.yaml --keys keys.json --no-commit
proposal #1: SET_VALUE sys/manifest (orgmanifest/v1 v1)
pending: 2-of-OPERATOR, min_humans 1, max_per_operator 1 — humans sign via `agentgate sign 1`. …
# each signer, on their own machine, signs proposal 1 with their own key: see the first change below
$ bitcd publish-manifest bitcd.yaml --keys keys.json # after the commit: puts it in placeA signer reads sys/proposals/<id>.blob from the store, checks it against the fingerprint, then signs and relays the approval as shown in the first change. Whoever completes the quorum can commit; look for COMMITTED. Each signer pays for their own transaction unless a relayer submits the batch.
Compare the chain with the file
diff checks ownership against ceremony.renounce, reads every declared policy and signer back, looks for active signers the file does not list, and checks the published definition against the file.
$ bitcd diff bitcd.yaml
contract 0x5bf4162b… runs class 0x621fbe866f…
CLEAN: yaml == ownership == chain policy table == active signer roster == on-chain manifestDifferences, including a published definition it cannot read or verify, are listed with exit code 2. An invalid file or another failure exits with 1. Run it after every step from here on.
Hand over to the quorums
Go through the checklist. Then set renounce: true and run the ceremony again. Every entry is skipped as already seeded, and ownership is given up.
$ bitcd apply-ceremony bitcd.yaml --keys keys.json
skip set_signer alice (already seeded)
…
renouncing ownership (no single actor remains)
owner now: 0x0 (renounced)
$ bitcd diff bitcd.yaml
MANIFEST DIFFERS: chain v1 (org v1) digest 0x194d0d75… != local 0xd0d8d4d2… — publish-manifest (or reconcile the yaml)
$ bitcd publish-manifest bitcd.yaml --keys keys.json # or --no-commit and sign, as in step 4
$ bitcd diff bitcd.yaml
CLEAN …The definition fingerprints the whole file, so the changed renounce line makes it differ until it is published again. Another apply-ceremony refuses: ownership already renounced. From here on every change goes through a quorum.
Route B: from your own code
A Node project in acme-org/, also used by the first governed change on every route. @bitcd/core is linked by path from the clone, with starknet ^9 and @aws-sdk/client-s3 beside it. acme-org/package.json:
{
"name": "acme-org",
"private": true,
"type": "module",
"dependencies": {
"@bitcd/core": "^0.1.0",
"starknet": "^9.0.0",
"@aws-sdk/client-s3": "^3.0.0"
}
}$ pnpm installonboard.mjs. Every policy is listed by hand, including the GLOBAL and sys gates the CLI would add, so the CLI can be adopted later.
import { readFileSync } from "node:fs";
import { provider, account, bitcdContract, str } from "@bitcd/core/client";
import { ACTION } from "@bitcd/core/protocol";
import { deployBitcd, runCeremony, signerSpec, policySpec } from "@bitcd/core/ceremony";
const keys = JSON.parse(readFileSync("keys.json", "utf8"));
const p = provider(); // BITCD_RPC, default http://localhost:5051
const owner = account(keys.genesis, p); // the temporary owner
const OPS = str("OPERATOR");
const quorum = policySpec(2, OPS, 1, 1, false); // threshold, role, min_humans, max_per_operator, allow_open
const person = (op) => signerSpec({ is_human: true, operator_id: str(op), role: OPS });
const signers = [
{ label: "alice", address: keys.alice.address, signer: person("OP_ALICE") },
{ label: "bob", address: keys.bob.address, signer: person("OP_BOB") },
{ label: "carol", address: keys.carol.address, signer: person("OP_CAROL") },
{ label: "release-bot", address: keys["release-bot"].address,
signer: signerSpec({ is_human: false, operator_id: str("OP_CI"), role: OPS }) },
];
const gate = (prefix, action) => ({ label: `${prefix} ${action}`, prefix: str(prefix), action: ACTION[action], policy: quorum });
const policies = [
gate("GLOBAL", "SET_ROLE"), gate("GLOBAL", "SET_POLICY"), // managing signers later
gate("sys", "SET_VALUE"), gate("sys", "TOMBSTONE_VALUE"), gate("sys", "SET_POLICY"), // the definition
gate("app/release", "SET_VALUE"), gate("app/release", "TOMBSTONE_VALUE"), gate("app/release", "SET_POLICY"),
gate("app/flags", "SET_POLICY"), // the spare prefix
];
const { address } = await deployBitcd({ owner, deploymentFile: "deployment.json", log: console.log });
await runCeremony({ contract: bitcdContract(address, owner), provider: p, signers, policies, renounce: false, log: console.log });
// runCeremony doesn't read anything back; this is the check the CLI's diff does for you.
const view = bitcdContract(address, p);
for (const s of signers) {
const on = await view.get_signer(s.address);
if (!on.active || on.is_human !== s.signer.is_human
|| BigInt(on.operator_id) !== BigInt(s.signer.operator_id) || BigInt(on.role) !== BigInt(s.signer.role))
throw new Error(`signer ${s.label} differs`);
}
for (const pl of policies) {
const on = await view.get_policy(pl.prefix, pl.action);
if (!on.exists || Number(on.threshold) !== pl.policy.threshold || BigInt(on.role) !== BigInt(pl.policy.role)
|| Number(on.min_humans) !== pl.policy.min_humans || Number(on.max_per_operator) !== pl.policy.max_per_operator
|| on.allow_open !== pl.policy.allow_open)
throw new Error(`policy ${pl.label} differs`);
}
console.log("every signer and policy reads back as written");$ node onboard.mjsMake a first governed change and go through the checklist. Then renounce as a separate step, renounce.mjs:
import { readFileSync } from "node:fs";
import { provider, account, bitcdContract, loadDeployment } from "@bitcd/core/client";
const keys = JSON.parse(readFileSync("keys.json", "utf8"));
const p = provider(), dep = loadDeployment("deployment.json");
const contract = bitcdContract(dep.address, account(keys.genesis, p));
await p.waitForTransaction((await contract.renounce_ownership()).transaction_hash);
console.log(`owner now: ${BigInt(await contract.owner()) === 0n ? "0x0 (renounced)" : "still set"}`);$ node renounce.mjs
owner now: 0x0 (renounced)deployBitcd writes the record the CLI uses. To adopt the CLI, write a bitcd.yaml and run bitcd diff; once it matches, publish-manifest and propose-changes work.
Route C: by hand
The setup surface: a constructor taking the owner, set_signer, set_policy, and OpenZeppelin's owner and renounce_ownership. The setters are owner-only. sncast takes quoted short strings and struct literals in --arguments, so nothing is encoded by hand. Any Starknet tool that can invoke a contract works.
Declare and deploy
From acme-org/: import the owner account into sncast, declare the class once per network, deploy it. If the class is already declared, sncast prints its hash; deploy that. Compare it with classHash in bitcd-core/core/artifacts/MANIFEST.json.
$ RPC=http://localhost:5051
$ OWNER=$(node -p 'require("./keys.json").genesis.address')
$ sncast account import --name owner --type oz --address "$OWNER" \
--private-key "$(node -p 'require("./keys.json").genesis.pk')" --url "$RPC"
$ (cd ../bitcd-core/contracts && sncast --account owner --wait declare --contract-name Bitcd --url "$RPC")
$ CLASS=0x621fbe86… # the class hash declare printed
$ sncast --account owner --wait deploy --class-hash "$CLASS" --arguments "$OWNER" --url "$RPC"
Contract Address: 0x0082…
$ C=0x0082… # the address deploy printed
$ echo "{\"address\":\"$C\",\"classHash\":\"$CLASS\",\"rpc\":\"$RPC\"}" > deployment.jsonThe last line writes the deployment record the SDK and the CLI read.
Write signers and policies
One call per signer and per (prefix, action) policy. One of each below; acme needs all four signers and the nine policies from route A's plan, including the GLOBAL and sys gates.
$ ALICE=$(node -p 'require("./keys.json").alice.address')
$ sncast --account owner --wait invoke --contract-address "$C" --function set_signer --url "$RPC" \
--arguments "$ALICE, bitcd::Signer { active: true, is_human: true, operator_id: 'OP_ALICE', role: 'OPERATOR' }"
$ sncast --account owner --wait invoke --contract-address "$C" --function set_policy --url "$RPC" \
--arguments "'GLOBAL', 'SET_ROLE', bitcd::Policy { threshold: 2, role: 'OPERATOR', min_humans: 1, max_per_operator: 1, allow_open: false, exists: true }"The contract refuses an open or zero-threshold SET_POLICY, SET_ROLE, TOMBSTONE_VALUE or FORCE_UNLOCK gate written with exists: true. Nothing else is checked: a quorum nobody can meet, or a row with exists: false that gates nothing, is accepted.
Read everything back
$ sncast call --contract-address "$C" --function get_signer --url "$RPC" --arguments "$ALICE"
Response: Signer { active: true, is_human: true, operator_id: 0x4f505f414c494345, role: 0x4f50455241544f52 }
$ sncast call --contract-address "$C" --function get_policy --url "$RPC" --arguments "'GLOBAL', 'SET_ROLE'"
Response: Policy { threshold: 2_u32, role: 0x4f50455241544f52, min_humans: 1_u32, max_per_operator: 1_u32, allow_open: false, exists: true }Roles and operators come back as short-string felts; 0x4f50455241544f52 is OPERATOR. Check every signer and policy you wrote.
Renounce, last
After a first governed change and the checklist:
$ sncast --account owner --wait invoke --contract-address "$C" --function renounce_ownership --url "$RPC"
$ sncast call --contract-address "$C" --function owner --url "$RPC"
Response Raw: [0x0]From now on the setters answer Caller is not the owner. Governed changes by hand use propose, approve, approve_sigs and commit, with the parameter layouts on the contract page. A plain approve from a person does not count toward min_humans; only a signed approval through approve_sigs does.
A first governed change
Before renouncing, commit one real change through the quorum you rely on most, signed as in production, while the owner can still fix things. Route A's manifest step does this for the manifest quorum. acme's release gate is a plain value, outside the CLI's intent commands, so it is proposed from code. first-change.mjs, in the route B project:
import { readFileSync } from "node:fs";
import { provider, account, bitcdContract, loadDeployment, approveWithSigs, str } from "@bitcd/core/client";
import { ACTION, STORAGE_CLASS, digestOf } from "@bitcd/core/protocol";
import { SELECTORS } from "@bitcd/core/events";
import { s3Store } from "@bitcd/core/store";
const keys = JSON.parse(readFileSync("keys.json", "utf8"));
const p = provider(), dep = loadDeployment("deployment.json");
const view = bitcdContract(dep.address, p);
const store = s3Store({ bucket: "acme-bitcd" }); // BITCD_S3_ENDPOINT
await store.ensureBucket();
const PREFIX = "app/release", KEY = "app/release/prod";
const doc = JSON.stringify({ version: "1.4.0", approved: true });
const cur = await view.get_value(str(KEY));
// SET_VALUE params: key, fingerprint, schema, storage class, the version you expect to replace
const params = [str(KEY), digestOf(doc), str("release/v1"), STORAGE_CLASS.S3, Number(cur.version)];
// 1. Any active signer proposes; the id comes back in the ProposalCreated event.
const bot = bitcdContract(dep.address, account(keys["release-bot"], p));
const receipt = await p.waitForTransaction(
(await bot.propose(ACTION.SET_VALUE, str(PREFIX), params)).transaction_hash);
const id = BigInt(receipt.events.find((e) => BigInt(e.keys[0]) === BigInt(SELECTORS.ProposalCreated)).keys[1]);
console.log(`proposal #${id}`);
// 2. The document and its claimed params wait beside the proposal, where signers read them.
await store.putText(`${PREFIX}/proposals/${id}.blob`, doc);
await store.putText(`${PREFIX}/proposals/${id}.json`, JSON.stringify({ params: params.map(String) }));
// 3. On a devnet: approve with development keys, then commit (anyone may, once the quorum is met).
await approveWithSigs({ p, contractAddress: dep.address, proposalId: id,
action: ACTION.SET_VALUE, prefix: str(PREFIX), params,
signers: [keys.alice, keys.bob], relayer: keys["release-bot"] });
await p.waitForTransaction((await bot.commit(id)).transaction_hash);
// 4. Only a committed document goes where readers look, and only while it's still the current one.
const now = await view.get_value(str(KEY));
if (BigInt(now.digest) === BigInt(digestOf(doc))) await store.putText(`${KEY}/spec`, doc);$ node first-change.mjs
proposal #1With real signers, stop after step 2 and have each person sign on their own machine: signApproval with their account, then approveWithSigs to relay, both from @bitcd/core. Anyone can commit once the quorum is complete. Then publish.mjs puts the document in place, after checking that the proposal is the committed version:
import { provider, bitcdContract, loadDeployment, str } from "@bitcd/core/client";
import { digestOf } from "@bitcd/core/protocol";
import { s3Store } from "@bitcd/core/store";
const [id] = process.argv.slice(2);
const PREFIX = "app/release", KEY = "app/release/prod";
const p = provider(), dep = loadDeployment("deployment.json");
const store = s3Store({ bucket: "acme-bitcd" });
const doc = await store.getText(`${PREFIX}/proposals/${id}.blob`);
const now = await bitcdContract(dep.address, p).get_value(str(KEY));
if (BigInt(now.digest) !== BigInt(digestOf(doc))) throw new Error(`proposal #${id} isn't the current committed ${KEY}`);
await store.putText(`${KEY}/spec`, doc);
console.log(`${KEY}/spec <- proposal #${id}`);$ node publish.mjs 1To read the document before signing, print it with its fingerprint:
$ node --input-type=module -e '
const { s3Store, digestOf } = await import("@bitcd/core/store");
const doc = await s3Store({ bucket: "acme-bitcd" }).getText(process.argv[1]);
console.log(digestOf(doc)); console.log(doc);' app/release/proposals/1.blobTo see a refusal, commit with one approval, or with two from the same operator. Both fail with commit: policy unmet.
Sepolia and mainnet
The steps are the same on every network. What changes:
| Local devnet | Public network | |
|---|---|---|
| RPC | http://localhost:5051 | Your provider's URL in --rpc / BITCD_RPC. |
| Accounts | Predeployed and funded. | Create, fund and deploy an account for each signer and for the temporary owner. People can use wallet accounts. Machine accounts are deployed from a funded deployer with an account class already declared on the network, such as OpenZeppelin’s. |
| Keystore | Every signer's development key. | Only the temporary owner, plus an account allowed to propose. Humans sign with their own key or a wallet, never through a shared file. |
| The contract class | Declared by the first deploy. | Declared once per network; check the class hash against MANIFEST.json. |
| Storage | The local store, test credentials. | A real bucket, one per deployment — stored paths don't include the contract address. Set BITCD_S3_ENDPOINT (it defaults to the local store) and BITCD_S3_REGION. For credentials, set AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY (plus AWS_SESSION_TOKEN for temporary credentials), or leave the keys out to use the AWS SDK's own chain — profiles, SSO, instance and task roles. An empty BITCD_S3_ENDPOINT means AWS's own regional endpoint. |
| Deployment record | Throwaway. | Keep one per network, and keep it safe. The CLI refuses a record whose contract it can't find on the chain behind --rpc, or whose class doesn't match. |
| Approvals | publish-manifest, propose-changes and --approve-dev approve with the development keys. | publish-manifest --no-commit and propose-changes --no-commit, then people sign. |
Before you renounce
bitcd diffprintsCLEAN, or your own read-back matched every signer and policy.- One governed change has gone through the quorum you rely on most, signed the way it will be in production.
('GLOBAL', SET_ROLE)and('GLOBAL', SET_POLICY)exist, so signers can be added, rotated and removed. For the CLI's tools, the manifest prefix'sSET_VALUE,TOMBSTONE_VALUEandSET_POLICYtoo.- Every prefix whose policies may change has a
SET_POLICYgate, and spare prefixes are reserved. No prefix can be added afterwards. - Each human signer is a different person, and each
operatormatches who controls the key. - Every quorum, including the manifest quorum, can still be met if one person loses their key.
- The deployment record is kept. On a public network, the whole setup was rehearsed on Sepolia.
After setup
Edit bitcd.yaml, check it, and the CLI turns the difference into proposals:
$ bitcd diff bitcd.yaml
SIGNER DIFFERS/MISSING: dave (0x23b7f487…)
MANIFEST DIFFERS: …
$ bitcd propose-changes bitcd.yaml --keys keys.json
plan: 1 signer change(s), 0 policy change(s)
proposal #…: SET_ROLE dave (OPERATOR, human)
…
org manifest committed: chain version 3, …| You want to | What happens |
|---|---|
| Add or change a signer | A SET_ROLE proposal through the GLOBAL gate. |
| Remove a signer | Delete them from the file; they're deactivated, and their approvals stop counting, even on open proposals. |
| Tighten a policy | A SET_POLICY proposal through that prefix's gate. Higher thresholds, more humans, a tighter per-operator cap. |
| Start using a reserved prefix | Replace its reserved_prefixes entry with the real section; its policies are created through the gate reserved at setup. |
| Loosen a policy or change its role | Refused by the CLI before anything is sent. For the SET_POLICY, SET_ROLE, TOMBSTONE_VALUE and FORCE_UNLOCK gates, the contract refuses it too. |
| Govern a prefix you didn't reserve | Not possible on this instance — it needs a new deployment. |
propose-changes checks the plan against the chain first and republishes the definition last. Each change is its own proposal; a rerun picks up where it stopped. By default it approves with the keystore's signers. With real signers add --no-commit: one proposal at a time, people sign, run again for the next, ending with the definition.
$ bitcd propose-changes bitcd.yaml --keys proposer.json --no-commit
proposal #7: SET_ROLE dave (OPERATOR, human) — needs 2-of-OPERATOR, min_humans 1, max_per_operator 1; sign via `agentgate sign 7`, then run propose-changes again (0 more step(s) after this one, then the manifest)
$ bitcd propose-changes bitcd.yaml --keys proposer.json --no-commit # before anyone has signed
waiting: proposal #7 (SET_ROLE dave (OPERATOR, human)) needs 2-of-OPERATOR, … — sign via `agentgate sign 7`, then run propose-changes againThe CLI reference has every option.
Good to know
- The keystore is plain JSON with private keys. Fine on a devnet. On a public network keep it to the temporary owner and a proposer; people sign with their own keys.
- What
diffdoes not check. It compares what the file declares, ownership included. A policy removed from the file stays on chain, unreported. - Publish the definition from the file as it will stay. It fingerprints everything the file declares, comments and formatting aside, so any change, even the
renounceflag, needs another publish. - Store settings must match. The CLI finds documents through
BITCD_S3_BUCKET, which defaults tobitcd-tfstate. Set it to yourstorage.bucket. - Which contract gets deployed. The SDK and CLI take
BITCD_SIERRA/BITCD_CASMif set, else abitcd-core/contracts/target/devbuild if one exists, else the bundled contract. The class hash is printed at deploy; check it againstMANIFEST.json. - Transactions take time. Each setup step is a transaction; on the local devnet expect around ten seconds each.