1use super::gas::estimate_message_gas;
5use crate::lotus_json::{LotusJson, NotNullVec, lotus_json_with_self};
6use crate::message::SignedMessage;
7use crate::prelude::*;
8use crate::rpc::error::ServerError;
9use crate::rpc::types::{ApiTipsetKey, MessageSendSpec};
10use crate::rpc::{ApiPaths, Ctx, Permission, RpcMethod};
11use crate::shim::{
12 address::{Address, Protocol},
13 message::Message,
14 percent::Percent,
15};
16use ahash::HashSet;
17use enumflags2::BitFlags;
18use schemars::JsonSchema;
19use serde::{Deserialize, Serialize};
20use std::time::Duration;
21
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
23#[serde(rename_all = "PascalCase")]
24pub struct ApiMpoolConfig {
25 #[schemars(with = "LotusJson<Vec<Address>>")]
26 #[serde(with = "crate::lotus_json")]
27 pub priority_addrs: Vec<Address>,
28 pub size_limit_high: i64,
29 pub size_limit_low: i64,
30 #[serde(with = "crate::lotus_json")]
31 #[schemars(with = "LotusJson<Percent>")]
32 pub replace_by_fee_ratio: Percent,
33 #[schemars(with = "LotusJson<Duration>")]
34 #[serde(with = "crate::lotus_json")]
35 pub prune_cooldown: Duration,
36 pub gas_limit_overestimation: f64,
37}
38
39lotus_json_with_self!(ApiMpoolConfig);
40
41pub enum MpoolGetConfig {}
43impl RpcMethod<0> for MpoolGetConfig {
44 const NAME: &'static str = "Filecoin.MpoolGetConfig";
45 const PARAM_NAMES: [&'static str; 0] = [];
46 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
47 const PERMISSION: Permission = Permission::Read;
48 const DESCRIPTION: &'static str = "Returns a copy of the current mpool config.";
49
50 type Params = ();
51 type Ok = ApiMpoolConfig;
52
53 async fn handle(
54 ctx: Ctx,
55 (): Self::Params,
56 _: &http::Extensions,
57 ) -> Result<Self::Ok, ServerError> {
58 let cfg = ctx.mpool.config();
59 Ok(ApiMpoolConfig {
60 priority_addrs: cfg.priority_addrs,
61 size_limit_high: cfg.size_limit_high,
62 size_limit_low: cfg.size_limit_low,
63 replace_by_fee_ratio: cfg.replace_by_fee_ratio,
64 prune_cooldown: cfg.prune_cooldown,
65 gas_limit_overestimation: cfg.gas_limit_overestimation,
66 })
67 }
68}
69
70pub enum MpoolGetNonce {}
72impl RpcMethod<1> for MpoolGetNonce {
73 const NAME: &'static str = "Filecoin.MpoolGetNonce";
74 const PARAM_NAMES: [&'static str; 1] = ["address"];
75 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
76 const PERMISSION: Permission = Permission::Read;
77 const DESCRIPTION: &'static str = "Returns the current nonce for the specified address.";
78
79 type Params = (Address,);
80 type Ok = u64;
81
82 async fn handle(
83 ctx: Ctx,
84 (address,): Self::Params,
85 _: &http::Extensions,
86 ) -> Result<Self::Ok, ServerError> {
87 Ok(ctx.mpool.get_sequence(&address).await?)
88 }
89}
90
91pub enum MpoolPending {}
93impl RpcMethod<1> for MpoolPending {
94 const NAME: &'static str = "Filecoin.MpoolPending";
95 const PARAM_NAMES: [&'static str; 1] = ["tipsetKey"];
96 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
97 const PERMISSION: Permission = Permission::Read;
98 const DESCRIPTION: &'static str = "Returns the pending messages for a given tipset.";
99
100 type Params = (ApiTipsetKey,);
101 type Ok = NotNullVec<SignedMessage>;
102
103 async fn handle(
104 ctx: Ctx,
105 (ApiTipsetKey(tipset_key),): Self::Params,
106 _: &http::Extensions,
107 ) -> Result<Self::Ok, ServerError> {
108 let mut ts = ctx
109 .chain_store()
110 .load_required_tipset_or_heaviest(&tipset_key)?;
111
112 let (mut pending, mpts) = ctx.mpool.pending();
113
114 if mpts.epoch() > ts.epoch() || mpts == ts {
116 return Ok(pending.into());
117 }
118
119 let mut have_cids: HashSet<_> = pending.iter().map(|m| m.cid()).collect();
120
121 loop {
122 if mpts.epoch() == ts.epoch() {
125 if mpts == ts {
126 break;
127 }
128
129 let have = ctx.mpool.messages_for_blocks(mpts.block_headers().iter())?;
133 have_cids.extend(have.iter().map(|m| m.cid()));
134 }
135
136 let msgs = ctx.mpool.messages_for_blocks(ts.block_headers().iter())?;
137
138 for m in msgs {
139 if have_cids.insert(m.cid()) {
140 pending.push(m);
141 }
142 }
143
144 if mpts.epoch() >= ts.epoch() {
145 break;
146 }
147
148 ts = ctx.chain_index().load_required_tipset(ts.parents())?;
149 }
150 Ok(pending.into())
151 }
152}
153
154pub enum MpoolSelect {}
156impl RpcMethod<2> for MpoolSelect {
157 const NAME: &'static str = "Filecoin.MpoolSelect";
158 const PARAM_NAMES: [&'static str; 2] = ["tipsetKey", "ticketQuality"];
159 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
160 const PERMISSION: Permission = Permission::Read;
161 const DESCRIPTION: &'static str =
162 "Returns a list of pending messages for inclusion in the next block.";
163
164 type Params = (ApiTipsetKey, f64);
165 type Ok = Vec<SignedMessage>;
166
167 async fn handle(
168 ctx: Ctx,
169 (ApiTipsetKey(tipset_key), ticket_quality): Self::Params,
170 _: &http::Extensions,
171 ) -> Result<Self::Ok, ServerError> {
172 let ts = ctx
173 .chain_store()
174 .load_required_tipset_or_heaviest(&tipset_key)?;
175 Ok(ctx.mpool.select_messages(&ts, ticket_quality)?)
176 }
177}
178
179pub enum MpoolPush {}
181impl RpcMethod<1> for MpoolPush {
182 const NAME: &'static str = "Filecoin.MpoolPush";
183 const PARAM_NAMES: [&'static str; 1] = ["message"];
184 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
185 const PERMISSION: Permission = Permission::Write;
186 const DESCRIPTION: &'static str = "Adds a signed message to the message pool.";
187
188 type Params = (SignedMessage,);
189 type Ok = Cid;
190
191 async fn handle(
192 ctx: Ctx,
193 (message,): Self::Params,
194 _: &http::Extensions,
195 ) -> Result<Self::Ok, ServerError> {
196 let cid = ctx.mpool.push(message).await?;
197 Ok(cid)
198 }
199}
200
201pub enum MpoolBatchPush {}
203impl RpcMethod<1> for MpoolBatchPush {
204 const NAME: &'static str = "Filecoin.MpoolBatchPush";
205 const PARAM_NAMES: [&'static str; 1] = ["messages"];
206 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
207 const PERMISSION: Permission = Permission::Write;
208 const DESCRIPTION: &'static str = "Adds a set of signed messages to the message pool.";
209
210 type Params = (Vec<SignedMessage>,);
211 type Ok = Vec<Cid>;
212
213 async fn handle(
214 ctx: Ctx,
215 (messages,): Self::Params,
216 _: &http::Extensions,
217 ) -> Result<Self::Ok, ServerError> {
218 let mut cids = vec![];
219 for msg in messages {
220 cids.push(ctx.mpool.push(msg).await?);
221 }
222 Ok(cids)
223 }
224}
225
226pub enum MpoolPushUntrusted {}
228impl RpcMethod<1> for MpoolPushUntrusted {
229 const NAME: &'static str = "Filecoin.MpoolPushUntrusted";
230 const PARAM_NAMES: [&'static str; 1] = ["message"];
231 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
232 const PERMISSION: Permission = Permission::Write;
233 const DESCRIPTION: &'static str =
234 "Adds a message to the message pool with verification checks.";
235
236 type Params = (SignedMessage,);
237 type Ok = Cid;
238
239 async fn handle(
240 ctx: Ctx,
241 (message,): Self::Params,
242 _: &http::Extensions,
243 ) -> Result<Self::Ok, ServerError> {
244 let cid = ctx.mpool.push_untrusted(message).await?;
248 Ok(cid)
249 }
250}
251
252pub enum MpoolBatchPushUntrusted {}
254impl RpcMethod<1> for MpoolBatchPushUntrusted {
255 const NAME: &'static str = "Filecoin.MpoolBatchPushUntrusted";
256 const PARAM_NAMES: [&'static str; 1] = ["messages"];
257 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
258 const PERMISSION: Permission = Permission::Write;
259 const DESCRIPTION: &'static str =
260 "Adds a set of messages to the message pool with additional verification checks.";
261
262 type Params = (Vec<SignedMessage>,);
263 type Ok = Vec<Cid>;
264
265 async fn handle(
266 ctx: Ctx,
267 (messages,): Self::Params,
268 ext: &http::Extensions,
269 ) -> Result<Self::Ok, ServerError> {
270 MpoolBatchPush::handle(ctx, (messages,), ext).await
272 }
273}
274
275pub enum MpoolPushMessage {}
277impl RpcMethod<2> for MpoolPushMessage {
278 const NAME: &'static str = "Filecoin.MpoolPushMessage";
279 const PARAM_NAMES: [&'static str; 2] = ["message", "sendSpec"];
280 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
281 const PERMISSION: Permission = Permission::Sign;
282 const DESCRIPTION: &'static str =
283 "Assigns a nonce, signs, and pushes a message to the mempool.";
284
285 type Params = (Message, Option<MessageSendSpec>);
286 type Ok = SignedMessage;
287
288 async fn handle(
289 ctx: Ctx,
290 (message, send_spec): Self::Params,
291 extensions: &http::Extensions,
292 ) -> Result<Self::Ok, ServerError> {
293 let from = message.from;
294
295 let heaviest_tipset = ctx.chain_store().heaviest_tipset();
296 let key_addr = ctx
297 .state_manager
298 .resolve_to_deterministic_address(from, &heaviest_tipset)
299 .await?;
300
301 if message.sequence != 0 {
302 return Err(anyhow::anyhow!(
303 "Expected nonce for MpoolPushMessage is 0, and will be calculated for you"
304 )
305 .into());
306 }
307
308 let _sender_guard = ctx.mpool_locker.take_lock(key_addr).await;
309
310 let mut message =
311 estimate_message_gas(&ctx, message, send_spec, Default::default()).await?;
312 if message.gas_premium > message.gas_fee_cap {
313 return Err(anyhow::anyhow!(
314 "After estimation, gas premium is greater than gas fee cap"
315 )
316 .into());
317 }
318
319 if from.protocol() == Protocol::ID {
320 message.from = key_addr;
321 }
322
323 let balance =
324 super::wallet::WalletBalance::handle(ctx.clone(), (message.from,), extensions).await?;
325 let required_funds = &message.value + &message.gas_fee_cap * message.gas_limit;
326 if balance < required_funds {
327 return Err(anyhow::anyhow!(
328 "mpool push: not enough funds: {balance} < {required_funds}",
329 )
330 .into());
331 }
332
333 let key = crate::key_management::Key::try_from(crate::key_management::try_find(
334 &key_addr,
335 &ctx.keystore.as_ref().read(),
336 )?)?;
337 let eth_chain_id = ctx.chain_config().eth_chain_id;
338
339 let smsg = ctx
340 .nonce_tracker
341 .sign_and_push(&ctx.mpool, message, &key, eth_chain_id)
342 .await?;
343
344 Ok(smsg)
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351 use crate::blocks::{Block, CachingBlockHeader, FullTipset, RawBlockHeader, Tipset};
352 use crate::chain::ChainStore;
353 use crate::chain_sync::TipsetValidator;
354 use crate::rpc::RPCState;
355 use crate::rpc::test_utils::chain_store;
356 use crate::shim::crypto::{SECP_SIG_LEN, Signature};
357 use crate::test_utils::dummy_ticket;
358 use fvm_ipld_blockstore::Blockstore;
359
360 fn secp_message(sequence: u64) -> SignedMessage {
362 SignedMessage::new_unchecked(
363 Message {
364 from: Address::new_id(100),
365 to: Address::new_id(101),
366 sequence,
367 ..Default::default()
368 },
369 Signature::new_secp256k1(vec![0; SECP_SIG_LEN]),
370 )
371 }
372
373 fn tipset_at(
376 db: &impl Blockstore,
377 parent: &Tipset,
378 epoch: i64,
379 ticket: u8,
380 bls: &[Message],
381 secp: &[SignedMessage],
382 ) -> Tipset {
383 let fts = FullTipset::new([Block {
384 header: CachingBlockHeader::new(RawBlockHeader {
385 parents: parent.key().clone(),
386 epoch,
387 messages: TipsetValidator::compute_msg_root(db, bls, secp).unwrap(),
388 ticket: dummy_ticket(ticket),
389 ..Default::default()
390 }),
391 bls_messages: bls.to_vec(),
392 secp_messages: secp.to_vec(),
393 }])
394 .unwrap();
395 fts.persist(db).unwrap();
396 fts.into_tipset()
397 }
398
399 fn tipset_on(
401 db: &impl Blockstore,
402 parent: &Tipset,
403 ticket: u8,
404 bls: &[Message],
405 secp: &[SignedMessage],
406 ) -> Tipset {
407 tipset_at(db, parent, parent.epoch() + 1, ticket, bls, secp)
408 }
409
410 fn ctx_on(cs: ChainStore, mpool_ts: &Tipset) -> Arc<RPCState> {
412 cs.set_heaviest_tipset(mpool_ts.clone()).unwrap();
413 let (ctx, _) = RPCState::for_tests(cs).unwrap();
414 assert_eq!(
415 &ctx.mpool.pending().1,
416 mpool_ts,
417 "the pool must adopt the heaviest tipset"
418 );
419 ctx
420 }
421
422 async fn pending_at(ctx: Arc<RPCState>, ts: &Tipset) -> Vec<SignedMessage> {
423 let NotNullVec(pending) = MpoolPending::handle(
424 ctx,
425 (ApiTipsetKey(Some(ts.key().clone())),),
426 &Default::default(),
427 )
428 .await
429 .unwrap();
430 pending
431 }
432
433 #[tokio::test]
437 async fn merges_messages_of_a_same_height_fork() {
438 let cs = chain_store();
439 let genesis = cs.genesis_tipset();
440 let shared = secp_message(0);
441 let only_in_fork = secp_message(1);
442
443 let mpool_ts = tipset_on(cs.db(), &genesis, 1, &[], std::slice::from_ref(&shared));
444 let fork_ts = tipset_on(cs.db(), &genesis, 2, &[], &[shared, only_in_fork.clone()]);
445
446 let ctx = ctx_on(cs, &mpool_ts);
447 assert_eq!(pending_at(ctx, &fork_ts).await, vec![only_in_fork]);
448 }
449
450 #[tokio::test]
452 async fn walks_back_to_the_mpool_tipset() {
453 let cs = chain_store();
454 let genesis = cs.genesis_tipset();
455 let in_mpool_ts = secp_message(0);
456 let in_child = secp_message(1);
457
458 let mpool_ts = tipset_on(
459 cs.db(),
460 &genesis,
461 1,
462 &[],
463 std::slice::from_ref(&in_mpool_ts),
464 );
465 let child_ts = tipset_on(cs.db(), &mpool_ts, 2, &[], std::slice::from_ref(&in_child));
466
467 let ctx = ctx_on(cs, &mpool_ts);
468 assert_eq!(pending_at(ctx, &child_ts).await, vec![in_child]);
469 }
470
471 #[tokio::test]
474 async fn merges_across_a_null_round_past_the_mpool_tipset() {
475 let cs = chain_store();
476 let genesis = cs.genesis_tipset();
477 let only_in_ts = secp_message(1);
478
479 let base = tipset_on(cs.db(), &genesis, 5, &[], &[]);
481 let mpool_ts = tipset_on(cs.db(), &base, 1, &[], &[secp_message(0)]);
483 let ts = tipset_at(cs.db(), &base, 3, 3, &[], std::slice::from_ref(&only_in_ts));
486
487 let ctx = ctx_on(cs, &mpool_ts);
488 assert_eq!(pending_at(ctx, &ts).await, vec![only_in_ts]);
489 }
490
491 #[tokio::test]
493 async fn does_not_merge_at_or_behind_the_mpool_tipset() {
494 let cs = chain_store();
495 let genesis = cs.genesis_tipset();
496 let mpool_ts = tipset_on(cs.db(), &genesis, 1, &[], &[secp_message(0)]);
497
498 let ctx = ctx_on(cs, &mpool_ts);
499 assert!(pending_at(ctx.clone(), &mpool_ts).await.is_empty());
501 assert!(pending_at(ctx, &genesis).await.is_empty());
502 }
503
504 #[tokio::test]
508 async fn skips_bls_messages_with_an_uncached_signature() {
509 let cs = chain_store();
510 let genesis = cs.genesis_tipset();
511 let only_in_fork = secp_message(1);
512
513 let mpool_ts = tipset_on(cs.db(), &genesis, 1, &[], &[]);
514 let fork_ts = tipset_on(
516 cs.db(),
517 &genesis,
518 2,
519 &[Message {
521 from: Address::new_id(200),
522 to: Address::new_id(201),
523 ..Default::default()
524 }],
525 std::slice::from_ref(&only_in_fork),
526 );
527
528 let ctx = ctx_on(cs, &mpool_ts);
529 assert_eq!(pending_at(ctx, &fork_ts).await, vec![only_in_fork]);
530 }
531}