Skip to content
Open
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
6 changes: 6 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

118 changes: 118 additions & 0 deletions docs/storage-cli-quickstart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Storage CLI Quickstart

This guide walks you through testing the IPC decentralized storage CLI on the test subnet.

## Prerequisites

Build the CLI (macOS, targeting the local machine):

```bash
cargo build --release -p ipc-cli --features ipc-storage
```

Make sure you have an IPC wallet set up (`~/.ipc/config.toml` with an EVM key).
If not, create one:

```bash
./target/release/ipc-cli wallet new --wallet-type evm
./target/release/ipc-cli wallet set-default --wallet-type evm --address <0xYOUR_ADDRESS>
```

## Step 1: Fund your account on the storage subnet

Send tokens from the parent chain (calibnet) into the storage subnet:

```bash
./target/release/ipc-cli cross-msg fund \
--subnet "/r314159/t410fg32br4ow4kdhp3wssi6c4xumsdpjzhw6y4ydbxq" \
--from 0xYOUR_ADDRESS \
--to 0xYOUR_ADDRESS \
60
```

Wait for the top-down message to be finalized (up to ~3 minutes), then verify your balance:

```bash
curl http://136.115.12.207:8545 \
-H 'content-type: application/json' \
--data '{"jsonrpc":"2.0","method":"eth_getBalance","params":["0xYOUR_ADDRESS","latest"],"id":1}'
```

A non-zero `result` means your account is funded.

## Step 2: Initialize the storage client

```bash
./target/release/ipc-cli storage client init \
--rpc-url http://136.115.12.207:26657 \
--gateway-url http://136.115.12.207:8080
```

This creates `~/.ipc/storage/client/config.yaml`. The CLI uses your default EVM wallet key for signing transactions.

## Step 3: Run the test suite

```bash
./test.sh
```

The script automatically:
1. Buys storage credit (0.1 FIL)
2. Creates a bucket (or reuses an existing one)
3. Tests all 18 operations: upload, list, stat, cat, download, recursive upload/download, move, delete

Phase 1 (steps 2-12) tests read/write operations immediately.
Phase 2 (steps 13-18) waits 90 seconds for blob finalization, then tests move and delete.

## Manual commands

Once initialized, you can use any storage command directly:

```bash
# Buy storage credit
./target/release/ipc-cli storage client credit buy 0.1

# Create a bucket
./target/release/ipc-cli storage client bucket create

# List buckets
./target/release/ipc-cli storage client bucket list

# Upload a file
./target/release/ipc-cli storage client cp /path/to/file.txt ipc://BUCKET/key.txt --gateway http://136.115.12.207:8080

# Upload a directory
./target/release/ipc-cli storage client cp -r /path/to/dir ipc://BUCKET/prefix --gateway http://136.115.12.207:8080

# List objects
./target/release/ipc-cli storage client ls ipc://BUCKET/

# Get object metadata
./target/release/ipc-cli storage client stat ipc://BUCKET/key.txt

# Read file contents
./target/release/ipc-cli storage client cat ipc://BUCKET/key.txt --gateway http://136.115.12.207:8080

# Download a file
./target/release/ipc-cli storage client cp ipc://BUCKET/key.txt /local/path.txt --gateway http://136.115.12.207:8080

# Move/rename
./target/release/ipc-cli storage client mv ipc://BUCKET/old.txt ipc://BUCKET/new.txt --gateway http://136.115.12.207:8080

# Delete
./target/release/ipc-cli storage client rm --force ipc://BUCKET/key.txt

# Delete recursively
./target/release/ipc-cli storage client rm -r --force ipc://BUCKET/prefix/

# Check credit info
./target/release/ipc-cli storage client credit info
```

Replace `BUCKET` with your bucket address (e.g. `t0123`).

## Notes

- **Blob finalization**: After uploading, blobs take ~10-15 seconds to be finalized by the storage node. Until finalized, delete and move operations will fail with "blob pending finalization".
- **Gateway URL**: The `--gateway` flag is required for commands that transfer data (cp, cat, mv). Read-only commands (ls, stat, credit info, bucket list) only need the RPC.
- **Overwrite**: Use `--overwrite` with `cp` to replace an existing object.
89 changes: 89 additions & 0 deletions fendermint/actors/blobs/shared/src/execution.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright 2026 Recall Contributors
// SPDX-License-Identifier: Apache-2.0, MIT

use fvm_ipld_encoding::tuple::*;
use fvm_shared::{address::Address, clock::ChainEpoch};
use serde::{Deserialize, Serialize};

use crate::bytes::B256;

// FEVM InvokeContract selectors used by blobs actor facade for execution methods.
pub const CREATE_JOB_SELECTOR: [u8; 4] = [0x6b, 0xa4, 0x8d, 0x87];
pub const CLAIM_JOB_SELECTOR: [u8; 4] = [0x9c, 0x7d, 0xd2, 0x19];
pub const COMPLETE_JOB_SELECTOR: [u8; 4] = [0x59, 0x2f, 0x72, 0xc4];
pub const FAIL_JOB_SELECTOR: [u8; 4] = [0xf5, 0xe2, 0x2c, 0x70];

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum JobStatus {
Pending,
Claimed,
Running,
Succeeded,
Failed,
TimedOut,
}

#[derive(Clone, Debug, Serialize_tuple, Deserialize_tuple)]
pub struct ExecutionJob {
pub id: u64,
pub creator: Address,
pub claimed_by: Option<Address>,
pub status: JobStatus,
pub binary_ref: String,
pub input_refs: Vec<String>,
pub args: Vec<String>,
pub env: Vec<(String, String)>,
pub timeout_secs: u64,
pub created_epoch: ChainEpoch,
pub started_epoch: Option<ChainEpoch>,
pub completed_epoch: Option<ChainEpoch>,
pub output_refs: Vec<String>,
pub output_commitment: Option<B256>,
pub exit_code: Option<i32>,
pub error: Option<String>,
}

#[derive(Clone, Debug, Serialize_tuple, Deserialize_tuple)]
pub struct CreateJobParams {
pub binary_ref: String,
pub input_refs: Vec<String>,
pub args: Vec<String>,
pub env: Vec<(String, String)>,
pub timeout_secs: u64,
}

#[derive(Clone, Debug, Serialize_tuple, Deserialize_tuple)]
pub struct ClaimJobParams {
pub id: u64,
}

#[derive(Clone, Debug, Serialize_tuple, Deserialize_tuple)]
pub struct CompleteJobParams {
pub id: u64,
pub output_refs: Vec<String>,
pub output_commitment: B256,
pub exit_code: i32,
}

#[derive(Clone, Debug, Serialize_tuple, Deserialize_tuple)]
pub struct FailJobParams {
pub id: u64,
pub reason: String,
pub exit_code: i32,
}

#[derive(Clone, Debug, Serialize_tuple, Deserialize_tuple)]
pub struct GetJobParams {
pub id: u64,
}

#[derive(Clone, Debug, Serialize_tuple, Deserialize_tuple)]
pub struct ListJobsParams {
pub status: Option<JobStatus>,
pub limit: u32,
}

#[derive(Clone, Debug, Serialize_tuple, Deserialize_tuple)]
pub struct ListJobsReturn {
pub jobs: Vec<ExecutionJob>,
}
1 change: 1 addition & 0 deletions fendermint/actors/blobs/shared/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod accounts;
pub mod blobs;
pub mod bytes;
pub mod credit;
pub mod execution;
pub mod method;
pub mod operators;
pub mod sdk;
Expand Down
8 changes: 8 additions & 0 deletions fendermint/actors/blobs/shared/src/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,12 @@ pub enum Method {
RegisterNodeOperator = frc42_dispatch::method_hash!("RegisterNodeOperator"),
GetOperatorInfo = frc42_dispatch::method_hash!("GetOperatorInfo"),
GetActiveOperators = frc42_dispatch::method_hash!("GetActiveOperators"),

// Execution methods (MVP in blobs actor)
CreateJob = frc42_dispatch::method_hash!("CreateJob"),
ClaimJob = frc42_dispatch::method_hash!("ClaimJob"),
CompleteJob = frc42_dispatch::method_hash!("CompleteJob"),
FailJob = frc42_dispatch::method_hash!("FailJob"),
GetJob = frc42_dispatch::method_hash!("GetJob"),
ListJobs = frc42_dispatch::method_hash!("ListJobs"),
}
67 changes: 66 additions & 1 deletion fendermint/actors/blobs/src/actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::{
};

mod admin;
mod execution;
mod metrics;
mod system;
mod user;
Expand Down Expand Up @@ -52,7 +53,63 @@ impl BlobsActor {
params: InvokeContractParams,
) -> Result<InvokeContractReturn, ActorError> {
let input_data: InputData = params.try_into()?;
if sol_blobs::can_handle(&input_data) {
if sol_blobs::is_register_node_operator_call(&input_data) {
let params = sol_blobs::parse_register_node_operator_input(&input_data)?;
let params = fendermint_actor_blobs_shared::operators::RegisterNodeOperatorParams {
bls_pubkey: params.bls_pubkey,
rpc_url: params.rpc_url,
};
let _ = Self::register_node_operator(rt, params)?;
Ok(InvokeContractReturn {
output_data: Vec::new(),
})
} else if sol_blobs::is_get_operator_info_call(&input_data) {
let params = sol_blobs::parse_get_operator_info_input(&input_data)?;
let address = rt
.resolve_address(&params.address)
.map(fvm_shared::address::Address::new_id)
.unwrap_or(params.address);
let info = Self::get_operator_info(
rt,
fendermint_actor_blobs_shared::operators::GetOperatorInfoParams { address },
)?;
let output_data = sol_blobs::encode_get_operator_info_output(info)?;
Ok(InvokeContractReturn { output_data })
} else if sol_blobs::is_get_active_operators_call(&input_data) {
let operators = Self::get_active_operators(rt)?;
let output_data = sol_blobs::encode_get_active_operators_output(operators.operators)?;
Ok(InvokeContractReturn { output_data })
} else if sol_blobs::is_create_job_call(&input_data) {
let params = sol_blobs::parse_create_job_input(&input_data)?;
let _ = Self::create_job(rt, params.into())?;
Ok(InvokeContractReturn {
output_data: Vec::new(),
})
} else if sol_blobs::is_claim_job_call(&input_data) {
let params = sol_blobs::parse_claim_job_input(&input_data)?;
let _ = Self::claim_job(rt, params.into())?;
Ok(InvokeContractReturn {
output_data: Vec::new(),
})
} else if sol_blobs::is_complete_job_call(&input_data) {
let params = sol_blobs::parse_complete_job_input(&input_data)?;
let _ = Self::complete_job(rt, params.into())?;
Ok(InvokeContractReturn {
output_data: Vec::new(),
})
} else if sol_blobs::is_fail_job_call(&input_data) {
let params = sol_blobs::parse_fail_job_input(&input_data)?;
let _ = Self::fail_job(rt, params.into())?;
Ok(InvokeContractReturn {
output_data: Vec::new(),
})
} else if sol_blobs::is_finalize_blob_call(&input_data) {
let params = sol_blobs::parse_finalize_blob_input(&input_data, rt)?;
Self::finalize_blob(rt, params)?;
Ok(InvokeContractReturn {
output_data: Vec::new(),
})
} else if sol_blobs::can_handle(&input_data) {
let output_data = match sol_blobs::parse_input(&input_data)? {
sol_blobs::Calls::addBlob(call) => {
let params = call.params(rt)?;
Expand Down Expand Up @@ -213,6 +270,14 @@ impl ActorCode for BlobsActor {
GetOperatorInfo => get_operator_info,
GetActiveOperators => get_active_operators,

// Execution methods (MVP)
CreateJob => create_job,
ClaimJob => claim_job,
CompleteJob => complete_job,
FailJob => fail_job,
GetJob => get_job,
ListJobs => list_jobs,

_ => fallback,
}
}
Expand Down
Loading