Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/subnet contract versioning #274

Merged
merged 10 commits into from
Jun 6, 2023
28 changes: 28 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
docker_tag := "latest"
docker_registry := "localhost:5000"
docker_image := docker_registry + "/subnet-node:" + docker_tag

# Print help message
help:
@just --list --unsorted
@echo ""
@echo "Available variables and default values:"
@just --evaluate

# Build subnets node
build *args:
#!/usr/bin/env bash
pushd testnet/stacks-node
cargo build {{args}}
popd

# Build release version subnets node
build-release: (build "--features" "monitoring_prom,slog_json" "--release")

# Build docker image
docker-build:
docker build -t {{docker_image}} .

# Build and push docker image
docker-push: docker-build
docker push {{docker_image}}
30 changes: 29 additions & 1 deletion core-contracts/contracts/multi-miner.clar
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
(define-constant ERR_INVALID_SIGNATURE 103)
(define-constant ERR_UNAUTHORIZED_CONTRACT_CALLER 104)
(define-constant ERR_MINER_ALREADY_SET 105)
(define-constant ERR_UNSUPPORTED_SUBNET_CONTRACT_VERSION 106)

;; SIP-018 Constants
(define-constant sip18-prefix 0x534950303138)
Expand All @@ -19,6 +20,33 @@
;; List of miners
(define-data-var miners (optional (list 10 principal)) none)

;; Minimun version of subnet contract required
(define-constant SUBNET_CONTRACT_VERSION_MIN {
major: 2,
minor: 0,
patch: 0,
})

;; Return error if subnet contract version not supported
(define-read-only (check-subnet-contract-version) (
let (
(subnet-contract-version (contract-call? .subnet-v2-0-0 get-version))
)

;; Check subnet contract version is greater than min supported version
(asserts! (is-eq (get major subnet-contract-version) (get major SUBNET_CONTRACT_VERSION_MIN)) (err ERR_UNSUPPORTED_SUBNET_CONTRACT_VERSION))
(asserts! (>= (get minor subnet-contract-version) (get minor SUBNET_CONTRACT_VERSION_MIN)) (err ERR_UNSUPPORTED_SUBNET_CONTRACT_VERSION))
;; Only check patch version if major and minor version are equal
(asserts! (or
(not (is-eq (get minor subnet-contract-version) (get minor SUBNET_CONTRACT_VERSION_MIN)))
(>= (get patch subnet-contract-version) (get patch SUBNET_CONTRACT_VERSION_MIN)))
(err ERR_UNSUPPORTED_SUBNET_CONTRACT_VERSION))
(ok true)
))

;; Fail if the subnet contract is not compatible
(try! (check-subnet-contract-version))

(define-private (get-miners)
(unwrap-panic (var-get miners)))

Expand Down Expand Up @@ -83,6 +111,6 @@
;; check that we have enough signatures
(try! (check-miners (append (get signers signer-principals) tx-sender)))
;; execute the block commit
(as-contract (contract-call? .subnet commit-block (get block block-data) (get subnet-block-height block-data) (get target-tip block-data) (get withdrawal-root block-data)))
(as-contract (contract-call? .subnet-v2-0-0 commit-block (get block block-data) (get subnet-block-height block-data) (get target-tip block-data) (get withdrawal-root block-data)))
)
)
18 changes: 18 additions & 0 deletions core-contracts/contracts/subnet.clar
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

(define-constant CONTRACT_ADDRESS (as-contract tx-sender))

;; Versioning of this contract
;; Must follow Semver rules: https://semver.org/
;; NOTE: Versioning was added as of `2.0.0`
;; NOTE: Contract should be deployed with name matching version here
(define-constant VERSION {
major: 2,
minor: 0,
patch: 0,
prerelease: "",
metadata: ""
})

;; Error codes
(define-constant ERR_BLOCK_ALREADY_COMMITTED 1)
(define-constant ERR_INVALID_MINER 2)
Expand Down Expand Up @@ -44,6 +56,12 @@
(use-trait ft-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait)
(use-trait mint-from-subnet-trait .subnet-traits.mint-from-subnet-trait)

;; Get the version of this contract
;; Returns a tuple containing the 5 Semver fields: major, minor, patch, prerelease, and metadata
(define-read-only (get-version)
VERSION
)

;; Update the miner for this contract.
(define-public (update-miner (new-miner principal))
(begin
Expand Down
117 changes: 115 additions & 2 deletions testnet/stacks-node/src/burnchains/l1_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ use stacks::chainstate::stacks::miner::SignedProposal;
use stacks::chainstate::stacks::StacksTransaction;
use stacks::codec::StacksMessageCodec;
use stacks::core::StacksEpoch;
use stacks::net::CallReadOnlyRequestBody;
use stacks::util::hash::hex_bytes;
use stacks::util::sleep_ms;
use stacks::util_lib::boot::boot_code_addr;
use stacks_common::types::chainstate::{BlockHeaderHash, BurnchainHeaderHash, StacksBlockId};

use super::commitment::{Layer1Committer, MultiPartyCommitter};
Expand Down Expand Up @@ -46,6 +48,28 @@ pub struct L1Controller {
chain_tip: Option<BurnchainTip>,

committer: Box<dyn Layer1Committer + Send>,

l1_contract_check_passed: bool,
}

/// Semver version of a Clarity contract
#[derive(Deserialize, Serialize)]
pub struct ContractVersion {
major: u32,
minor: u32,
patch: u32,
prerelease: Option<String>,
metadata: Option<String>,
}

/// Response from read-only function
#[derive(Deserialize, Serialize)]
pub struct GetVersionResponse {
okay: bool,
/// Response will contain `result` on success
result: Option<ContractVersion>,
/// Response will contain `cause` on failure
cause: Option<String>,
}

impl L1Channel {
Expand Down Expand Up @@ -109,7 +133,7 @@ impl L1Controller {
other_participants.clone(),
)),
};
Ok(L1Controller {
let l1_controller = L1Controller {
burnchain,
config,
indexer,
Expand All @@ -119,7 +143,9 @@ impl L1Controller {
coordinator,
chain_tip: None,
committer,
})
l1_contract_check_passed: false,
};
Ok(l1_controller)
}

fn receive_blocks(
Expand Down Expand Up @@ -227,6 +253,91 @@ impl L1Controller {
Err(Error::RPCError(res.text()?))
}
}

/// Return the Semver version of the `subnet.clar` contract this node is configured to use
fn get_l1_contract_version(&self) -> Result<ContractVersion, Error> {
let burn_conf = &self.config.burnchain;
let url = format!(
"{http_origin}/v2/contracts/call-read/{contract_addr}/{contract}/get-version",
http_origin = self.l1_rpc_interface(),
contract_addr = burn_conf.contract_identifier.issuer,
contract = burn_conf.contract_identifier.name,
);

let body = CallReadOnlyRequestBody {
sender: boot_code_addr(self.config.is_mainnet()).to_string(),
arguments: Vec::default(),
};

let response = reqwest::blocking::Client::new()
.post(url)
.header("Content-Type", "application/octet-stream")
.json(&body)
.send()?
.error_for_status()?
.json::<GetVersionResponse>()
.map_err(Error::from)?;

if !response.okay {
let message = response
.cause
.unwrap_or_else(|| "Unknown contract error".to_string());
return Err(Error::RPCError(message));
}

match response.result {
Some(r) => Ok(r),
None => Err(Error::RPCError("Empty result".to_string())),
}
}

/// Check that the version of `subnet.clar` the node is configured to use is supported
fn check_l1_contract_version(&self) -> Result<(), Error> {
const EXACT_MAJOR_VERSION: u32 = 2;
const MINIMUM_MINOR_VERSION: u32 = 0;
const MINIMUM_PATCH_VERSION: u32 = 0;
let ContractVersion {
major,
minor,
patch,
..
} = self.get_l1_contract_version()?;

if major != EXACT_MAJOR_VERSION {
let msg = format!("Major version must be {EXACT_MAJOR_VERSION} (found {major})");
return Err(Error::UnsupportedBurnchainContract(msg));
};
if minor < MINIMUM_MINOR_VERSION {
let msg =
format!("Minor version must be at least {MINIMUM_MINOR_VERSION} (found {minor})");
return Err(Error::UnsupportedBurnchainContract(msg));
};
if minor == MINIMUM_MINOR_VERSION && patch < MINIMUM_PATCH_VERSION {
let msg =
format!("Patch version must be at least {MINIMUM_PATCH_VERSION} (found {patch})");
return Err(Error::UnsupportedBurnchainContract(msg));
};
Ok(())
}

/// Check that the version of `subnet.clar` the node is configured to use is supported
fn l1_contract_ok(&mut self) -> Result<(), Error> {
match self.l1_contract_check_passed {
true => Ok(()),
false => match self.check_l1_contract_version() {
// This error is fatal. We can't continue with wrong contract version
Err(Error::UnsupportedBurnchainContract(e)) => {
panic!("Unsupported burnchain contract version: {e}")
}
// Error, but not fatal
Err(e) => Err(e),
Ok(_) => {
self.l1_contract_check_passed = true;
Ok(())
}
},
}
}
}

impl BurnchainController for L1Controller {
Expand Down Expand Up @@ -270,6 +381,8 @@ impl BurnchainController for L1Controller {
op_signer: &mut BurnchainOpSigner,
attempt: u64,
) -> Result<Txid, Error> {
self.l1_contract_ok()?;

let tx = self.committer.make_commit_tx(
committed_block_hash,
committed_block_height,
Expand Down
13 changes: 9 additions & 4 deletions testnet/stacks-node/src/burnchains/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ mod tests;
#[derive(Debug)]
pub enum Error {
UnsupportedBurnchain(String),
/// Problem with contract deployed on burnchain
UnsupportedBurnchainContract(String),
CoordinatorClosed,
IndexerError(burnchains::Error),
RPCError(String),
Expand All @@ -51,12 +53,15 @@ impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::UnsupportedBurnchain(ref chain_name) => {
write!(f, "Burnchain is not supported: {:?}", chain_name)
write!(f, "Burnchain is not supported: {chain_name:?}")
}
Error::UnsupportedBurnchainContract(ref msg) => {
write!(f, "Burnchain contract is not supported: {msg}")
}
Error::CoordinatorClosed => write!(f, "ChainsCoordinator closed"),
Error::IndexerError(ref e) => write!(f, "Indexer error: {:?}", e),
Error::RPCError(ref e) => write!(f, "ControllerError(RPCError: {})", e),
Error::BadCommitment(ref e) => write!(f, "ControllerError(BadCommitment: {}))", e),
Error::IndexerError(ref e) => write!(f, "Indexer error: {e:?}"),
Error::RPCError(ref e) => write!(f, "ControllerError(RPCError: {e})"),
Error::BadCommitment(ref e) => write!(f, "ControllerError(BadCommitment: {e}))"),
}
}
}
Expand Down
12 changes: 6 additions & 6 deletions testnet/stacks-node/src/neon_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -817,7 +817,7 @@ fn spawn_miner_relayer(
Vec<(AssembledAnchorBlock, Secp256k1PrivateKey)>,
> = HashMap::new();

let mut bitcoin_controller = config
let mut burnchain_controller = config
.make_burnchain_controller(coord_comms.clone())
.expect("couldn't create burnchain controller");
let mut microblock_miner_state: Option<MicroblockMinerState> = None;
Expand Down Expand Up @@ -1031,7 +1031,7 @@ fn spawn_miner_relayer(
burn_tenure_snapshot,
&mut keychain,
&mut mem_pool,
&mut *bitcoin_controller,
&mut *burnchain_controller,
&last_mined_blocks_vec.iter().map(|(blk, _)| blk).collect(),
&event_dispatcher,
);
Expand Down Expand Up @@ -1562,7 +1562,7 @@ impl StacksNode {
burn_block: BlockSnapshot,
keychain: &mut Keychain,
mem_pool: &mut MemPoolDB,
bitcoin_controller: &mut (dyn BurnchainController + Send),
burnchain_controller: &mut (dyn BurnchainController + Send),
last_mined_blocks: &Vec<&AssembledAnchorBlock>,
event_dispatcher: &EventDispatcher,
) -> Option<(AssembledAnchorBlock, Secp256k1PrivateKey)> {
Expand Down Expand Up @@ -1901,7 +1901,7 @@ impl StacksNode {
let withdrawal_merkle_root = anchored_block.header.withdrawal_merkle_root;

let mut op_signer = keychain.generate_op_signer();
let required_signatures = bitcoin_controller.commit_required_signatures();
let required_signatures = burnchain_controller.commit_required_signatures();
let signatures = if required_signatures > 0 {
// if we need to collect signatures, assemble the proposal and send to other participants
let proposal = Proposal {
Expand All @@ -1928,7 +1928,7 @@ impl StacksNode {

(0..required_signatures)
.filter_map(|participant_index| {
match bitcoin_controller.propose_block(participant_index, &proposal) {
match burnchain_controller.propose_block(participant_index, &proposal) {
Ok(signature) => Some(signature),
Err(rejection) => {
warn!("Failed to obtain approval"; "error" => %rejection);
Expand Down Expand Up @@ -2001,7 +2001,7 @@ impl StacksNode {
"attempt" => attempt
);

let res = bitcoin_controller.submit_commit(
let res = burnchain_controller.submit_commit(
committed_block_hash,
block_height,
target_burn_hash,
Expand Down