Skip to content

Commit

Permalink
rpc: fix multi-entry responses for getLedger entries (#1093)
Browse files Browse the repository at this point in the history
The key->entry correspondence was broken since the code assumed a
1-to-1 match (in size and ordering) between the keys in the request
and the keys in the response.

This didn't hold because:

1. Some keys may be missing in the response (if not found in the db).
2. The db access method (`LedgerEntryReadTx.GetLedgerEntries()`)
   returned a result whose ordering wasn't consistent with its arguments
   (due to traversing a map to build the result).

This change:

1. Fixes the key->entry mapping
2. Makes `getLedgerEntries` response ordering stable and consistent with
   the keys provided.
3. Make a few small performance improvements (mostly allocation related) along the way.
  • Loading branch information
2opremio authored Nov 21, 2023
1 parent c560bdd commit 3928c47
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 46 deletions.
39 changes: 22 additions & 17 deletions cmd/soroban-rpc/internal/db/ledgerentry.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,16 +260,22 @@ func entryKeyToTTLEntryKey(key xdr.LedgerKey) (xdr.LedgerKey, error) {
}

func (l *ledgerEntryReadTx) GetLedgerEntries(keys ...xdr.LedgerKey) ([]LedgerKeyAndEntry, error) {
encodedKeys := make([]string, len(keys), 2*len(keys))
encodedKeyToKey := make(map[string]xdr.LedgerKey, len(keys))
encodedKeyToEncodedTTLLedgerKey := make(map[string]string, len(keys))
for _, k := range keys {
encodedKeys := make([]string, 0, 2*len(keys))
type keyToEncoded struct {
key xdr.LedgerKey
encodedKey string
encodedTTLKey *string
}
keysToEncoded := make([]keyToEncoded, len(keys))
for i, k := range keys {
k2 := k
keysToEncoded[i].key = k2
encodedKey, err := encodeLedgerKey(l.buffer, k)
if err != nil {
return nil, err
}
keysToEncoded[i].encodedKey = encodedKey
encodedKeys = append(encodedKeys, encodedKey)
encodedKeyToKey[encodedKey] = k
if !hasTTLKey(k) {
continue
}
Expand All @@ -281,7 +287,7 @@ func (l *ledgerEntryReadTx) GetLedgerEntries(keys ...xdr.LedgerKey) ([]LedgerKey
if err != nil {
return nil, err
}
encodedKeyToEncodedTTLLedgerKey[encodedKey] = encodedTTLKey
keysToEncoded[i].encodedTTLKey = &encodedTTLKey
encodedKeys = append(encodedKeys, encodedTTLKey)
}

Expand All @@ -290,32 +296,31 @@ func (l *ledgerEntryReadTx) GetLedgerEntries(keys ...xdr.LedgerKey) ([]LedgerKey
return nil, err
}

result := make([]LedgerKeyAndEntry, 0, len(rawResult))
for encodedKey, key := range encodedKeyToKey {
encodedEntry, ok := rawResult[encodedKey]
result := make([]LedgerKeyAndEntry, 0, len(keys))
for _, k2e := range keysToEncoded {
encodedEntry, ok := rawResult[k2e.encodedKey]
if !ok {
continue
}
var entry xdr.LedgerEntry
if err := xdr.SafeUnmarshal([]byte(encodedEntry), &entry); err != nil {
return nil, errors.Wrap(err, "cannot decode ledger entry from DB")
}
encodedExpKey, has := encodedKeyToEncodedTTLLedgerKey[encodedKey]
if !has {
result = append(result, LedgerKeyAndEntry{key, entry, nil})
if k2e.encodedTTLKey == nil {
result = append(result, LedgerKeyAndEntry{k2e.key, entry, nil})
continue
}
encodedExpEntry, ok := rawResult[encodedExpKey]
encodedTTLEntry, ok := rawResult[*k2e.encodedTTLKey]
if !ok {
// missing ttl key. This should not happen.
return nil, errors.New("missing ttl key entry")
}
var expEntry xdr.LedgerEntry
if err := xdr.SafeUnmarshal([]byte(encodedExpEntry), &expEntry); err != nil {
var ttlEntry xdr.LedgerEntry
if err := xdr.SafeUnmarshal([]byte(encodedTTLEntry), &ttlEntry); err != nil {
return nil, errors.Wrap(err, "cannot decode TTL ledger entry from DB")
}
liveUntilSeq := uint32(expEntry.Data.Ttl.LiveUntilLedgerSeq)
result = append(result, LedgerKeyAndEntry{key, entry, &liveUntilSeq})
liveUntilSeq := uint32(ttlEntry.Data.Ttl.LiveUntilLedgerSeq)
result = append(result, LedgerKeyAndEntry{k2e.key, entry, &liveUntilSeq})
}

return result, nil
Expand Down
18 changes: 14 additions & 4 deletions cmd/soroban-rpc/internal/methods/get_ledger_entries.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,18 @@ func NewGetLedgerEntriesHandler(logger *log.Entry, ledgerEntryReader db.LedgerEn
}
}

for i, ledgerKeyAndEntry := range ledgerKeysAndEntries {
ledgerXDR, err := xdr.MarshalBase64(ledgerKeyAndEntry.Entry.Data)
for _, ledgerKeyAndEntry := range ledgerKeysAndEntries {
keyXDR, err := xdr.MarshalBase64(ledgerKeyAndEntry.Key)
if err != nil {
logger.WithError(err).WithField("request", request).
Infof("could not serialize ledger key %v", ledgerKeyAndEntry.Key)
return GetLedgerEntriesResponse{}, &jrpc2.Error{
Code: jrpc2.InternalError,
Message: fmt.Sprintf("could not serialize ledger key %v", ledgerKeyAndEntry.Key),
}
}

entryXDR, err := xdr.MarshalBase64(ledgerKeyAndEntry.Entry.Data)
if err != nil {
logger.WithError(err).WithField("request", request).
Infof("could not serialize ledger entry data for ledger entry %v", ledgerKeyAndEntry.Entry)
Expand All @@ -120,8 +130,8 @@ func NewGetLedgerEntriesHandler(logger *log.Entry, ledgerEntryReader db.LedgerEn
}

ledgerEntryResults = append(ledgerEntryResults, LedgerEntryResult{
Key: request.Keys[i],
XDR: ledgerXDR,
Key: keyXDR,
XDR: entryXDR,
LastModifiedLedger: int64(ledgerKeyAndEntry.Entry.LastModifiedLedgerSeq),
LiveUntilLedgerSeq: ledgerKeyAndEntry.LiveUntilLedgerSeq,
})
Expand Down
70 changes: 45 additions & 25 deletions cmd/soroban-rpc/internal/test/get_ledger_entries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"github.com/stretchr/testify/require"

"github.com/stellar/go/keypair"
proto "github.com/stellar/go/protocols/stellarcore"
"github.com/stellar/go/txnbuild"
"github.com/stellar/go/xdr"

Expand Down Expand Up @@ -80,8 +79,9 @@ func TestGetLedgerEntriesSucceeds(t *testing.T) {
ch := jhttp.NewChannel(test.sorobanRPCURL(), nil)
client := jrpc2.NewClient(ch, nil)

kp := keypair.Root(StandaloneNetworkPassphrase)
account := txnbuild.NewSimpleAccount(kp.Address(), 0)
sourceAccount := keypair.Root(StandaloneNetworkPassphrase)
address := sourceAccount.Address()
account := txnbuild.NewSimpleAccount(address, 0)

contractBinary := getHelloWorldContract(t)
params := preflightTransactionParams(t, client, txnbuild.TransactionParams{
Expand All @@ -96,35 +96,40 @@ func TestGetLedgerEntriesSucceeds(t *testing.T) {
},
})
tx, err := txnbuild.NewTransaction(params)
require.NoError(t, err)
tx, err = tx.Sign(StandaloneNetworkPassphrase, kp)
require.NoError(t, err)
b64, err := tx.Base64()
require.NoError(t, err)
assert.NoError(t, err)
sendSuccessfulTransaction(t, client, sourceAccount, tx)

sendTxRequest := methods.SendTransactionRequest{Transaction: b64}
var sendTxResponse methods.SendTransactionResponse
err = client.CallResult(context.Background(), "sendTransaction", sendTxRequest, &sendTxResponse)
require.NoError(t, err)
require.Equal(t, proto.TXStatusPending, sendTxResponse.Status)
params = preflightTransactionParams(t, client, txnbuild.TransactionParams{
SourceAccount: &account,
IncrementSequenceNum: true,
Operations: []txnbuild.Operation{
createCreateContractOperation(address, contractBinary),
},
BaseFee: txnbuild.MinBaseFee,
Preconditions: txnbuild.Preconditions{
TimeBounds: txnbuild.NewInfiniteTimeout(),
},
})
tx, err = txnbuild.NewTransaction(params)
assert.NoError(t, err)
sendSuccessfulTransaction(t, client, sourceAccount, tx)

txStatusResponse := getTransaction(t, client, sendTxResponse.Hash)
require.Equal(t, methods.TransactionStatusSuccess, txStatusResponse.Status)
contractID := getContractID(t, address, testSalt, StandaloneNetworkPassphrase)

contractHash := sha256.Sum256(contractBinary)
contractKeyB64, err := xdr.MarshalBase64(xdr.LedgerKey{
contractCodeKeyB64, err := xdr.MarshalBase64(xdr.LedgerKey{
Type: xdr.LedgerEntryTypeContractCode,
ContractCode: &xdr.LedgerKeyContractCode{
Hash: contractHash,
},
})
require.NoError(t, err)

// Doesn't exist.
sourceAccount := keypair.Root(StandaloneNetworkPassphrase).Address()
contractID := getContractID(t, sourceAccount, testSalt, StandaloneNetworkPassphrase)
notFoundKeyB64, err := xdr.MarshalBase64(getCounterLedgerKey(contractID))
require.NoError(t, err)

contractIDHash := xdr.Hash(contractID)
notFoundKeyB64, err := xdr.MarshalBase64(xdr.LedgerKey{
contractInstanceKeyB64, err := xdr.MarshalBase64(xdr.LedgerKey{
Type: xdr.LedgerEntryTypeContractData,
ContractData: &xdr.LedgerKeyContractData{
Contract: xdr.ScAddress{
Expand All @@ -139,21 +144,36 @@ func TestGetLedgerEntriesSucceeds(t *testing.T) {
})
require.NoError(t, err)

var keys []string
keys = append(keys, contractKeyB64)
keys = append(keys, notFoundKeyB64)
keys := []string{contractCodeKeyB64, notFoundKeyB64, contractInstanceKeyB64}
request := methods.GetLedgerEntriesRequest{
Keys: keys,
}

var result methods.GetLedgerEntriesResponse
err = client.CallResult(context.Background(), "getLedgerEntries", request, &result)
require.NoError(t, err)
require.Equal(t, 1, len(result.Entries))
require.Equal(t, 2, len(result.Entries))
require.Greater(t, result.LatestLedger, int64(0))

require.Greater(t, result.Entries[0].LastModifiedLedger, int64(0))
require.LessOrEqual(t, result.Entries[0].LastModifiedLedger, result.LatestLedger)
require.NotNil(t, result.Entries[0].LiveUntilLedgerSeq)
require.Greater(t, *result.Entries[0].LiveUntilLedgerSeq, uint32(result.LatestLedger))
require.Equal(t, contractCodeKeyB64, result.Entries[0].Key)
var firstEntry xdr.LedgerEntryData
require.NoError(t, xdr.SafeUnmarshalBase64(result.Entries[0].XDR, &firstEntry))
require.Equal(t, xdr.LedgerEntryTypeContractCode, firstEntry.Type)
require.Equal(t, contractBinary, firstEntry.MustContractCode().Code)
require.Equal(t, contractKeyB64, result.Entries[0].Key)

require.Greater(t, result.Entries[1].LastModifiedLedger, int64(0))
require.LessOrEqual(t, result.Entries[1].LastModifiedLedger, result.LatestLedger)
require.NotNil(t, result.Entries[1].LiveUntilLedgerSeq)
require.Greater(t, *result.Entries[1].LiveUntilLedgerSeq, uint32(result.LatestLedger))
require.Equal(t, contractInstanceKeyB64, result.Entries[1].Key)
var secondEntry xdr.LedgerEntryData
require.NoError(t, xdr.SafeUnmarshalBase64(result.Entries[1].XDR, &secondEntry))
require.Equal(t, xdr.LedgerEntryTypeContractData, secondEntry.Type)
require.True(t, secondEntry.MustContractData().Key.Equals(xdr.ScVal{
Type: xdr.ScValTypeScvLedgerKeyContractInstance,
}))
}

0 comments on commit 3928c47

Please sign in to comment.