Skip to content

Commit 1d61881

Browse files
committed
fix: storage node pattern
1 parent 8f852de commit 1d61881

8 files changed

Lines changed: 481 additions & 73 deletions

File tree

fendermint/actors/blobs/src/actor.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,33 @@ impl BlobsActor {
5252
params: InvokeContractParams,
5353
) -> Result<InvokeContractReturn, ActorError> {
5454
let input_data: InputData = params.try_into()?;
55-
if sol_blobs::can_handle(&input_data) {
55+
if sol_blobs::is_register_node_operator_call(&input_data) {
56+
let params = sol_blobs::parse_register_node_operator_input(&input_data)?;
57+
let params = fendermint_actor_blobs_shared::operators::RegisterNodeOperatorParams {
58+
bls_pubkey: params.bls_pubkey,
59+
rpc_url: params.rpc_url,
60+
};
61+
let _ = Self::register_node_operator(rt, params)?;
62+
Ok(InvokeContractReturn {
63+
output_data: Vec::new(),
64+
})
65+
} else if sol_blobs::is_get_operator_info_call(&input_data) {
66+
let params = sol_blobs::parse_get_operator_info_input(&input_data)?;
67+
let address = rt
68+
.resolve_address(&params.address)
69+
.map(fvm_shared::address::Address::new_id)
70+
.unwrap_or(params.address);
71+
let info = Self::get_operator_info(
72+
rt,
73+
fendermint_actor_blobs_shared::operators::GetOperatorInfoParams { address },
74+
)?;
75+
let output_data = sol_blobs::encode_get_operator_info_output(info)?;
76+
Ok(InvokeContractReturn { output_data })
77+
} else if sol_blobs::is_get_active_operators_call(&input_data) {
78+
let operators = Self::get_active_operators(rt)?;
79+
let output_data = sol_blobs::encode_get_active_operators_output(operators.operators)?;
80+
Ok(InvokeContractReturn { output_data })
81+
} else if sol_blobs::can_handle(&input_data) {
5682
let output_data = match sol_blobs::parse_input(&input_data)? {
5783
sol_blobs::Calls::addBlob(call) => {
5884
let params = call.params(rt)?;

fendermint/actors/blobs/src/sol_facade/blobs.rs

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use fendermint_actor_blobs_shared::{
88
TrimBlobExpiriesParams,
99
},
1010
bytes::B256,
11+
operators::OperatorInfo,
1112
GetStatsReturn,
1213
};
1314
use fil_actors_runtime::{actor_error, runtime::Runtime, ActorError};
@@ -112,6 +113,247 @@ pub fn parse_input(input: &ipc_storage_actor_sdk::evm::InputData) -> Result<Call
112113
.map_err(|e| actor_error!(illegal_argument, format!("invalid call: {}", e)))
113114
}
114115

116+
pub const REGISTER_NODE_OPERATOR_SELECTOR: [u8; 4] = [0x71, 0x3b, 0x10, 0xcf];
117+
pub const GET_OPERATOR_INFO_SELECTOR: [u8; 4] = [0x27, 0xd9, 0xab, 0x5d];
118+
pub const GET_ACTIVE_OPERATORS_SELECTOR: [u8; 4] = [0x64, 0xbd, 0xc6, 0x7e];
119+
120+
pub struct RegisterNodeOperatorInvokeCall {
121+
pub bls_pubkey: Vec<u8>,
122+
pub rpc_url: String,
123+
}
124+
125+
pub struct GetOperatorInfoInvokeCall {
126+
pub address: Address,
127+
}
128+
129+
pub fn is_register_node_operator_call(input: &ipc_storage_actor_sdk::evm::InputData) -> bool {
130+
input.selector() == REGISTER_NODE_OPERATOR_SELECTOR
131+
}
132+
133+
pub fn is_get_operator_info_call(input: &ipc_storage_actor_sdk::evm::InputData) -> bool {
134+
input.selector() == GET_OPERATOR_INFO_SELECTOR
135+
}
136+
137+
pub fn is_get_active_operators_call(input: &ipc_storage_actor_sdk::evm::InputData) -> bool {
138+
input.selector() == GET_ACTIVE_OPERATORS_SELECTOR
139+
}
140+
141+
pub fn parse_register_node_operator_input(
142+
input: &ipc_storage_actor_sdk::evm::InputData,
143+
) -> Result<RegisterNodeOperatorInvokeCall, ActorError> {
144+
let calldata = input.calldata();
145+
if calldata.len() < 64 {
146+
return Err(actor_error!(illegal_argument, "invalid call: input too short"));
147+
}
148+
149+
let bls_offset = decode_offset(calldata, 0)?;
150+
let rpc_offset = decode_offset(calldata, 32)?;
151+
152+
let bls_pubkey = decode_dynamic_bytes(calldata, bls_offset)?;
153+
let rpc_bytes = decode_dynamic_bytes(calldata, rpc_offset)?;
154+
let rpc_url = String::from_utf8(rpc_bytes)
155+
.map_err(|e| actor_error!(illegal_argument, format!("invalid call: bad UTF-8: {}", e)))?;
156+
157+
Ok(RegisterNodeOperatorInvokeCall {
158+
bls_pubkey,
159+
rpc_url,
160+
})
161+
}
162+
163+
pub fn parse_get_operator_info_input(
164+
input: &ipc_storage_actor_sdk::evm::InputData,
165+
) -> Result<GetOperatorInfoInvokeCall, ActorError> {
166+
let calldata = input.calldata();
167+
if calldata.len() < 32 {
168+
return Err(actor_error!(illegal_argument, "invalid call: input too short"));
169+
}
170+
let word = &calldata[0..32];
171+
if word[..12].iter().any(|b| *b != 0) {
172+
return Err(actor_error!(
173+
illegal_argument,
174+
"invalid call: malformed address"
175+
));
176+
}
177+
let address: Address = H160::from_slice(&word[12..32]).into();
178+
Ok(GetOperatorInfoInvokeCall { address })
179+
}
180+
181+
pub fn encode_get_operator_info_output(info: Option<OperatorInfo>) -> Result<Vec<u8>, ActorError> {
182+
let (bls_pubkey, rpc_url, active) = if let Some(info) = info {
183+
(info.bls_pubkey, info.rpc_url.into_bytes(), info.active)
184+
} else {
185+
(Vec::new(), Vec::new(), false)
186+
};
187+
188+
let bls_section = encode_dynamic_bytes(&bls_pubkey);
189+
let rpc_section = encode_dynamic_bytes(&rpc_url);
190+
191+
let head_size = 32 * 3;
192+
let bls_offset = head_size;
193+
let rpc_offset = head_size + bls_section.len();
194+
195+
let mut output = Vec::with_capacity(head_size + bls_section.len() + rpc_section.len());
196+
output.extend_from_slice(&abi_word_from_usize(bls_offset));
197+
output.extend_from_slice(&abi_word_from_usize(rpc_offset));
198+
output.extend_from_slice(&abi_word_from_bool(active));
199+
output.extend_from_slice(&bls_section);
200+
output.extend_from_slice(&rpc_section);
201+
Ok(output)
202+
}
203+
204+
pub fn encode_get_active_operators_output(operators: Vec<Address>) -> Result<Vec<u8>, ActorError> {
205+
let mut operators_section = Vec::with_capacity(32 + operators.len() * 32);
206+
operators_section.extend_from_slice(&abi_word_from_usize(operators.len()));
207+
for operator in operators {
208+
let h160 = H160::try_from(operator).map_err(|e| {
209+
actor_error!(
210+
illegal_argument,
211+
format!("failed to encode operator address: {}", e)
212+
)
213+
})?;
214+
operators_section.extend_from_slice(&abi_word_from_address(h160));
215+
}
216+
217+
let mut output = Vec::with_capacity(32 + operators_section.len());
218+
output.extend_from_slice(&abi_word_from_usize(32));
219+
output.extend_from_slice(&operators_section);
220+
Ok(output)
221+
}
222+
223+
fn decode_offset(calldata: &[u8], at: usize) -> Result<usize, ActorError> {
224+
let end = at + 32;
225+
if end > calldata.len() {
226+
return Err(actor_error!(illegal_argument, "invalid call: malformed offset"));
227+
}
228+
let word = &calldata[at..end];
229+
if word[..24].iter().any(|b| *b != 0) {
230+
return Err(actor_error!(
231+
illegal_argument,
232+
"invalid call: offset too large"
233+
));
234+
}
235+
let mut n = [0u8; 8];
236+
n.copy_from_slice(&word[24..32]);
237+
Ok(u64::from_be_bytes(n) as usize)
238+
}
239+
240+
fn decode_dynamic_bytes(calldata: &[u8], offset: usize) -> Result<Vec<u8>, ActorError> {
241+
if offset + 32 > calldata.len() {
242+
return Err(actor_error!(
243+
illegal_argument,
244+
"invalid call: dynamic offset out of bounds"
245+
));
246+
}
247+
248+
let len = decode_offset(calldata, offset)?;
249+
let start = offset + 32;
250+
let end = start
251+
.checked_add(len)
252+
.ok_or_else(|| actor_error!(illegal_argument, "invalid call: overflow"))?;
253+
254+
if end > calldata.len() {
255+
return Err(actor_error!(
256+
illegal_argument,
257+
"invalid call: dynamic value out of bounds"
258+
));
259+
}
260+
261+
Ok(calldata[start..end].to_vec())
262+
}
263+
264+
fn abi_word_from_usize(value: usize) -> [u8; 32] {
265+
let mut word = [0u8; 32];
266+
word[24..32].copy_from_slice(&(value as u64).to_be_bytes());
267+
word
268+
}
269+
270+
fn abi_word_from_bool(value: bool) -> [u8; 32] {
271+
let mut word = [0u8; 32];
272+
word[31] = u8::from(value);
273+
word
274+
}
275+
276+
fn abi_word_from_address(value: H160) -> [u8; 32] {
277+
let mut word = [0u8; 32];
278+
word[12..32].copy_from_slice(&value.to_fixed_bytes());
279+
word
280+
}
281+
282+
fn encode_dynamic_bytes(value: &[u8]) -> Vec<u8> {
283+
let mut out = Vec::with_capacity(32 + padded_32_len(value.len()));
284+
out.extend_from_slice(&abi_word_from_usize(value.len()));
285+
out.extend_from_slice(value);
286+
let padding = padded_32_len(value.len()) - value.len();
287+
out.extend(std::iter::repeat(0u8).take(padding));
288+
out
289+
}
290+
291+
fn padded_32_len(size: usize) -> usize {
292+
if size == 0 { 0 } else { size.div_ceil(32) * 32 }
293+
}
294+
295+
#[cfg(test)]
296+
mod tests {
297+
use super::*;
298+
299+
fn address_word(bytes20: [u8; 20]) -> [u8; 32] {
300+
let mut word = [0u8; 32];
301+
word[12..32].copy_from_slice(&bytes20);
302+
word
303+
}
304+
305+
#[test]
306+
fn parses_get_operator_info_input_address() {
307+
let addr = [0x11u8; 20];
308+
let mut input = Vec::new();
309+
input.extend_from_slice(&GET_OPERATOR_INFO_SELECTOR);
310+
input.extend_from_slice(&address_word(addr));
311+
let input =
312+
ipc_storage_actor_sdk::evm::InputData::try_from(ipc_storage_actor_sdk::evm::InvokeContractParams {
313+
input_data: input,
314+
})
315+
.expect("valid input");
316+
317+
let parsed = parse_get_operator_info_input(&input).expect("parse succeeds");
318+
let expected = Address::new_delegated(10, &addr).expect("delegated");
319+
assert_eq!(parsed.address, expected);
320+
}
321+
322+
#[test]
323+
fn encodes_get_active_operators_output_as_address_array() {
324+
let id = Address::new_id(66);
325+
let delegated = Address::new_delegated(10, &[0x22; 20]).expect("delegated");
326+
327+
let encoded = encode_get_active_operators_output(vec![id, delegated]).expect("encode");
328+
329+
assert_eq!(&encoded[0..32], &abi_word_from_usize(32));
330+
assert_eq!(&encoded[32..64], &abi_word_from_usize(2));
331+
332+
let id_h160 = H160::try_from(id).expect("id to h160");
333+
let delegated_h160 = H160::try_from(delegated).expect("delegated to h160");
334+
assert_eq!(&encoded[64..96], &abi_word_from_address(id_h160));
335+
assert_eq!(&encoded[96..128], &abi_word_from_address(delegated_h160));
336+
}
337+
338+
#[test]
339+
fn encodes_get_operator_info_output_tuple() {
340+
let info = OperatorInfo {
341+
bls_pubkey: vec![1, 2, 3, 4],
342+
rpc_url: "http://127.0.0.1:8081".to_string(),
343+
active: true,
344+
};
345+
let encoded = encode_get_operator_info_output(Some(info)).expect("encode");
346+
347+
assert_eq!(&encoded[0..32], &abi_word_from_usize(96));
348+
let bls_section_len = 32 + 32; // len + padded data for 4 bytes
349+
assert_eq!(
350+
&encoded[32..64],
351+
&abi_word_from_usize(96 + bls_section_len)
352+
);
353+
assert_eq!(&encoded[64..96], &abi_word_from_bool(true));
354+
}
355+
}
356+
115357
fn blob_status_as_solidity_enum(blob_status: BlobStatus) -> u8 {
116358
match blob_status {
117359
BlobStatus::Added => 0,

ipc-storage/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ export SECRET_KEY_FILE=./test-network/keys/alice.sk
5151

5252
```
5353

54+
Notes:
55+
- `register-operator` now supports delegated (`t410...`) operator keys through the blobs actor `InvokeContract` facade path.
56+
- Operator query methods are also available through the same facade path: `getOperatorInfo(address)` and `getActiveOperators()`.
57+
- Storage node and gateway use delegated (`t410...`) sender path for on-chain interactions; fund delegated operator address before running.
58+
5459
## 3. Launch ipc-dropbox
5560
Launch `ipc-dropbox` in `ipc-storage/ipc-dropbox` with `npm run dev`.
5661

ipc-storage/ipc-decentralized-storage/src/bin/gateway.rs

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33

44
//! CLI for running the blob gateway with objects API
55
6-
use anyhow::{anyhow, Context, Result};
6+
use anyhow::{Context, Result};
77
use bls_signatures::{PrivateKey as BlsPrivateKey, Serialize as BlsSerialize};
88
use clap::Parser;
99
use fendermint_rpc::message::SignedMessageFactory;
1010
use fendermint_rpc::FendermintClient;
1111
use fendermint_rpc::QueryClient;
12+
use fendermint_vm_actor_interface::eam::EthAddress as FvmEthAddress;
1213
use fendermint_vm_message::query::FvmQueryHeight;
1314
use fvm_shared::address::{set_current_network, Address, Network};
1415
use fvm_shared::chainid::ChainID;
@@ -79,16 +80,16 @@ struct Args {
7980
iroh_v6_addr: Option<SocketAddrV6>,
8081
}
8182

82-
/// Get the next sequence number (nonce) of an account.
83-
async fn get_sequence(client: &impl QueryClient, addr: &Address) -> Result<u64> {
83+
/// Get the next sequence number (nonce) of an account if it exists.
84+
async fn get_sequence_opt(client: &impl QueryClient, addr: &Address) -> Result<Option<u64>> {
8485
let state = client
8586
.actor_state(addr, FvmQueryHeight::default())
8687
.await
8788
.context("failed to get actor state")?;
8889

8990
match state.value {
90-
Some((_id, state)) => Ok(state.sequence),
91-
None => Err(anyhow!("cannot find actor {addr}")),
91+
Some((_id, state)) => Ok(Some(state.sequence)),
92+
None => Ok(None),
9293
}
9394
}
9495

@@ -125,10 +126,13 @@ async fn main() -> Result<()> {
125126
.context("failed to read secret key")?;
126127

127128
let pk = sk.public_key();
128-
// Use f1 address (secp256k1) for signing native FVM actor transactions
129-
let from_addr =
130-
Address::new_secp256k1(&pk.serialize()).context("failed to create f1 address")?;
131-
tracing::info!("Gateway sender address: {}", from_addr);
129+
let from_f1 = Address::new_secp256k1(&pk.serialize()).context("failed to create f1 address")?;
130+
let from_eth = FvmEthAddress::new_secp256k1(&pk.serialize())
131+
.context("failed to derive delegated address from secret key")?;
132+
let from_f410 =
133+
Address::new_delegated(10, &from_eth.0).context("failed to create f410 address")?;
134+
tracing::info!("Gateway sender f1 address: {}", from_f1);
135+
tracing::info!("Gateway sender f410 address: {}", from_f410);
132136

133137
// Parse or generate BLS private key if provided
134138
let _bls_private_key = if let Some(key_file) = &args.bls_key_file {
@@ -214,11 +218,6 @@ async fn main() -> Result<()> {
214218
let client = FendermintClient::new_http(args.rpc_url, None)
215219
.context("failed to create Fendermint client")?;
216220

217-
// Query the account nonce from the state
218-
let sequence = get_sequence(&client, &from_addr)
219-
.await
220-
.context("failed to get account sequence")?;
221-
222221
// Query the chain ID
223222
let chain_id = client
224223
.state_params(FvmQueryHeight::default())
@@ -228,6 +227,20 @@ async fn main() -> Result<()> {
228227
.chain_id;
229228

230229
tracing::info!("Chain ID: {}", chain_id);
230+
231+
let (from_addr, sequence) = if let Some(sequence) = get_sequence_opt(&client, &from_f410)
232+
.await
233+
.context("failed to get delegated account sequence")?
234+
{
235+
tracing::info!("Using delegated sender (f410) for gateway transactions");
236+
(from_f410, sequence)
237+
} else {
238+
anyhow::bail!(
239+
"delegated sender {} not found on-chain; cross-fund this delegated address and retry (native f1 {} is intentionally not used)",
240+
from_f410, from_f1
241+
);
242+
};
243+
tracing::info!("Gateway sender address: {}", from_addr);
231244
tracing::info!("Account sequence: {}", sequence);
232245

233246
// Create signed message factory

0 commit comments

Comments
 (0)