Skip to content

Commit d5747e2

Browse files
committed
test: end-to-end proof that forged solana gateway events are not stored (F-2026-18198)
1 parent f12ed18 commit d5747e2

1 file changed

Lines changed: 75 additions & 0 deletions

File tree

universalClient/chains/svm/event_listener_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)