@@ -15,6 +15,7 @@ import (
1515 "github.com/stretchr/testify/assert"
1616 "github.com/stretchr/testify/require"
1717
18+ "github.com/pushchain/push-chain-node/universalClient/chains/common"
1819 "github.com/pushchain/push-chain-node/universalClient/db"
1920 uregistrytypes "github.com/pushchain/push-chain-node/x/uregistry/types"
2021)
@@ -805,3 +806,77 @@ func TestInvokedProgramAndExit(t *testing.T) {
805806 assert .False (t , isProgramExit ("Program data: AQID" ))
806807 assert .False (t , isProgramExit ("Program " + testGatewayProgram + " consumed 1 of 2 compute units" ))
807808}
809+
810+ // forgeryRPC serves one transaction whose logs the test controls.
811+ type forgeryRPC struct {
812+ slot uint64
813+ sig solana.Signature
814+ logs []string
815+ }
816+
817+ func (m * forgeryRPC ) GetLatestSlot (context.Context ) (uint64 , error ) { return m .slot , nil }
818+
819+ func (m * forgeryRPC ) GetSignaturesForAddress (context.Context , solana.PublicKey , solana.Signature ) ([]* solanarpc.TransactionSignature , error ) {
820+ return []* solanarpc.TransactionSignature {{Signature : m .sig , Slot : m .slot }}, nil
821+ }
822+
823+ func (m * forgeryRPC ) GetTransaction (context.Context , solana.Signature ) (* solanarpc.GetTransactionResult , error ) {
824+ return & solanarpc.GetTransactionResult {
825+ Slot : m .slot ,
826+ Meta : & solanarpc.TransactionMeta {LogMessages : m .logs },
827+ }, nil
828+ }
829+
830+ // End-to-end proof that the listener drops a forged event. The same valid
831+ // send_funds payload is served twice: emitted by an attacker program it must be
832+ // ignored, emitted by the gateway it must be stored. Running both with one
833+ // payload shows attribution is what rejects it, not a decode failure.
834+ func TestProcessSignatureBatch_RejectsForgedGatewayEvent (t * testing.T ) {
835+ discriminator := "0000000000000000" // buildSendFundsPayload zeroes the discriminator
836+ payload := buildSendFundsPayload (
837+ [32 ]byte {1 }, [20 ]byte {2 }, [32 ]byte {3 }, 1_000_000 ,
838+ nil , [32 ]byte {4 }, 0 , nil , false ,
839+ )
840+ dataLog := "Program data: " + base64 .StdEncoding .EncodeToString (payload )
841+
842+ run := func (t * testing.T , logs []string ) int {
843+ t .Helper ()
844+ database , err := db .OpenInMemoryDB (true )
845+ require .NoError (t , err )
846+ t .Cleanup (func () { database .Close () })
847+
848+ methods := []* uregistrytypes.GatewayMethods {
849+ {Name : EventTypeSendFunds , EventIdentifier : discriminator },
850+ }
851+ rpc := & forgeryRPC {slot : 100 , sig : mkSig (7 ), logs : logs }
852+ el , err := NewEventListener (rpc , testGatewayProgram , "solana:test" , methods , database , 10 , nil , zerolog .Nop ())
853+ require .NoError (t , err )
854+
855+ _ , err = el .processSignatureBatch (context .Background (), []* solanarpc.TransactionSignature {
856+ {Signature : mkSig (7 ), Slot : 100 },
857+ }, 0 , 200 )
858+ require .NoError (t , err )
859+
860+ events , err := common .NewChainStore (database ).GetPendingEvents (100 )
861+ require .NoError (t , err )
862+ return len (events )
863+ }
864+
865+ t .Run ("forged by attacker program is not stored" , func (t * testing.T ) {
866+ stored := run (t , []string {
867+ "Program " + testAttackerProgram + " invoke [1]" ,
868+ dataLog ,
869+ "Program " + testAttackerProgram + " success" ,
870+ })
871+ assert .Zero (t , stored , "forged gateway event must not become an inbound" )
872+ })
873+
874+ t .Run ("same payload from the gateway is stored" , func (t * testing.T ) {
875+ stored := run (t , []string {
876+ "Program " + testGatewayProgram + " invoke [1]" ,
877+ dataLog ,
878+ "Program " + testGatewayProgram + " success" ,
879+ })
880+ assert .Equal (t , 1 , stored , "genuine gateway event must be observed" )
881+ })
882+ }
0 commit comments