{"id":31016997,"url":"https://github.com/chorusone/nebula","last_synced_at":"2026-03-07T15:30:55.745Z","repository":{"id":308351697,"uuid":"990906923","full_name":"ChorusOne/nebula","owner":"ChorusOne","description":null,"archived":false,"fork":false,"pushed_at":"2025-09-12T10:42:15.000Z","size":293,"stargazers_count":4,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-09-12T12:31:23.063Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ChorusOne.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-05-26T20:36:18.000Z","updated_at":"2025-09-12T10:42:18.000Z","dependencies_parsed_at":"2025-09-12T12:17:06.279Z","dependency_job_id":null,"html_url":"https://github.com/ChorusOne/nebula","commit_stats":null,"previous_names":["chorusone/nebula"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/ChorusOne/nebula","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ChorusOne%2Fnebula","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ChorusOne%2Fnebula/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ChorusOne%2Fnebula/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ChorusOne%2Fnebula/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ChorusOne","download_url":"https://codeload.github.com/ChorusOne/nebula/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ChorusOne%2Fnebula/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":274935970,"owners_count":25376834,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-13T02:00:10.085Z","response_time":70,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-09-13T07:45:18.039Z","updated_at":"2026-03-07T15:30:55.733Z","avatar_url":"https://github.com/ChorusOne.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Nebula - the CometBFT Remote signer, written in Rust\n\n# NOTE: THIS IS AN ALPHA VERSION OF THE SIGNER!\nAs of 2025-10-02, Nebula is used in a setup of one signer connected to two validators on the XPLA network.\n\nValidator identity: https://www.mintscan.io/xpla/validators/xplavaloper14yeq3lkajldaggj28hmq8xng9xux7x5g80w747\n\nNebula is a CometBFT remote signer. It uses Raft to create a cluster of signer nodes that collectively maintain the signature high water mark.\n\n## Principles, core assumptions\n\nThe core principle of Nebula is that the decision to sign a block is treated as a state transition in a distributed state machine.\n\nA signature is only produced and transmitted *after* the state transition has been successfully committed to a quorum of nodes in the Raft cluster.\n\nThere is only ONE signer (Raft leader) at a time that is capable of connecting to CometBFT nodes. (That is also enforced by the privval protocol)\n\nNebula tries to err on the side of signing less, than actually signing more, so in turbulent leadership changes, uptime is expected to suffer slightly.\n\nNebula connects to only one blockchain node, with only one consensus key. That means you will need one instance per identity on a network.\n\n### Why it's correct\n\nIn order to not double-sign, privval protocol requires a signer to reliably track last signed state (HRS). Here's how we achieve it:\n\n#### HRS validation logic\n\n[CometBFT documentation](https://docs.cometbft.com/main/spec/consensus/signing) states clearly when a signer should sign an incoming consensus message.\n\nWe reject any request that violates CometBFT's consensus rules.\n\n#### Handling only one request at any given time\n\nNebula uses a mutex which is locked when entering the replication/signing sequence, to make it easier to reason about the signing process.\n\n\n#### The cluster always agrees on latest HRS\n\nRaft's leader completeness property ensures all nodes see the same committed HRS sequence.\nWhen leadership changes occur, the new leader must contain all previously committed entries.\n\nLeaders can crash, restart, or be replaced, but Raft guarantees that the new leader must include all committed entries in its log, so the recorded HRS cannot roll back.\n\n#### HRS state is durably persisted\n\nA log entry containing the HRS is considered committed once the leader that created the entry has replicated it on a majority of servers.\n\nAdding RocksDB's synchronous writes, it means that the \"last signed HRS\" is only advanced when it's been durably stored in a quorum.\n\n#### Signing only occurs after persisting the state\n\nThe signature is sent to the CometBFT node only after it's been replicated, so in a case of a failure during replication, the signer does not advance.\n\nSince we never sign without first recording the new HRS durably across a majority, and Raft + RocksDB guarantee this state cannot be lost or rolled back, double-signing should be impossible.\n\n\n### Sequence of a Signing Request\n\nConsider a \"happy\" case, where leader of the Nebula cluster connected to a single CometBFT node receives a signing request, e.g a proposal at height 100, at round 1. The flow is as follows:\n\n1. A mutex is acquired to ensure requests from only one node is processed at a time. (`src/handler.rs`)\n2. The node verifies it is still the Raft leader. If not, it bails early.\n3. The request's Height/Round/Step (HRS) is checked against the last known committed state. If signing would violate CometBFT's double-signing rules, the request is rejected. This logic is in `src/safeguards.rs`.\n4. Leader proposes the new HRS state (`{h: 100, r: 1, step: Proposal}`) to the Raft cluster. The handler thread then **blocks** and waits for confirmation that this entry has been committed by a majority of the cluster.\n    -   This is implemented in `SignerRaftNode::replicate_state` (`src/cluster/mod.rs`), which uses an `mpsc::channel` to wait for a callback.\n    -   The callback is only sent by the Raft machinery in `handle_committed_entries` after the entry has been written to the distributed log.\n    -   If a quorum cannot be reached, this step will time out and return an error.\n5.  Leader usees the configured signing backend to produce a signature.\n6.  The signature is sent back to the CometBFT validator.\n7.  Mutex acquired at the beginning is released.\n\nAfter which, CometBFT node propagates the signature.\n\nNow, consider a very similar case, but the Nebula leader is connected to two CometBFT nodes, say nodes A and B. Nebula received a signing request from node A first.\nFor node A, the signing flow looks identical to the one described above. Node B will send the same request, and what happens is as follows:\n\n1. Handler will wait on the mutex until Node A's request is served.\n2. The node verifies it is still the Raft leader. If not, it bails early.\n3. The request's Height/Round/Step (HRS) is checked against the last known committed state. Because this request is the same as one served just before, it will fail here. Nebula will log: `Prevented double signing vote`, and the CometBFT node will report an error:\n```\nfailed signing vote err=\"signerEndpoint returned error #1: Would double-sign vote at same height/round\" height=100 module=consensus round=1 vote={\"block_id\":{\"hash\":\"41648E00251B1F6A94089BF7F4D942B640665325863F0D92E44D36AEBB604904\",\"parts\":{\"hash\":\"DDCA7D5234BC6EB67F2E91E68CC22E434C3B2BA5D5D77CFAA44D9FC0D254AC5F\",\"total\":1}},\"extension\":null,\"extension_signature\":null,\"height\":\"100\",\"round\":1,\"signature\":null,\"timestamp\":\"2025-08-20T15:11:30.581382895Z\",\"type\":1,\"validator_address\":\"0F38A435D89DF98B10BE57928BA79111D7440379\",\"validator_index\":23}\n```\n\nThis concludes the signing request, and only one signature will be transmitted to a CometBFT node.\n\n\n## Testing Strategy and Limitations\n\nNebula is currently evaluated primarily through integration tests.\n\n-   The test suite in `src/cluster/integration_tests.rs` creates an in-memory cluster of multiple Raft nodes.\n-   The network connection to the CometBFT validator is mocked.\n-   The tests simulate failures by shutting down nodes, transferring leadership, and sending duplicate or out-of-order requests.\n\n### Current Limitations\n\n-   They **do not** test the physical network I/O layers. Bugs in the TCP stream handling or `secret_connection` layer would not be caught.\n-   Fault injection is currently programmatic (shutting down threads) rather than simulating true network partitions.\n-   Probably a lot more which I have not thought about yet\n\n## Supported Backends and Features\n\n-   Signing Backends:\n    -   `native`: Key is stored in a local file.\n    -   `vault_transit`: Uses HashiCorp Vault's Transit engine.\n    -   `vault_signer_plugin`: Uses a custom Vault plugin.\n-   Native key types: Ed25519, Secp256k1, Bls12381.\n\n## Usage\n\nRun `init` with `--help` to check available backends to bootstrap with:\n```\nnebula init --help\nUsage: nebula init --output-path \u003cOUTPUT_PATH\u003e --backend \u003cBACKEND\u003e\n\nOptions:\n  -o, --output-path \u003cOUTPUT_PATH\u003e\n  -b, --backend \u003cBACKEND\u003e          [possible values: vault-transit, vault-signer-plugin, native]\n```\n\n\nGenerate a default config file at `test.toml`:\n```\nnebula init --output-path test.toml --backend native\n```\n\nExample output:\n\n```\nlog_level = \"info\"\nchain_id = \"test-chain-v1\"\nversion = \"v1_0\"\nsigning_mode = \"native\"\n\n[[connections]]\nhost = \"127.0.0.1\"\nport = 36558\n\n[[connections]]\nhost = \"127.0.0.1\"\nport = 26558\n\n[raft]\nnode_id = 1\nbind_addr = \"127.0.0.1:8080\"\ndata_path = \"./raft_data\"\ninitial_state_path = \"./initial_state.json\"\n\n[[raft.peers]]\nid = 1\naddr = \"127.0.0.1:7001\"\n\n\n[signing.native]\nprivate_key_path = \"./privkey\"\nkey_type = \"ed25519\"\n```\n\n`connections` array is the list of CometBFT the Nebula leader will connect to.\n`host` and `port` must be set in accordace with `priv_validator_laddr` in CometBFT's config.toml.\n\n`raft` section defines settings for the Raft signer cluster.\nPeer list must include EVERY member of the cluster, including the very node you're setting up.\n\nFor native signing, you can generate keys with `./target/release/nebula keys generate --key-type ed25519`.\nExample output:\n```\nprivate key: UINHR9vYjWllyqYo+Jxc5fjYUBox3eRygj6dbUughIE=\n{\n  \"address\": \"D79D2CD52901D6D42D7617B977447EEC1CA00887\",\n  \"priv_key\": {\n    \"type\": \"tendermint/PrivKeyEd25519\",\n    \"value\": \"UINHR9vYjWllyqYo+Jxc5fjYUBox3eRygj6dbUughIEVDPizljxIzHVAz2b6YuTdmuLUTgn12On0wXXxyEkhHw==\"\n  },\n  \"pub_key\": {\n    \"type\": \"tendermint/PubKeyEd25519\",\n    \"value\": \"FQz4s5Y8SMx1QM9m+mLk3Zri1E4J9djp9MF18chJIR8=\"\n  }\n}\n```\n\nIn this case, `UINHR9vYjWllyqYo+Jxc5fjYUBox3eRygj6dbUughIE=` must be put under ./privkey.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchorusone%2Fnebula","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchorusone%2Fnebula","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchorusone%2Fnebula/lists"}