@@ -38,6 +38,7 @@ use helpers::{
3838 fetch_input_infos, fetch_rpc_token_info, fetch_utxo, fetch_utxo_extra_info, into_balances,
3939} ;
4040use itertools:: Itertools as _;
41+ use node_comm:: node_traits:: NodeInterfaceError as _;
4142use runtime_wallet:: RuntimeWallet ;
4243use std:: {
4344 collections:: { BTreeMap , BTreeSet } ,
@@ -773,26 +774,116 @@ where
773774
774775 /// Try to generate the `block_count` number of blocks.
775776 /// The function may return an error early if some attempt fails.
777+ ///
778+ /// Note that this function is intended to be used on regtest/signet to populate the chainstate
779+ /// and it won't work reliably if a large number of blocks enters the chainstate via other means
780+ /// at the same time (e.g. via p2p during initial block download).
776781 pub async fn generate_blocks (
777782 & mut self ,
778783 account_index : U31 ,
779784 block_count : u32 ,
780785 ) -> Result < ( ) , ControllerError < N > > {
781- for _ in 0 ..block_count {
782- self . sync_once ( ) . await ?;
783- let block = self
784- . generate_block (
785- account_index,
786- vec ! [ ] ,
787- vec ! [ ] ,
788- PackingStrategy :: FillSpaceFromMempool ,
789- )
790- . await ?;
786+ let mut recoverable_errors_seen_count = 0 ;
787+
788+ for block_idx in 0 ..block_count {
789+ // Perform a few attempts to produce a block, retrying on a "recoverable mempool error",
790+ // which indicates that the block production was aborted because the tip has changed
791+ // when collecting transactions from the mempool.
792+ // This may happen either because the mempool is lagging behind chainstate (so the
793+ // new tip is one of the previously produced blocks) or if blocks enter the chainstate
794+ // via other means (e.g. p2p).
795+ // Note:
796+ // 1) Potentially we may see a "recoverable mempool error" for each of the blocks
797+ // we produce here, so the retry count should be at least "the number of blocks we
798+ // produced so far" minus "the number of recoverable errors seen so far". We add
799+ // a small number to that to have some leeway in case blocks are also entering
800+ // the chainstate via other means.
801+ // 2) The alternative to retrying is to wait for a NewTip event from the mempool after
802+ // each block; this would make the function more reliable in the case of the mempool
803+ // lagging, but less reliable in the case when blocks enter chainstate via p2p
804+ // (in which case a newly produced block may not become a tip at all).
805+ // I.e. there seems to be no way to make this function 100% reliable.
806+ let recoverable_error_retry_count =
807+ ( block_idx + 1 ) . saturating_sub ( recoverable_errors_seen_count) + 5 ;
808+ let mut recoverable_error_retry_idx = 0 ;
809+ loop {
810+ self . sync_once ( ) . await ?;
811+ let block_gen_result = self
812+ . generate_block (
813+ account_index,
814+ vec ! [ ] ,
815+ vec ! [ ] ,
816+ PackingStrategy :: FillSpaceFromMempool ,
817+ )
818+ . await ;
791819
792- self . rpc_client
793- . submit_block ( block)
794- . await
795- . map_err ( ControllerError :: NodeCallError ) ?;
820+ match block_gen_result {
821+ Ok ( block) => {
822+ self . rpc_client
823+ . submit_block ( block)
824+ . await
825+ . map_err ( ControllerError :: NodeCallError ) ?;
826+ break ;
827+ }
828+ Err ( err) => {
829+ let is_recoverable_err = match & err {
830+ ControllerError :: NodeCallError ( err) => {
831+ err. is_recoverable_mempool_error_during_block_production ( )
832+ }
833+ ControllerError :: SyncError ( _)
834+ | ControllerError :: NotEnoughBlockHeight ( _, _)
835+ | ControllerError :: WalletFileError ( _, _)
836+ | ControllerError :: WalletError ( _)
837+ | ControllerError :: AddressEncodingError ( _)
838+ | ControllerError :: NoStakingPool
839+ | ControllerError :: FrozenToken ( _)
840+ | ControllerError :: WalletIsLocked
841+ | ControllerError :: StakingRunning
842+ | ControllerError :: EndToEndEncryptionError ( _)
843+ | ControllerError :: NodeNotInSyncYet
844+ | ControllerError :: InvalidLookaheadSize
845+ | ControllerError :: WalletFileAlreadyOpen
846+ | ControllerError :: NoWallet
847+ | ControllerError :: SearchForTimestampsFailed ( _)
848+ | ControllerError :: ExpectingNonEmptyInputs
849+ | ControllerError :: ExpectingNonEmptyOutputs
850+ | ControllerError :: NoCoinUtxosToPayFeeFrom
851+ | ControllerError :: InvalidTxOutput ( _)
852+ | ControllerError :: NotFungibleToken ( _)
853+ | ControllerError :: InvalidCoinAmount
854+ | ControllerError :: PartiallySignedTransactionError ( _)
855+ | ControllerError :: InvalidTokenId
856+ | ControllerError :: SighashInputCommitmentCreationError ( _)
857+ | ControllerError :: InvalidHtlcSecretsCount => false ,
858+ } ;
859+
860+ if !is_recoverable_err {
861+ return Err ( err) ;
862+ }
863+
864+ recoverable_errors_seen_count += 1 ;
865+ recoverable_error_retry_idx += 1 ;
866+
867+ if recoverable_error_retry_idx >= recoverable_error_retry_count {
868+ log:: warn!(
869+ "Too many recoverable mempool errors happened during block production, aborting"
870+ ) ;
871+
872+ return Err ( err) ;
873+ }
874+
875+ log:: info!(
876+ "A recoverable mempool error happened during block production, retrying"
877+ ) ;
878+
879+ // Sleep for some amount of time, increasing it as the number of retries grows,
880+ // with the cap of 1 sec.
881+ let delay = Duration :: from_millis ( 100 * recoverable_error_retry_idx as u64 ) ;
882+ let delay = std:: cmp:: min ( delay, Duration :: from_secs ( 1 ) ) ;
883+ tokio:: time:: sleep ( delay) . await ;
884+ }
885+ }
886+ }
796887 }
797888
798889 self . sync_once ( ) . await
0 commit comments