Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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