Skip to content

Commit

Permalink
Merge pull request #120 from stellar/compute-ledger-entry-diff
Browse files Browse the repository at this point in the history
Add ledger entry diff to simulateTransaction response
  • Loading branch information
2opremio authored Apr 4, 2024
2 parents 1f735e2 + 9a4d755 commit d0c22a3
Show file tree
Hide file tree
Showing 8 changed files with 345 additions and 27 deletions.
3 changes: 3 additions & 0 deletions .github/actions/setup-integration-tests/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ runs:
sudo apt-get remove -y moby-compose
sudo apt-get install -y docker-compose-plugin
# add alias for docker compose
ln -f -s /usr/libexec/docker/cli-plugins/docker-compose /usr/local/bin/docker-compose
echo "Docker Compose Version:"
docker-compose version
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/soroban-rpc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ jobs:
matrix:
os: [ubuntu-20.04, ubuntu-22.04]
go: [1.22]
test: ['.*CLI.*', '^Test(([^C])|(C[^L])|(CL[^I])).*$']
runs-on: ${{ matrix.os }}
env:
SOROBAN_RPC_INTEGRATION_TESTS_ENABLED: true
Expand All @@ -127,4 +126,4 @@ jobs:
- name: Run Soroban RPC Integration Tests
run: |
make install_rust
go test -race -run '${{ matrix.test }}' -timeout 60m -v ./cmd/soroban-rpc/internal/test/...
go test -race -timeout 60m -v ./cmd/soroban-rpc/internal/test/...
112 changes: 112 additions & 0 deletions cmd/soroban-rpc/internal/methods/simulate_transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package methods
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"

"github.com/creachadair/jrpc2"
"github.com/creachadair/jrpc2/handler"
Expand Down Expand Up @@ -34,6 +37,108 @@ type RestorePreamble struct {
TransactionData string `json:"transactionData"` // SorobanTransactionData XDR in base64
MinResourceFee int64 `json:"minResourceFee,string"`
}
type LedgerEntryChangeType int

const (
LedgerEntryChangeTypeCreated LedgerEntryChangeType = iota + 1
LedgerEntryChangeTypeUpdated
LedgerEntryChangeTypeDeleted
)

var (
LedgerEntryChangeTypeName = map[LedgerEntryChangeType]string{
LedgerEntryChangeTypeCreated: "created",
LedgerEntryChangeTypeUpdated: "updated",
LedgerEntryChangeTypeDeleted: "deleted",
}
LedgerEntryChangeTypeValue = map[string]LedgerEntryChangeType{
"created": LedgerEntryChangeTypeCreated,
"updated": LedgerEntryChangeTypeUpdated,
"deleted": LedgerEntryChangeTypeDeleted,
}
)

func (l LedgerEntryChangeType) String() string {
return LedgerEntryChangeTypeName[l]
}

func (l LedgerEntryChangeType) MarshalJSON() ([]byte, error) {
return json.Marshal(l.String())
}

func (l *LedgerEntryChangeType) Parse(s string) error {
s = strings.TrimSpace(strings.ToLower(s))
value, ok := LedgerEntryChangeTypeValue[s]
if !ok {
return fmt.Errorf("%q is not a valid ledger entry change type", s)
}
*l = value
return nil
}

func (l *LedgerEntryChangeType) UnmarshalJSON(data []byte) (err error) {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
return l.Parse(s)
}

func (l *LedgerEntryChange) FromXDRDiff(diff preflight.XDRDiff) error {
beforePresent := len(diff.Before) > 0
afterPresent := len(diff.After) > 0
var (
entryXDR []byte
changeType LedgerEntryChangeType
)
switch {
case beforePresent:
entryXDR = diff.Before
if afterPresent {
changeType = LedgerEntryChangeTypeUpdated
} else {
changeType = LedgerEntryChangeTypeDeleted
}
case afterPresent:
entryXDR = diff.After
changeType = LedgerEntryChangeTypeCreated
default:
return errors.New("missing before and after")
}
var entry xdr.LedgerEntry

if err := xdr.SafeUnmarshal(entryXDR, &entry); err != nil {
return err
}
key, err := entry.LedgerKey()
if err != nil {
return err
}
keyB64, err := xdr.MarshalBase64(key)
if err != nil {
return err
}
l.Type = changeType
l.Key = keyB64
if beforePresent {
before := base64.StdEncoding.EncodeToString(diff.Before)
l.Before = &before
}
if afterPresent {
after := base64.StdEncoding.EncodeToString(diff.After)
l.After = &after
}
return nil
}

// LedgerEntryChange designates a change in a ledger entry. Before and After cannot be be omitted at the same time.
// If Before is omitted, it constitutes a creation, if After is omitted, it constitutes a delation.
type LedgerEntryChange struct {
Type LedgerEntryChangeType
Key string // LedgerEntryKey in base64
Before *string `json:"before"` // LedgerEntry XDR in base64
After *string `json:"after"` // LedgerEntry XDR in base64
}

type SimulateTransactionResponse struct {
Error string `json:"error,omitempty"`
Expand All @@ -43,6 +148,7 @@ type SimulateTransactionResponse struct {
Results []SimulateHostFunctionResult `json:"results,omitempty"` // an array of the individual host function call results
Cost SimulateTransactionCost `json:"cost,omitempty"` // the effective cpu and memory cost of the invoked transaction execution.
RestorePreamble *RestorePreamble `json:"restorePreamble,omitempty"` // If present, it indicates that a prior RestoreFootprint is required
StateChanges []LedgerEntryChange `json:"stateChanges,omitempty"` // If present, it indicates how the state (ledger entries) will change as a result of the transaction execution.
LatestLedger uint32 `json:"latestLedger"`
}

Expand Down Expand Up @@ -149,6 +255,11 @@ func NewSimulateTransactionHandler(logger *log.Entry, ledgerEntryReader db.Ledge
}
}

stateChanges := make([]LedgerEntryChange, len(result.LedgerEntryDiff))
for i := 0; i < len(stateChanges); i++ {
stateChanges[i].FromXDRDiff(result.LedgerEntryDiff[i])
}

return SimulateTransactionResponse{
Error: result.Error,
Results: results,
Expand All @@ -161,6 +272,7 @@ func NewSimulateTransactionHandler(logger *log.Entry, ledgerEntryReader db.Ledge
},
LatestLedger: latestLedger,
RestorePreamble: restorePreamble,
StateChanges: stateChanges,
}
})
}
Expand Down
94 changes: 94 additions & 0 deletions cmd/soroban-rpc/internal/methods/simulate_transaction_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package methods

import (
"encoding/base64"
"encoding/json"
"testing"

"github.com/stellar/go/xdr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/preflight"
)

func TestLedgerEntryChange(t *testing.T) {
entry := xdr.LedgerEntry{
LastModifiedLedgerSeq: 100,
Data: xdr.LedgerEntryData{
Type: xdr.LedgerEntryTypeAccount,
Account: &xdr.AccountEntry{
AccountId: xdr.MustAddress("GBXGQJWVLWOYHFLVTKWV5FGHA3LNYY2JQKM7OAJAUEQFU6LPCSEFVXON"),
Balance: 100,
SeqNum: 1,
},
},
}

entryXDR, err := entry.MarshalBinary()
require.NoError(t, err)
entryB64 := base64.StdEncoding.EncodeToString(entryXDR)

key, err := entry.LedgerKey()
require.NoError(t, err)
keyXDR, err := key.MarshalBinary()
require.NoError(t, err)
keyB64 := base64.StdEncoding.EncodeToString(keyXDR)

for _, test := range []struct {
name string
input preflight.XDRDiff
expectedOutput LedgerEntryChange
}{
{
name: "creation",
input: preflight.XDRDiff{
Before: nil,
After: entryXDR,
},
expectedOutput: LedgerEntryChange{
Type: LedgerEntryChangeTypeCreated,
Key: keyB64,
Before: nil,
After: &entryB64,
},
},
{
name: "deletion",
input: preflight.XDRDiff{
Before: entryXDR,
After: nil,
},
expectedOutput: LedgerEntryChange{
Type: LedgerEntryChangeTypeDeleted,
Key: keyB64,
Before: &entryB64,
After: nil,
},
},
{
name: "update",
input: preflight.XDRDiff{
Before: entryXDR,
After: entryXDR,
},
expectedOutput: LedgerEntryChange{
Type: LedgerEntryChangeTypeUpdated,
Key: keyB64,
Before: &entryB64,
After: &entryB64,
},
},
} {
var change LedgerEntryChange
require.NoError(t, change.FromXDRDiff(test.input), test.name)
assert.Equal(t, test.expectedOutput, change)

// test json roundtrip
changeJSON, err := json.Marshal(change)
require.NoError(t, err, test.name)
var change2 LedgerEntryChange
require.NoError(t, json.Unmarshal(changeJSON, &change2))
assert.Equal(t, change, change2, test.name)
}
}
17 changes: 17 additions & 0 deletions cmd/soroban-rpc/internal/preflight/preflight.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ type PreflightParameters struct {
EnableDebug bool
}

type XDRDiff struct {
Before []byte // optional before XDR
After []byte // optional after XDR
}

type Preflight struct {
Error string
Events [][]byte // DiagnosticEvents XDR
Expand All @@ -116,6 +121,7 @@ type Preflight struct {
MemoryBytes uint64
PreRestoreTransactionData []byte // SorobanTransactionData XDR
PreRestoreMinFee int64
LedgerEntryDiff []XDRDiff
}

func CXDR(xdr []byte) C.xdr_t {
Expand All @@ -138,6 +144,16 @@ func GoXDRVector(xdrVector C.xdr_vector_t) [][]byte {
return result
}

func GoXDRDiffVector(xdrDiffVector C.xdr_diff_vector_t) []XDRDiff {
result := make([]XDRDiff, xdrDiffVector.len)
inputSlice := unsafe.Slice(xdrDiffVector.array, xdrDiffVector.len)
for i, v := range inputSlice {
result[i].Before = GoXDR(v.before)
result[i].After = GoXDR(v.after)
}
return result
}

func GetPreflight(ctx context.Context, params PreflightParameters) (Preflight, error) {
switch params.OpBody.Type {
case xdr.OperationTypeInvokeHostFunction:
Expand Down Expand Up @@ -259,6 +275,7 @@ func GoPreflight(result *C.preflight_result_t) Preflight {
MemoryBytes: uint64(result.memory_bytes),
PreRestoreTransactionData: GoXDR(result.pre_restore_transaction_data),
PreRestoreMinFee: int64(result.pre_restore_min_fee),
LedgerEntryDiff: GoXDRDiffVector(result.ledger_entry_diff),
}
return preflight
}
14 changes: 14 additions & 0 deletions cmd/soroban-rpc/internal/test/simulate_transaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,20 @@ func TestSimulateTransactionSucceeds(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, expectedXdr, resultXdr)

// Check state diff
assert.Len(t, result.StateChanges, 1)
assert.Nil(t, result.StateChanges[0].Before)
assert.NotNil(t, result.StateChanges[0].After)
assert.Equal(t, methods.LedgerEntryChangeTypeCreated, result.StateChanges[0].Type)
var after xdr.LedgerEntry
assert.NoError(t, xdr.SafeUnmarshalBase64(*result.StateChanges[0].After, &after))
assert.Equal(t, xdr.LedgerEntryTypeContractCode, after.Data.Type)
entryKey, err := after.LedgerKey()
assert.NoError(t, err)
entryKeyB64, err := xdr.MarshalBase64(entryKey)
assert.NoError(t, err)
assert.Equal(t, entryKeyB64, result.StateChanges[0].Key)

// test operation which does not have a source account
withoutSourceAccountOp := createInstallContractCodeOperation("", contractBinary)
params = txnbuild.TransactionParams{
Expand Down
31 changes: 21 additions & 10 deletions cmd/soroban-rpc/lib/preflight.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,32 @@ typedef struct xdr_vector_t {
size_t len;
} xdr_vector_t;

typedef struct xdr_diff_t {
xdr_t before;
xdr_t after;
} xdr_diff_t;

typedef struct xdr_diff_vector_t {
xdr_diff_t *array;
size_t len;
} xdr_diff_vector_t;

typedef struct resource_config_t {
uint64_t instruction_leeway; // Allow this many extra instructions when budgeting
} resource_config_t;

typedef struct preflight_result_t {
char *error; // Error string in case of error, otherwise null
xdr_vector_t auth; // array of SorobanAuthorizationEntries
xdr_t result; // XDR SCVal
xdr_t transaction_data;
int64_t min_fee; // Minimum recommended resource fee
xdr_vector_t events; // array of XDR DiagnosticEvents
uint64_t cpu_instructions;
uint64_t memory_bytes;
xdr_t pre_restore_transaction_data; // SorobanTransactionData XDR for a prerequired RestoreFootprint operation
int64_t pre_restore_min_fee; // Minimum recommended resource fee for a prerequired RestoreFootprint operation
char *error; // Error string in case of error, otherwise null
xdr_vector_t auth; // array of SorobanAuthorizationEntries
xdr_t result; // XDR SCVal
xdr_t transaction_data;
int64_t min_fee; // Minimum recommended resource fee
xdr_vector_t events; // array of XDR DiagnosticEvents
uint64_t cpu_instructions;
uint64_t memory_bytes;
xdr_t pre_restore_transaction_data; // SorobanTransactionData XDR for a prerequired RestoreFootprint operation
int64_t pre_restore_min_fee; // Minimum recommended resource fee for a prerequired RestoreFootprint operation
xdr_diff_vector_t ledger_entry_diff; // Contains the ledger entry changes which would be caused by the transaction execution
} preflight_result_t;

preflight_result_t *preflight_invoke_hf_op(uintptr_t handle, // Go Handle to forward to SnapshotSourceGet
Expand Down
Loading

0 comments on commit d0c22a3

Please sign in to comment.