Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,34 @@ make ( or make build-with-ui )
make test
```

## Local Development with Anvil

For local development and testing, you can use Anvil (local Ethereum node) with your IPC keystore accounts:

```sh
# Quick start - setup Anvil with all IPC keystore accounts funded
./scripts/setup-anvil-with-ipc-keys.sh

# Stop Anvil
/tmp/stop-anvil-ipc.sh

# Save/load Anvil state for persistence across restarts
./scripts/anvil-persistent-state.sh save
./scripts/anvil-persistent-state.sh load
```

This automatically:
- Starts Anvil on port 8545 with chain ID 31337
- Funds all addresses from `~/.ipc/evm_keystore.json` with 10,000 ETH each
- Uses deterministic accounts for consistent testing

For detailed documentation, see [scripts/ANVIL_IPC_SETUP.md](./scripts/ANVIL_IPC_SETUP.md).

**Optional**: Source convenience aliases:
```sh
source scripts/aliases.sh # Adds anvil-start, anvil-stop, etc.
```

## Code organization

- `ipc/cli`: A Rust binary crate for our client `ipc-cli` application that provides a simple and easy-to-use interface to interact with IPC as a user and run all the processes required for the operation of a subnet.
Expand Down
8 changes: 4 additions & 4 deletions contracts/contracts/lib/cometbft/CometbftLightClient.sol
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ library CometbftLightClient {
/// @dev Verifies that signatures meet BFT consensus requirements (>2/3 voting power)
///
/// @param header The light client header containing the block header
/// @param voteTemplate The canonical vote tempalted filled except for the timestamp and chain id. The chain id will be
/// assigned in the contract while timestamp is take from the signatures. The rest of the fields in voteTemplate should be
/// @param voteTemplate The canonical vote tempalted filled except for the timestamp and chain id. The chain id will be
/// assigned in the contract while timestamp is take from the signatures. The rest of the fields in voteTemplate should be
/// the same for all validators.
/// @param certificate The validator certificate with each signature and signing timestamp
///
Expand All @@ -82,7 +82,7 @@ library CometbftLightClient {
function verifyValidatorsQuorum(LightHeader.Data memory header, ValidatorCertificate memory certificate, CanonicalVote.Data memory voteTemplate) internal view {
prepareParams(header, voteTemplate);

// make sure the vote template block hash matches the header, so that
// make sure the vote template block hash matches the header, so that
// the validators are signing the same light client header hash.
checkCommitHash(header, toBytes32(voteTemplate.block_id.hash));

Expand Down Expand Up @@ -219,7 +219,7 @@ library CometbftLightClient {
}
}

/// @dev This method hash LightHeader.Data into a bytes32 hash that is exactly how cometbft go client
/// @dev This method hash LightHeader.Data into a bytes32 hash that is exactly how cometbft go client
/// does it. This code is taken from tendermint-sol/contracts/proto/TendermintHelper.sol#hash method.
/// The original method takes SignedHeader.Data as parameter, while this contract requires LightHeader.Data,
/// immplementations are the same.
Expand Down
38 changes: 37 additions & 1 deletion fendermint/eth/deployer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,8 @@ impl EthContractDeployer {
use ipc_api::subnet_id::SubnetID;

let ipc_params = GatewayParams::new(SubnetID::new(self.chain_id, vec![]));
let params = GatewayConstructor::new(ipc_params, vec![])
let commit_sha = get_commit_sha();
let params = GatewayConstructor::new(ipc_params, vec![], commit_sha)
.context("failed to create gateway constructor parameters")?;

let facets = self
Expand Down Expand Up @@ -325,3 +326,38 @@ impl EthContractDeployer {
)
}
}

/// Get the commit SHA for contract deployment.
/// Uses the COMMIT_SHA environment variable if set, otherwise uses git,
/// or falls back to a default value.
fn get_commit_sha() -> [u8; 32] {
// Try to get from environment variable first (matches TypeScript deployment)
if let Ok(sha) = std::env::var("COMMIT_SHA") {
let mut result = [0u8; 32];
let bytes = sha.as_bytes();
let len = bytes.len().min(32);
result[..len].copy_from_slice(&bytes[..len]);
return result;
}

// Try to get from git
if let Ok(output) = std::process::Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.output()
{
if output.status.success() {
let sha = String::from_utf8_lossy(&output.stdout).trim().to_string();
let mut result = [0u8; 32];
let bytes = sha.as_bytes();
let len = bytes.len().min(32);
result[..len].copy_from_slice(&bytes[..len]);
return result;
}
}

// Fall back to default value (matches test default)
let default_sha = b"c7d8f53f";
let mut result = [0u8; 32];
result[..default_sha.len()].copy_from_slice(default_sha);
result
}
4 changes: 4 additions & 0 deletions fendermint/vm/actor_interface/src/ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,12 +350,14 @@ pub mod gateway {
pub majority_percentage: u8,
pub network_name: GatewaySubnetID,
pub validators: Vec<GatewayValidator>,
pub commit_sha: [u8; 32],
}

impl ConstructorParameters {
pub fn new(
params: GatewayParams,
validators: Vec<Validator<Collateral>>,
commit_sha: [u8; 32],
) -> anyhow::Result<Self> {
// Every validator has an Ethereum address.
let validators = validators
Expand All @@ -380,6 +382,7 @@ pub mod gateway {
majority_percentage: params.majority_percentage,
network_name: GatewaySubnetID { root, route },
validators,
commit_sha,
})
}
}
Expand Down Expand Up @@ -420,6 +423,7 @@ pub mod gateway {
metadata: Bytes::new(),
}],
active_validators_limit: 100,
commit_sha: [0u8; 32],
};

// It looks like if we pass just the record then it will be passed as 5 tokens,
Expand Down
14 changes: 13 additions & 1 deletion fendermint/vm/interpreter/src/genesis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,16 @@ struct DeployConfig<'a> {
deployer_addr: ethers::types::Address,
}

/// Get the commit SHA for genesis contract deployment.
/// For genesis, we use a default value as genesis is typically built at compile time.
fn get_genesis_commit_sha() -> [u8; 32] {
// Use default value for genesis (matches test default)
let default_sha = b"c7d8f53f";
let mut result = [0u8; 32];
result[..default_sha.len()].copy_from_slice(default_sha);
result
}

fn deploy_contracts(
ipc_contracts: Vec<ContractSourceAndName>,
top_level_contracts: &EthContractMap,
Expand Down Expand Up @@ -539,7 +549,9 @@ fn deploy_contracts(
GatewayParams::new(SubnetID::new(config.chain_id.into(), vec![]))
};

let params = ConstructorParameters::new(ipc_params, validators)
// Get commit SHA for genesis deployment
let commit_sha = get_genesis_commit_sha();
let params = ConstructorParameters::new(ipc_params, validators, commit_sha)
.context("failed to create gateway constructor")?;

let facets = deployer
Expand Down
1 change: 1 addition & 0 deletions ipc-ui/frontend/src/stores/wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface SubnetConfig {
supplySourceAddress?: string
minCrossMsgFee?: number
genesisSubnetIpcContractsOwner?: string
chainId?: number

// Advanced configuration (optional)
activeValidatorsLimit?: number
Expand Down
16 changes: 15 additions & 1 deletion ipc-ui/frontend/src/views/wizard/BasicConfigView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const formData = ref({
supplySourceAddress: wizardStore.config.supplySourceAddress || '',
minCrossMsgFee: wizardStore.config.minCrossMsgFee || 0.000001,
genesisSubnetIpcContractsOwner: wizardStore.config.genesisSubnetIpcContractsOwner || '',
chainId: wizardStore.config.chainId || Math.floor(100000 + Math.random() * 900000),
gatewayMode: wizardStore.config.gatewayMode || 'deploy',
customGatewayAddress: wizardStore.config.customGatewayAddress || '',
customRegistryAddress: wizardStore.config.customRegistryAddress || '',
Expand Down Expand Up @@ -115,7 +116,7 @@ const validateForm = () => {
fieldErrors.value = {}

// Validate all required fields
const requiredFields = ['parent', 'minValidatorStake', 'minValidators', 'bottomupCheckPeriod', 'permissionMode', 'supplySourceKind', 'genesisSubnetIpcContractsOwner', 'gatewayMode']
const requiredFields = ['parent', 'minValidatorStake', 'minValidators', 'bottomupCheckPeriod', 'chainId', 'permissionMode', 'supplySourceKind', 'genesisSubnetIpcContractsOwner', 'gatewayMode']
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Missing Validation Case for chainId

The chainId field was added to the requiredFields array and form validation, but no corresponding validation case was added to the validateField function in the wizard store. When the validation runs, wizardStore.validateField('chainId', value) will fall through the switch statement and return null (no error), even if the chainId is missing or invalid. This means the chainId field can never show validation errors despite being marked as required and having error display binding in the template.

Fix in Cursor Fix in Web


requiredFields.forEach(field => {
validateField(field)
Expand Down Expand Up @@ -557,6 +558,19 @@ const selectGateway = (gatewayId: string) => {
/>
</div>

<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<FormInput
v-model="formData.chainId"
type="number"
label="Subnet Chain ID"
placeholder="123456"
required
:error="fieldErrors.chainId"
help-text="Unique chain ID for this subnet (must be globally unique)"
@blur="handleFieldBlur('chainId')"
/>
</div>

<FormSelect
v-model="formData.supplySourceKind"
label="Supply Source"
Expand Down
4 changes: 4 additions & 0 deletions ipc/cli/src/commands/ui/api/deployment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ async fn handle_deploy_request(
.await;
}
Err(e) => {
// Log the full error with context
log::error!("❌ Deployment {} failed with error: {:?}", deploy_id, e);
log::error!("Error details: {}", e);

// Broadcast failure
broadcast_progress(
&deploy_state,
Expand Down
Loading
Loading