@@ -31,11 +31,11 @@ use pallet_ismp::{
3131} ;
3232use pallet_ismp_host_executive:: { EvmHostParam , HostParam } ;
3333use pallet_ismp_relayer:: {
34- self as pallet_ismp_relayer, message,
34+ self as pallet_ismp_relayer, beneficiary_message , message,
3535 withdrawal:: { Signature , WithdrawalInputData , WithdrawalProof } ,
3636 OutboundConsensusDeliveryClaim ,
3737} ;
38- use sp_core:: { keccak_256 , Pair , H256 , U256 } ;
38+ use sp_core:: { Pair , H256 , U256 } ;
3939use sp_trie:: LayoutV0 ;
4040use std:: time:: Duration ;
4141use substrate_state_machine:: { HashAlgorithm , StateMachineProof , SubstrateStateProof } ;
@@ -220,7 +220,13 @@ fn test_accumulate_fees() {
220220
221221 let beneficiary_address = H256 :: random ( ) ;
222222
223- let signature = pair. sign ( & keccak_256 ( beneficiary_address. as_bytes ( ) ) ) . to_vec ( ) ;
223+ let signature = pair
224+ . sign ( & beneficiary_message (
225+ 0 ,
226+ StateMachine :: Kusama ( 2000 ) ,
227+ beneficiary_address. as_bytes ( ) ,
228+ ) )
229+ . to_vec ( ) ;
224230
225231 let withdrawal_proof = WithdrawalProof {
226232 commitments : keys,
@@ -699,7 +705,13 @@ fn test_accumulate_fees_evm_signatures() {
699705
700706 let beneficiary_address = H256 :: random ( ) ;
701707
702- let signature = pair. sign_prehashed ( & keccak_256 ( beneficiary_address. as_bytes ( ) ) ) . to_vec ( ) ;
708+ let signature = pair
709+ . sign_prehashed ( & beneficiary_message (
710+ 0 ,
711+ StateMachine :: Kusama ( 2000 ) ,
712+ beneficiary_address. as_bytes ( ) ,
713+ ) )
714+ . to_vec ( ) ;
703715
704716 let withdrawal_proof = WithdrawalProof {
705717 commitments : keys,
@@ -725,7 +737,7 @@ fn test_accumulate_fees_evm_signatures() {
725737 } ,
726738 beneficiary_details : Some ( (
727739 beneficiary_address. 0 . to_vec ( ) . clone ( ) ,
728- Signature :: Evm { address : eth_address, signature } ,
740+ Signature :: Evm { address : eth_address. clone ( ) , signature } ,
729741 ) ) ,
730742 } ;
731743
@@ -742,6 +754,188 @@ fn test_accumulate_fees_evm_signatures() {
742754 ) ,
743755 U256 :: from( 5000u128 )
744756 ) ;
757+ assert_eq ! (
758+ pallet_ismp_relayer:: Nonce :: <Test >:: get( eth_address, StateMachine :: Kusama ( 2000 ) ) ,
759+ 1 ,
760+ "a successful redirect must consume the signer's nonce"
761+ ) ;
762+ } )
763+ }
764+
765+ /// A redirect signature signed for one source chain must not satisfy verification
766+ /// when reused on another. The state machine is folded into the signed payload
767+ /// so the recovered address only matches the delivery address on the chain the
768+ /// signature was issued for.
769+ #[ test]
770+ fn test_accumulate_fees_rejects_replay_from_other_chain ( ) {
771+ let mut ext = new_test_ext ( ) ;
772+ ext. execute_with ( || {
773+ set_timestamp :: < Test > ( 10_000_000_000 ) ;
774+ let requests = ( 0u64 ..2 )
775+ . into_iter ( )
776+ . map ( |nonce| {
777+ let post = PostRequest {
778+ source : StateMachine :: Kusama ( 2000 ) ,
779+ dest : StateMachine :: Kusama ( 2001 ) ,
780+ nonce,
781+ from : vec ! [ ] ,
782+ to : vec ! [ ] ,
783+ timeout_timestamp : 0 ,
784+ body : vec ! [ ] ,
785+ } ;
786+ hash_request :: < Ismp > ( & Request :: Post ( post) )
787+ } )
788+ . collect :: < Vec < _ > > ( ) ;
789+
790+ let pair = sp_core:: ecdsa:: Pair :: from_seed_slice ( H256 :: random ( ) . as_bytes ( ) ) . unwrap ( ) ;
791+ let eth_address = pair. public ( ) . to_eth_address ( ) . unwrap ( ) . to_vec ( ) ;
792+
793+ let mut source_root = H256 :: default ( ) ;
794+ let mut source_db = MemoryDB :: < KeccakHasher > :: default ( ) ;
795+ let mut source_trie =
796+ TrieDBMutBuilder :: < LayoutV0 < KeccakHasher > > :: new ( & mut source_db, & mut source_root)
797+ . build ( ) ;
798+ let mut dest_root = H256 :: default ( ) ;
799+ let mut dest_db = MemoryDB :: < KeccakHasher > :: default ( ) ;
800+ let mut dest_trie =
801+ TrieDBMutBuilder :: < LayoutV0 < KeccakHasher > > :: new ( & mut dest_db, & mut dest_root) . build ( ) ;
802+
803+ for request in & requests {
804+ let request_commitment_key = RequestCommitments :: < Test > :: storage_key ( * request) ;
805+ let request_receipt_key = RequestReceipts :: < Test > :: storage_key ( * request) ;
806+ let fee_metadata = FeeMetadata :: < Test > { payer : [ 0 ; 32 ] . into ( ) , fee : 1000u128 . into ( ) } ;
807+ let leaf_meta = RequestMetadata {
808+ offchain : LeafIndexAndPos { leaf_index : 0 , pos : 0 } ,
809+ fee : fee_metadata,
810+ claimed : false ,
811+ } ;
812+ RequestCommitments :: < Test > :: insert ( * request, leaf_meta. clone ( ) ) ;
813+ source_trie. insert ( & request_commitment_key, & leaf_meta. encode ( ) ) . unwrap ( ) ;
814+ dest_trie. insert ( & request_receipt_key, & eth_address. encode ( ) ) . unwrap ( ) ;
815+ }
816+ drop ( source_trie) ;
817+ drop ( dest_trie) ;
818+
819+ let mut source_recorder = Recorder :: < LayoutV0 < KeccakHasher > > :: default ( ) ;
820+ let mut dest_recorder = Recorder :: < LayoutV0 < KeccakHasher > > :: default ( ) ;
821+ let source_trie = TrieDBBuilder :: < LayoutV0 < KeccakHasher > > :: new ( & source_db, & source_root)
822+ . with_recorder ( & mut source_recorder)
823+ . build ( ) ;
824+ let dest_trie = TrieDBBuilder :: < LayoutV0 < KeccakHasher > > :: new ( & dest_db, & dest_root)
825+ . with_recorder ( & mut dest_recorder)
826+ . build ( ) ;
827+
828+ let mut keys = vec ! [ ] ;
829+ for request in requests. iter ( ) {
830+ let request_commitment_key = RequestCommitments :: < Test > :: storage_key ( * request) ;
831+ let request_receipt_key = RequestReceipts :: < Test > :: storage_key ( * request) ;
832+ source_trie. get ( & request_commitment_key) . unwrap ( ) ;
833+ dest_trie. get ( & request_receipt_key) . unwrap ( ) ;
834+ keys. push ( * request) ;
835+ }
836+
837+ let source_state_proof = SubstrateStateProof :: OverlayProof ( StateMachineProof {
838+ hasher : HashAlgorithm :: Keccak ,
839+ storage_proof : source_recorder. drain ( ) . into_iter ( ) . map ( |f| f. data ) . collect :: < Vec < _ > > ( ) ,
840+ } ) ;
841+ let dest_state_proof = SubstrateStateProof :: OverlayProof ( StateMachineProof {
842+ hasher : HashAlgorithm :: Keccak ,
843+ storage_proof : dest_recorder. drain ( ) . into_iter ( ) . map ( |f| f. data ) . collect :: < Vec < _ > > ( ) ,
844+ } ) ;
845+
846+ let host = Ismp :: default ( ) ;
847+ for chain in [ StateMachine :: Kusama ( 2000 ) , StateMachine :: Kusama ( 2001 ) ] {
848+ host. store_state_machine_commitment (
849+ StateMachineHeight {
850+ id : StateMachineId {
851+ state_id : chain,
852+ consensus_state_id : MOCK_CONSENSUS_STATE_ID ,
853+ } ,
854+ height : 1 ,
855+ } ,
856+ StateCommitment {
857+ timestamp : 100 ,
858+ overlay_root : Some ( if chain == StateMachine :: Kusama ( 2000 ) {
859+ source_root
860+ } else {
861+ dest_root
862+ } ) ,
863+ state_root : Default :: default ( ) ,
864+ } ,
865+ )
866+ . unwrap ( ) ;
867+ host. store_state_machine_update_time (
868+ StateMachineHeight {
869+ id : StateMachineId {
870+ state_id : chain,
871+ consensus_state_id : MOCK_CONSENSUS_STATE_ID ,
872+ } ,
873+ height : 1 ,
874+ } ,
875+ Duration :: from_secs ( 100 ) ,
876+ )
877+ . unwrap ( ) ;
878+ host. store_challenge_period (
879+ StateMachineId { state_id : chain, consensus_state_id : MOCK_CONSENSUS_STATE_ID } ,
880+ 0 ,
881+ )
882+ . unwrap ( ) ;
883+ }
884+ host. store_consensus_state ( MOCK_CONSENSUS_STATE_ID , Default :: default ( ) ) . unwrap ( ) ;
885+ host. store_consensus_state_id ( MOCK_CONSENSUS_STATE_ID , MOCK_CONSENSUS_CLIENT_ID )
886+ . unwrap ( ) ;
887+ host. store_unbonding_period ( MOCK_CONSENSUS_STATE_ID , 10_000_000_000 ) . unwrap ( ) ;
888+
889+ let beneficiary_address = H256 :: random ( ) ;
890+
891+ // A signature the relayer would have produced for a different source chain.
892+ // The byte payload is reused as if captured from that chain and replayed here.
893+ let foreign_chain = StateMachine :: Kusama ( 7777 ) ;
894+ let signature = pair
895+ . sign_prehashed ( & beneficiary_message ( 0 , foreign_chain, beneficiary_address. as_bytes ( ) ) )
896+ . to_vec ( ) ;
897+
898+ let withdrawal_proof = WithdrawalProof {
899+ commitments : keys,
900+ source_proof : Proof {
901+ height : StateMachineHeight {
902+ id : StateMachineId {
903+ state_id : StateMachine :: Kusama ( 2000 ) ,
904+ consensus_state_id : MOCK_CONSENSUS_STATE_ID ,
905+ } ,
906+ height : 1 ,
907+ } ,
908+ proof : source_state_proof. encode ( ) ,
909+ } ,
910+ dest_proof : Proof {
911+ height : StateMachineHeight {
912+ id : StateMachineId {
913+ state_id : StateMachine :: Kusama ( 2001 ) ,
914+ consensus_state_id : MOCK_CONSENSUS_STATE_ID ,
915+ } ,
916+ height : 1 ,
917+ } ,
918+ proof : dest_state_proof. encode ( ) ,
919+ } ,
920+ beneficiary_details : Some ( (
921+ beneficiary_address. 0 . to_vec ( ) ,
922+ Signature :: Evm { address : eth_address, signature } ,
923+ ) ) ,
924+ } ;
925+
926+ let result = pallet_ismp_relayer:: Pallet :: < Test > :: accumulate_fees (
927+ RuntimeOrigin :: none ( ) ,
928+ withdrawal_proof,
929+ ) ;
930+
931+ assert ! ( result. is_err( ) , "replayed signature from a different chain must be rejected" ) ;
932+ assert_eq ! (
933+ pallet_ismp_relayer:: Fees :: <Test >:: get(
934+ StateMachine :: Kusama ( 2000 ) ,
935+ beneficiary_address. 0 . to_vec( )
936+ ) ,
937+ U256 :: zero( )
938+ ) ;
745939 } )
746940}
747941
0 commit comments