diff --git a/cmd/stellar-rpc/internal/methods/simulate_transaction.go b/cmd/stellar-rpc/internal/methods/simulate_transaction.go index 4d0f4d9dc..f786f908b 100644 --- a/cmd/stellar-rpc/internal/methods/simulate_transaction.go +++ b/cmd/stellar-rpc/internal/methods/simulate_transaction.go @@ -227,6 +227,28 @@ func formatResponse(preflight preflight.Preflight, StateChanges: stateChanges, } + // stellar core charges the budget's contract_events_and_return_value_size + // cost with the XDR bytes of successful contract events plus the return + // value, so diagnostic output must not count toward this limit. + var totalEventsSize uint64 + for _, eventBytes := range preflight.Events { + var diagEvent xdr.DiagnosticEvent + if err := xdr.SafeUnmarshal(eventBytes, &diagEvent); err != nil { + continue + } + if !diagEvent.InSuccessfulContractCall || diagEvent.Event.Type != xdr.ContractEventTypeContract { + continue + } + if encoded, err := diagEvent.Event.MarshalBinary(); err == nil { + totalEventsSize += uint64(len(encoded)) + } + } + totalEventsSize += uint64(len(preflight.Result)) + const defaultMaxContractEventsSizeBytes uint64 = 16384 + if simResp.Error == "" && totalEventsSize > defaultMaxContractEventsSizeBytes { + simResp.Error = fmt.Sprintf("total contract events size (%d bytes) exceeds maximum limit (%d bytes)", totalEventsSize, defaultMaxContractEventsSizeBytes) + } + switch format { case protocol.FormatJSON: simResp.TransactionDataJSON, err = xdr2json.ConvertBytes( diff --git a/cmd/stellar-rpc/internal/methods/simulate_transaction_test.go b/cmd/stellar-rpc/internal/methods/simulate_transaction_test.go index c21dd984d..165ffb371 100644 --- a/cmd/stellar-rpc/internal/methods/simulate_transaction_test.go +++ b/cmd/stellar-rpc/internal/methods/simulate_transaction_test.go @@ -678,3 +678,60 @@ func TestSimulateTransactionThreadsUseUpgradedAuth(t *testing.T) { }) } } + +func TestSimulateTransaction_ContractEventsSizeLimit(t *testing.T) { + encodedEvent := func(t *testing.T, inSuccessful bool, eventType xdr.ContractEventType, size int) []byte { + payload := xdr.ScBytes(make([]byte, size)) + event := xdr.DiagnosticEvent{ + InSuccessfulContractCall: inSuccessful, + Event: xdr.ContractEvent{ + Type: eventType, + Body: xdr.ContractEventBody{ + V: 0, + V0: &xdr.ContractEventV0{ + Data: xdr.ScVal{ + Type: xdr.ScValTypeScvBytes, + Bytes: &payload, + }, + }, + }, + }, + } + b, err := event.MarshalBinary() + require.NoError(t, err) + return b + } + + t.Run("successful contract events over the limit", func(t *testing.T) { + pf := preflight.Preflight{ + Events: [][]byte{ + encodedEvent(t, true, xdr.ContractEventTypeContract, 10000), + encodedEvent(t, true, xdr.ContractEventTypeContract, 7000), + }, + } + resp, err := formatResponse(pf, protocol.FormatBase64, 100) + require.NoError(t, err) + require.Contains(t, resp.Error, "total contract events size") + require.Contains(t, resp.Error, "exceeds maximum limit") + }) + + t.Run("diagnostic and system events are not counted", func(t *testing.T) { + pf := preflight.Preflight{ + Events: [][]byte{ + encodedEvent(t, false, xdr.ContractEventTypeContract, 10000), + encodedEvent(t, true, xdr.ContractEventTypeSystem, 10000), + }, + } + resp, err := formatResponse(pf, protocol.FormatBase64, 100) + require.NoError(t, err) + require.Empty(t, resp.Error) + }) + + t.Run("return value alone can exceed the limit", func(t *testing.T) { + pf := preflight.Preflight{Result: make([]byte, 16385)} + resp, err := formatResponse(pf, protocol.FormatBase64, 100) + require.NoError(t, err) + require.Contains(t, resp.Error, "exceeds maximum limit") + }) +} +