@@ -27,8 +27,8 @@ use citrea_e2e::Result;
2727use citrea_fullnode:: rpc:: FullNodeRpcClient ;
2828use citrea_light_client_prover:: circuit:: {
2929 AddSecurityCouncilMember , BatchProofMethodIdUpdate , RemoveBatchProofMethodId ,
30- RemoveSecurityCouncilMember , UpdateBatchProverDaPubKey , UpdateSecurityCouncilThreshold ,
31- UpdateSequencerDaPubKey ,
30+ RemoveSecurityCouncilMember , ReplaceSecurityCouncilMember , UpdateBatchProverDaPubKey ,
31+ UpdateSecurityCouncilThreshold , UpdateSequencerDaPubKey ,
3232} ;
3333use citrea_light_client_prover:: rpc:: LightClientProverRpcClient ;
3434use citrea_primitives:: compression:: { compress_blob, decompress_blob} ;
@@ -39,9 +39,10 @@ use risc0_zkvm::{FakeReceipt, InnerReceipt, MaybePruned, ReceiptClaim};
3939use sov_modules_api:: BlobReaderTrait ;
4040use sov_rollup_interface:: da:: {
4141 AddSecurityCouncilMemberV1Body , BatchProofMethodIdBody , DaTxRequest , DaVerifier , DataOnDa ,
42- RemoveBatchProofMethodIdV1Body , RemoveSecurityCouncilMemberV1Body , SecurityCouncilTx ,
43- SecurityCouncilTxType , SequencerCommitment , UpdateBatchProverDaPubKeyV1Body ,
44- UpdateSecurityCouncilThresholdV1Body , UpdateSequencerDaPubKeyV1Body ,
42+ RemoveBatchProofMethodIdV1Body , RemoveSecurityCouncilMemberV1Body ,
43+ ReplaceSecurityCouncilMemberV1Body , SecurityCouncilTx , SecurityCouncilTxType ,
44+ SequencerCommitment , UpdateBatchProverDaPubKeyV1Body , UpdateSecurityCouncilThresholdV1Body ,
45+ UpdateSequencerDaPubKeyV1Body ,
4546} ;
4647use sov_rollup_interface:: rpc:: BatchProofMethodIdRpcResponse ;
4748use sov_rollup_interface:: services:: da:: DaService ;
@@ -4530,6 +4531,126 @@ impl TestCase for SecurityCouncilMemberManagementTest {
45304531 "CASE 7: Should still have 4 members (below min rejected)"
45314532 ) ;
45324533
4534+ // --- CASE 8: Replace non-existent member (rejected) ---
4535+ // Try to replace a member that doesn't exist in the council.
4536+ let replace_body_1 = ReplaceSecurityCouncilMemberV1Body {
4537+ to_be_replaced : [ 0xAAu8 ; 20 ] , // not in council
4538+ new_member : [ 0x33u8 ; 20 ] ,
4539+ nonce : 7 ,
4540+ } ;
4541+ let payload = ReplaceSecurityCouncilMember :: from ( replace_body_1. clone ( ) ) ;
4542+ let signatures_with_index = create_valid_signatures ( & signers, & payload, 2 ) ;
4543+ bitcoin_da_service
4544+ . send_transaction_with_fee_rate (
4545+ DaTxRequest :: SecurityCouncilTx ( SecurityCouncilTx {
4546+ tx_type : SecurityCouncilTxType :: ReplaceSecurityCouncilMemberV1 ( replace_body_1) ,
4547+ signatures_with_index,
4548+ } ) ,
4549+ 1.0 ,
4550+ )
4551+ . await ?;
4552+ da. wait_mempool_len ( 2 , None ) . await ?;
4553+ da. generate ( DEFAULT_FINALITY_DEPTH ) . await ?;
4554+ let l1_height = da. get_finalized_height ( None ) . await ?;
4555+ light_client_prover
4556+ . wait_for_l1_height ( l1_height, Some ( TEN_MINS ) )
4557+ . await ?;
4558+
4559+ let addresses = light_client_prover
4560+ . client
4561+ . http_client ( )
4562+ . get_security_council_addresses ( )
4563+ . await ?;
4564+ assert_eq ! (
4565+ addresses. len( ) ,
4566+ 4 ,
4567+ "CASE 8: Should still have 4 members (non-existent member replace rejected)"
4568+ ) ;
4569+
4570+ // --- CASE 9: Replace with already existing member (rejected) ---
4571+ // Try to replace one member with another who is already in the council.
4572+ let replace_body_2 = ReplaceSecurityCouncilMemberV1Body {
4573+ to_be_replaced : _initial_addresses[ 0 ] . 0 . 0 ,
4574+ new_member : _initial_addresses[ 1 ] . 0 . 0 , // already in council
4575+ nonce : 7 ,
4576+ } ;
4577+ let payload = ReplaceSecurityCouncilMember :: from ( replace_body_2. clone ( ) ) ;
4578+ let signatures_with_index = create_valid_signatures ( & signers, & payload, 2 ) ;
4579+ bitcoin_da_service
4580+ . send_transaction_with_fee_rate (
4581+ DaTxRequest :: SecurityCouncilTx ( SecurityCouncilTx {
4582+ tx_type : SecurityCouncilTxType :: ReplaceSecurityCouncilMemberV1 ( replace_body_2) ,
4583+ signatures_with_index,
4584+ } ) ,
4585+ 1.0 ,
4586+ )
4587+ . await ?;
4588+ da. wait_mempool_len ( 2 , None ) . await ?;
4589+ da. generate ( DEFAULT_FINALITY_DEPTH ) . await ?;
4590+ let l1_height = da. get_finalized_height ( None ) . await ?;
4591+ light_client_prover
4592+ . wait_for_l1_height ( l1_height, Some ( TEN_MINS ) )
4593+ . await ?;
4594+
4595+ let addresses = light_client_prover
4596+ . client
4597+ . http_client ( )
4598+ . get_security_council_addresses ( )
4599+ . await ?;
4600+ assert_eq ! (
4601+ addresses. len( ) ,
4602+ 4 ,
4603+ "CASE 9: Should still have 4 members (duplicate member replace rejected)"
4604+ ) ;
4605+
4606+ // --- CASE 10: Valid replace member ---
4607+ // Replace _initial_addresses[2] with a new address.
4608+ let new_replacement = [ 0x33u8 ; 20 ] ;
4609+ let replace_body_3 = ReplaceSecurityCouncilMemberV1Body {
4610+ to_be_replaced : _initial_addresses[ 2 ] . 0 . 0 ,
4611+ new_member : new_replacement,
4612+ nonce : 7 ,
4613+ } ;
4614+ let payload = ReplaceSecurityCouncilMember :: from ( replace_body_3. clone ( ) ) ;
4615+ let signatures_with_index = create_valid_signatures ( & signers, & payload, 2 ) ;
4616+ bitcoin_da_service
4617+ . send_transaction_with_fee_rate (
4618+ DaTxRequest :: SecurityCouncilTx ( SecurityCouncilTx {
4619+ tx_type : SecurityCouncilTxType :: ReplaceSecurityCouncilMemberV1 ( replace_body_3) ,
4620+ signatures_with_index,
4621+ } ) ,
4622+ 1.0 ,
4623+ )
4624+ . await ?;
4625+ da. wait_mempool_len ( 2 , None ) . await ?;
4626+ da. generate ( DEFAULT_FINALITY_DEPTH ) . await ?;
4627+ let l1_height = da. get_finalized_height ( None ) . await ?;
4628+ light_client_prover
4629+ . wait_for_l1_height ( l1_height, Some ( TEN_MINS ) )
4630+ . await ?;
4631+
4632+ let addresses = light_client_prover
4633+ . client
4634+ . http_client ( )
4635+ . get_security_council_addresses ( )
4636+ . await ?;
4637+ assert_eq ! (
4638+ addresses. len( ) ,
4639+ 4 ,
4640+ "CASE 10: Should still have 4 members after replace"
4641+ ) ;
4642+ // Verify the old member is gone and new member is present
4643+ let old_member_addr = format ! ( "{:?}" , _initial_addresses[ 2 ] ) ;
4644+ let new_member_addr = format ! ( "{:?}" , Address :: from_slice( & new_replacement) ) ;
4645+ assert ! (
4646+ !addresses. contains( & old_member_addr) ,
4647+ "CASE 10: Old member should be removed"
4648+ ) ;
4649+ assert ! (
4650+ addresses. contains( & new_member_addr) ,
4651+ "CASE 10: New member should be present"
4652+ ) ;
4653+
45334654 Ok ( ( ) )
45344655 }
45354656}
0 commit comments