Update DEVELOPERS.md
This commit is contained in:
parent
2de6affc75
commit
e1acaa62dd
443
DEVELOPERS.md
443
DEVELOPERS.md
|
|
@ -1,234 +1,246 @@
|
|||
# Developing and Activating CLP Implementations
|
||||
|
||||
This guide explains how developers prepare code for an approved Contractless Proposal and make that code activate at the block selected by on-chain governance.
|
||||
This guide explains how developers make code changes that activate only after an implementation has been approved by Contractless governance.
|
||||
|
||||
Governance does not dynamically download or execute code. Developers must ship a candidate node release containing the proposed implementation before activation voting finishes.
|
||||
Governance does not download or execute source code. Developers release candidate software containing the proposed changes before activation. That software keeps the new behavior disabled until the approved activation block.
|
||||
|
||||
That candidate release must remain compatible with the existing chain until the approved activation block is reached.
|
||||
## Development Process
|
||||
|
||||
## Development Sequence
|
||||
The complete process is:
|
||||
|
||||
The expected sequence is:
|
||||
1. A CLP is submitted through a ProposalKey transaction.
|
||||
2. Eligible node operators approve the proposal.
|
||||
3. Developers implement the approved proposal.
|
||||
4. Developers publish an implementation document identifying the exact code and commits.
|
||||
5. Node operators install and test candidate software containing both the original and proposed behavior.
|
||||
6. Eligible node operators vote on that implementation.
|
||||
7. An implementation reaching 85 percent approval is scheduled to activate 5,760 blocks later.
|
||||
8. Candidate software begins using the new behavior at the activation block.
|
||||
|
||||
1. A finalized CLP document is committed through a ProposalKey transaction.
|
||||
2. Eligible nodes approve the proposal with ProposalVote transactions.
|
||||
3. Developers implement the approved behavior.
|
||||
4. Developers publish an implementation document describing the exact candidate.
|
||||
5. Node operators install candidate software containing both the old and proposed rules.
|
||||
6. Eligible nodes vote on the implementation document.
|
||||
7. When approval reaches 85 percent, activation is scheduled 5,760 blocks later.
|
||||
8. Candidate nodes begin applying the new rules at the scheduled block.
|
||||
Candidate software must be available before activation voting finishes. Otherwise node operators cannot verify what they are being asked to activate.
|
||||
|
||||
Implementation development may begin before proposal approval, but activation votes are rejected until the proposal has been approved.
|
||||
## Implementation Document
|
||||
|
||||
## The Implementation Document
|
||||
|
||||
The implementation document is distinct from the proposal.
|
||||
|
||||
The proposal describes what the network should change. The implementation document identifies the code that claims to implement that decision.
|
||||
The implementation document identifies the exact code presented for activation. It is separate from the original proposal.
|
||||
|
||||
It should include:
|
||||
|
||||
- The ProposalKey being implemented.
|
||||
- Relevant repository and branch.
|
||||
- Every commit included in the implementation.
|
||||
- Files and consensus paths affected.
|
||||
- New validation behavior.
|
||||
- Historical behavior that must remain available.
|
||||
- Database or record-format changes.
|
||||
- Network-protocol compatibility.
|
||||
- Required node, wallet, or application upgrades.
|
||||
- Testing performed.
|
||||
- Known limitations and migration instructions.
|
||||
- The source repository and branch.
|
||||
- Every relevant commit.
|
||||
- The files and consensus paths changed.
|
||||
- The original and proposed behavior.
|
||||
- Transaction, block, or database format changes.
|
||||
- Compatibility and migration requirements.
|
||||
- Tests performed.
|
||||
|
||||
The exact raw bytes of this document are hashed when an activation vote is created. Once activation voting begins for that hash, changing the document creates a different implementation candidate.
|
||||
Activation votes hash the exact bytes of this document. Editing it creates a different implementation hash and therefore a different activation candidate.
|
||||
|
||||
Do not silently replace or edit a document that already has votes. Publish a new document and allow operators to vote on the new development hash.
|
||||
Do not alter an implementation document after voting begins. Publish a new document for revised code.
|
||||
|
||||
## Creating an Activation Candidate
|
||||
## Activation Status
|
||||
|
||||
There is no separate transaction that registers an implementation candidate.
|
||||
|
||||
The first activation vote for a development hash creates the candidate. Use:
|
||||
|
||||
```bash
|
||||
./create_activation_vote_tx <proposal_key> <implementation_file> <development_location> <yes|no>
|
||||
```
|
||||
|
||||
The tool:
|
||||
|
||||
1. Reads the exact implementation-document bytes.
|
||||
2. Hashes those bytes with Skein-256.
|
||||
3. Associates that development hash with the approved ProposalKey.
|
||||
4. Records the document location.
|
||||
5. Creates and signs an activation vote.
|
||||
|
||||
All voters for the same candidate must use the exact same implementation document bytes and ProposalKey. They should also use the same canonical development location so the recorded location remains unambiguous.
|
||||
|
||||
## Candidate Software Must Be Dormant
|
||||
|
||||
Do not release candidate software that immediately applies new consensus rules.
|
||||
|
||||
Before activation, upgraded and unupgraded nodes must continue validating the chain under the same existing rules. The new path must remain dormant until governance records its activation height.
|
||||
|
||||
A typical validation decision should follow this shape:
|
||||
All consensus changes use one shared function:
|
||||
|
||||
```rust
|
||||
if implementation_is_active(db, proposal_key, expected_development_hash, block_height)? {
|
||||
validate_with_new_rules(transaction, block_height)
|
||||
} else {
|
||||
validate_with_old_rules(transaction, block_height)
|
||||
}
|
||||
use crate::common::governance::implementation_activation_status;
|
||||
|
||||
let (implementation_approved, activation_block) =
|
||||
implementation_activation_status(
|
||||
db,
|
||||
PROPOSAL_KEY,
|
||||
DEVELOPMENT_HASH,
|
||||
)?;
|
||||
```
|
||||
|
||||
`block_height` must be the height of the block being validated or replayed. Do not use only the node's current tip height.
|
||||
|
||||
That distinction is essential during:
|
||||
|
||||
- Initial synchronization.
|
||||
- Full historical validation.
|
||||
- Orphan correction.
|
||||
- Chain rollback and replay.
|
||||
- Snapshot verification.
|
||||
|
||||
A block below the activation height must always use the old rules, even when being checked years after activation.
|
||||
|
||||
## Reading the Activation Record
|
||||
|
||||
Approved activation schedules are stored in the Sled tree named:
|
||||
The function returns:
|
||||
|
||||
```text
|
||||
consensus_activations
|
||||
(false, 0)
|
||||
```
|
||||
|
||||
The key is the decoded 32-byte ProposalKey.
|
||||
when that proposal has no approved implementation.
|
||||
|
||||
The value is 136 bytes:
|
||||
It returns:
|
||||
|
||||
| Offset | Length | Meaning |
|
||||
|---:|---:|---|
|
||||
| `0` | 32 | Approved implementation document hash |
|
||||
| `32` | 4 | Activation block as little-endian `u32` |
|
||||
| `36` | 100 | Space-padded ASCII implementation-document location |
|
||||
|
||||
Code selecting consensus behavior must verify both:
|
||||
|
||||
- The ProposalKey matches the rule being activated.
|
||||
- The stored development hash matches the implementation compiled into that release.
|
||||
|
||||
Conceptually:
|
||||
|
||||
```rust
|
||||
fn implementation_is_active(
|
||||
db: &sled::Db,
|
||||
proposal_key_hex: &str,
|
||||
expected_development_hash_hex: &str,
|
||||
block_height: u32,
|
||||
) -> Result<bool, String> {
|
||||
let proposal_key = hex::decode(proposal_key_hex)
|
||||
.map_err(|err| format!("Invalid proposal key: {err}"))?;
|
||||
let expected_hash = hex::decode(expected_development_hash_hex)
|
||||
.map_err(|err| format!("Invalid development hash: {err}"))?;
|
||||
|
||||
let tree = db
|
||||
.open_tree("consensus_activations")
|
||||
.map_err(|err| format!("Could not open activation schedules: {err}"))?;
|
||||
|
||||
let Some(value) = tree
|
||||
.get(proposal_key)
|
||||
.map_err(|err| format!("Could not read activation schedule: {err}"))?
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
if value.len() != 136 {
|
||||
return Err("Stored activation schedule has an invalid length.".to_string());
|
||||
}
|
||||
|
||||
if value[0..32] != expected_hash {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let activation_height = u32::from_le_bytes(
|
||||
value[32..36]
|
||||
.try_into()
|
||||
.map_err(|_| "Could not decode activation height.")?,
|
||||
);
|
||||
|
||||
Ok(block_height >= activation_height)
|
||||
}
|
||||
```text
|
||||
(true, activation_block)
|
||||
```
|
||||
|
||||
This illustrates the current stored format. Shared production code should centralize this parsing rather than duplicating raw offsets throughout validation modules.
|
||||
when the exact development hash was approved and scheduled.
|
||||
|
||||
## Choosing the Correct Boundary
|
||||
It returns an error when the governance record is malformed or the proposal scheduled a different implementation than the software expects.
|
||||
|
||||
If an implementation activates at block `50,000`:
|
||||
`PROPOSAL_KEY` is the ProposalKey transaction hash from the CLP `.id` file.
|
||||
|
||||
- Blocks `0` through `49,999` use the old behavior.
|
||||
- Block `50,000` and every later block use the approved behavior.
|
||||
`DEVELOPMENT_HASH` is the hash of the exact implementation document approved by node operators.
|
||||
|
||||
For a later proposal that revises the same rule at block `100,000`:
|
||||
## Standard Activation Check
|
||||
|
||||
- Blocks `0` through `49,999` use the original behavior.
|
||||
- Blocks `50,000` through `99,999` use the first approved implementation.
|
||||
- Blocks beginning at `100,000` use the revised implementation.
|
||||
|
||||
Do not delete historical validation paths merely because a newer implementation activated. Nodes still need them to validate old blocks.
|
||||
|
||||
## Apply the Boundary Everywhere
|
||||
|
||||
A consensus change is incomplete if its activation check is added to only the normal transaction-submission path.
|
||||
|
||||
Review every path that interprets or validates the affected rule:
|
||||
|
||||
- Mempool admission.
|
||||
- Received-block validation.
|
||||
- Locally created block saving.
|
||||
- Initial chain synchronization.
|
||||
- Orphan comparison and replay.
|
||||
- Rollback and undo handling.
|
||||
- Pending-effects calculation.
|
||||
- Database reconstruction.
|
||||
- Balance and ownership rebuilding.
|
||||
- RPC validation tools.
|
||||
- CLI validation tools.
|
||||
|
||||
The same block must produce the same result regardless of whether it was mined locally, downloaded during sync, received live, or replayed during orphan correction.
|
||||
|
||||
## Data-Format Changes
|
||||
|
||||
Changing fixed transaction bytes, block bytes, database keys, or serialized records requires special care.
|
||||
|
||||
New code may need to parse both formats:
|
||||
Every affected code path uses the same condition:
|
||||
|
||||
```rust
|
||||
if block_height < activation_height {
|
||||
parse_legacy_format(bytes)
|
||||
if implementation_approved && block_height >= activation_block {
|
||||
// New behavior
|
||||
} else {
|
||||
parse_activated_format(bytes)
|
||||
// Original behavior
|
||||
}
|
||||
```
|
||||
|
||||
Do not reinterpret historical bytes using a new structure.
|
||||
The comparison must be:
|
||||
|
||||
If a new transaction type is introduced, nodes may need to recognize its byte length before activation while rejecting its use until activation. Otherwise candidate nodes may disconnect from peers or fail to parse candidate traffic before the activation boundary.
|
||||
```rust
|
||||
block_height >= activation_block
|
||||
```
|
||||
|
||||
Network-protocol changes should be designed so upgraded and unupgraded nodes can remain connected during the 5,760-block activation delay whenever possible.
|
||||
The original rules remain active below the activation block. The new rules begin at the activation block.
|
||||
|
||||
## Activation Voting
|
||||
If activation is scheduled for block `50,000`:
|
||||
|
||||
Node operators should verify:
|
||||
- Block `49,999` uses the original behavior.
|
||||
- Block `50,000` uses the new behavior.
|
||||
- Block `50,001` uses the new behavior.
|
||||
|
||||
- The proposal was approved.
|
||||
- The implementation document matches the proposal.
|
||||
- The listed commits match the reviewed source.
|
||||
- Their built binary contains those commits.
|
||||
- The implementation remains dormant before activation.
|
||||
- Historical validation still passes.
|
||||
- Sync, rollback, and orphan correction work across the boundary.
|
||||
## Which Height to Use
|
||||
|
||||
They can then create an activation vote:
|
||||
`block_height` must be the height of the specific block being processed.
|
||||
|
||||
Do not automatically use the node's currently saved chain height.
|
||||
|
||||
For example, a node at height `100,000` may be replaying block `40,000`. Block `40,000` must use the rules that applied at block `40,000`, not the rules active at the current chain tip.
|
||||
|
||||
| Operation | Height to compare |
|
||||
|---|---|
|
||||
| Validating a received block | Height of the received block |
|
||||
| Saving a block | Height of the block being saved |
|
||||
| Parsing historical transactions | Height containing those transactions |
|
||||
| Initial synchronization | Height of each downloaded block |
|
||||
| Orphan replay | Height of each block being replayed |
|
||||
| Undo or rewind | Height of the block being undone |
|
||||
| Creating the next local block | Height of the block being created |
|
||||
|
||||
## Changing Existing Behavior
|
||||
|
||||
Use an `if` and `else` when replacing an existing rule:
|
||||
|
||||
```rust
|
||||
if implementation_approved && block_height >= activation_block {
|
||||
validate_with_new_rules(transaction)?;
|
||||
} else {
|
||||
validate_with_original_rules(transaction)?;
|
||||
}
|
||||
```
|
||||
|
||||
Both paths must remain in the software. Historical blocks still require the original rules after activation.
|
||||
|
||||
## Adding New Behavior
|
||||
|
||||
Reject new functionality before activation:
|
||||
|
||||
```rust
|
||||
if !implementation_approved || block_height < activation_block {
|
||||
return Err("This feature has not been activated.".to_string());
|
||||
}
|
||||
|
||||
validate_new_feature(transaction)?;
|
||||
```
|
||||
|
||||
This prevents miners or users from introducing the new behavior before the approved block.
|
||||
|
||||
## Changing a Structure
|
||||
|
||||
Rust structure definitions cannot appear or disappear at runtime. Candidate software contains both formats and chooses the correct parser:
|
||||
|
||||
```rust
|
||||
if implementation_approved && block_height >= activation_block {
|
||||
NewTransaction::from_bytes(bytes)
|
||||
} else {
|
||||
OriginalTransaction::from_bytes(bytes)
|
||||
}
|
||||
```
|
||||
|
||||
Never parse historical bytes using a structure that did not apply when the block was created.
|
||||
|
||||
## Saving Records
|
||||
|
||||
Saving follows the same condition:
|
||||
|
||||
```rust
|
||||
if implementation_approved && block_height >= activation_block {
|
||||
save_with_new_rules(transaction, db)?;
|
||||
} else {
|
||||
save_with_original_rules(transaction, db)?;
|
||||
}
|
||||
```
|
||||
|
||||
The locally mined, live downloaded, initial-sync, and orphan-replay paths must produce the same records for the same block.
|
||||
|
||||
## Undoing and Rewinding
|
||||
|
||||
Undo logic must use the height of the block being undone:
|
||||
|
||||
```rust
|
||||
let block_height = height_of_block_being_undone;
|
||||
|
||||
if implementation_approved && block_height >= activation_block {
|
||||
undo_new_behavior(transaction, db)?;
|
||||
} else {
|
||||
undo_original_behavior(transaction, db)?;
|
||||
}
|
||||
```
|
||||
|
||||
If activation occurred at block `50,000`:
|
||||
|
||||
- Undoing block `50,000` uses the new undo behavior.
|
||||
- Undoing block `49,999` uses the original undo behavior.
|
||||
|
||||
This allows orphan correction to move backward and forward across an activation boundary without corrupting chain state.
|
||||
|
||||
## Later Revisions
|
||||
|
||||
A later CLP may revise an earlier consensus change.
|
||||
|
||||
For example:
|
||||
|
||||
- Original rules apply through block `49,999`.
|
||||
- The first implementation applies from block `50,000` through `99,999`.
|
||||
- A revised implementation applies beginning at block `100,000`.
|
||||
|
||||
That code checks both activation boundaries:
|
||||
|
||||
```rust
|
||||
if second_approved && block_height >= second_activation_block {
|
||||
apply_second_rules(...)
|
||||
} else if first_approved && block_height >= first_activation_block {
|
||||
apply_first_rules(...)
|
||||
} else {
|
||||
apply_original_rules(...)
|
||||
}
|
||||
```
|
||||
|
||||
Do not remove older rule paths. Nodes require them for complete historical validation.
|
||||
|
||||
## Paths Developers Must Review
|
||||
|
||||
Apply the activation condition everywhere the changed consensus rule is interpreted:
|
||||
|
||||
- Transaction parsing.
|
||||
- Mempool validation.
|
||||
- Received-block validation.
|
||||
- Local block saving.
|
||||
- Initial synchronization.
|
||||
- Record and balance updates.
|
||||
- Pending effects.
|
||||
- Orphan replay.
|
||||
- Block undo and chain rewinds.
|
||||
- Database reconstruction.
|
||||
- Validation CLI tools.
|
||||
|
||||
A change is incomplete if one path applies the new rule while another still applies the original rule at the same block height.
|
||||
|
||||
## Creating an Activation Vote
|
||||
|
||||
After reviewing and installing the candidate implementation, eligible node operators use:
|
||||
|
||||
```bash
|
||||
./create_activation_vote_tx \
|
||||
|
|
@ -238,9 +250,11 @@ They can then create an activation vote:
|
|||
yes
|
||||
```
|
||||
|
||||
Voting `yes` approves the exact development hash produced from that document. Voting `no` rejects that candidate, not every possible future implementation of the proposal.
|
||||
The first vote for a ProposalKey and development hash creates that implementation candidate.
|
||||
|
||||
## Confirming the Schedule
|
||||
Voting `yes` approves the exact implementation-document hash. Voting `no` rejects that candidate without rejecting every future implementation of the proposal.
|
||||
|
||||
## Checking Activation
|
||||
|
||||
Use:
|
||||
|
||||
|
|
@ -248,48 +262,29 @@ Use:
|
|||
./lookup_governance_proposal <proposal_key>
|
||||
```
|
||||
|
||||
For each implementation candidate, the response identifies:
|
||||
The response shows each implementation candidate, its development hash, vote totals, activation state, and activation block.
|
||||
|
||||
- `development_hash`
|
||||
- `development_location`
|
||||
- `state`
|
||||
- `current_vote`
|
||||
- `final_vote`
|
||||
- `activation_block`
|
||||
The possible states are:
|
||||
|
||||
The implementation state is:
|
||||
- `open`: The implementation has not reached approval.
|
||||
- `scheduled`: It was approved, but its activation block has not arrived.
|
||||
- `active`: The local chain tip has reached or passed its activation block.
|
||||
|
||||
- `open`: Activation voting has not finalized.
|
||||
- `scheduled`: Voting finalized, but the activation block has not arrived.
|
||||
- `active`: The local chain height has reached or passed the activation block.
|
||||
The displayed state is informational. Consensus code uses `implementation_activation_status` and compares the specific block being processed with the returned activation block.
|
||||
|
||||
The reported state is informational. Consensus code must still compare the height of the specific block being validated against the stored activation block.
|
||||
## Testing Requirements
|
||||
|
||||
## Release Requirements
|
||||
Before asking node operators to approve an implementation, test:
|
||||
|
||||
Before asking operators to vote for activation:
|
||||
- Blocks immediately before and at activation.
|
||||
- Blocks created after activation.
|
||||
- Synchronization from before the activation block.
|
||||
- Historical validation across the activation block.
|
||||
- Undo from after activation to before activation.
|
||||
- Replay from before activation through activation.
|
||||
- Orphan correction across the boundary.
|
||||
- Candidate nodes operating before activation.
|
||||
- Malformed or mismatched activation records.
|
||||
|
||||
- Publish reproducible source and build instructions.
|
||||
- Publish the implementation document.
|
||||
- Identify the exact development hash operators should expect.
|
||||
- Provide tests that cross the activation boundary.
|
||||
- Test startup and sync from before the activation block.
|
||||
- Test rollback from after activation to before activation.
|
||||
- Test replay from before activation through activation.
|
||||
- Test mixed candidate and existing nodes during the delay.
|
||||
- Confirm every consensus path uses the same boundary.
|
||||
The same block must produce the same result whether it was mined locally, received live, downloaded during sync, or replayed during orphan correction.
|
||||
|
||||
Node operators need enough time to review, build, install, and test the candidate before voting completes.
|
||||
|
||||
## Failure Behavior
|
||||
|
||||
Consensus activation checks must fail safely.
|
||||
|
||||
- A malformed activation record must produce an error, not silently activate.
|
||||
- A different development hash must not activate the compiled implementation.
|
||||
- A missing schedule must retain the old behavior.
|
||||
- Arithmetic around activation heights must not wrap.
|
||||
- Historical validation must never depend on wall-clock time.
|
||||
- Repository availability must not determine whether an already-recorded activation applies.
|
||||
|
||||
The on-chain ProposalKey, approved development hash, and activation block are the authoritative consensus records. The repository explains those records and makes their source auditable, but it does not activate them by itself.
|
||||
|
|
|
|||
Loading…
Reference in New Issue