Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions pkg/replay/address_lookup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package replay

import (
"errors"

"github.com/Overclock-Validator/mithril/pkg/accounts"
"github.com/Overclock-Validator/mithril/pkg/addresses"
"github.com/Overclock-Validator/mithril/pkg/sealevel"
"github.com/gagliardetto/solana-go"
)

var (
TxErrAddressLookupTableNotFound = errors.New("TxErrAddressLookupTableNotFound")
TxErrInvalidAddressLookupTableOwner = errors.New("TxErrInvalidAddressLookupTableOwner")
TxErrInvalidAddressLookupTableData = errors.New("TxErrInvalidAddressLookupTableData")
TxErrInvalidAddressLookupTableIndex = errors.New("TxErrInvalidAddressLookupTableIndex")
)

// activeLookupTable applies the Solana protocol's table rules to a table
// account as read for slot, judging deactivation against the SlotHashes of
// the bank executing at slot. It returns the table's active addresses:
//
// - a missing table, or a deactivated one, fails with AddressLookupTableNotFound;
// - a table not owned by the address lookup table program fails with
// InvalidAddressLookupTableOwner, and one that does not decode with
// InvalidAddressLookupTableData.
func activeLookupTable(acct *accounts.Account, slot uint64, slotHashes sealevel.SysvarSlotHashes) (solana.PublicKeySlice, *TransactionError) {
if acct == nil || acct.Lamports == 0 {
return nil, &TransactionError{ErrorType: TransactionErrorAddressLookupTableNotFound, InstructionError: TxErrAddressLookupTableNotFound}
}
if acct.Owner != addresses.AddressLookupTableAddr {
return nil, &TransactionError{ErrorType: TransactionErrorInvalidAddressLookupTableOwner, InstructionError: TxErrInvalidAddressLookupTableOwner}
}
table, err := sealevel.UnmarshalAddressLookupTable(acct.Data)
if err != nil {
return nil, &TransactionError{ErrorType: TransactionErrorInvalidAddressLookupTableData, InstructionError: TxErrInvalidAddressLookupTableData}
}
// A deactivated table may be closed at any point, so it is reported as
// missing rather than with an error of its own.
if table.Meta.Status(slot, slotHashes).Status == sealevel.AddressLookupTableStatusTypeDeactivated {
return nil, &TransactionError{ErrorType: TransactionErrorAddressLookupTableNotFound, InstructionError: TxErrAddressLookupTableNotFound}
}

activeLen := len(table.Addresses)
if slot <= table.Meta.LastExtendedSlot {
activeLen = int(table.Meta.LastExtendedSlotStartIndex)
}
if activeLen > len(table.Addresses) {
return nil, &TransactionError{ErrorType: TransactionErrorInvalidAddressLookupTableData, InstructionError: TxErrInvalidAddressLookupTableData}
}
return table.Addresses[:activeLen], nil
}

// checkLookupIndexes fails with InvalidAddressLookupTableIndex if the lookup
// uses an index at or past the table's active length.
func checkLookupIndexes(active solana.PublicKeySlice, lookup *solana.MessageAddressTableLookup) *TransactionError {
for _, indexes := range [2][]uint8{lookup.WritableIndexes, lookup.ReadonlyIndexes} {
for _, idx := range indexes {
if int(idx) >= len(active) {
return &TransactionError{ErrorType: TransactionErrorInvalidAddressLookupTableIndex, InstructionError: TxErrInvalidAddressLookupTableIndex}
}
}
}
return nil
}
74 changes: 58 additions & 16 deletions pkg/replay/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,12 @@ func serializeAllEpochStakes() map[uint64][]byte {
return result
}

func resolveAddrTableLookups(accountsDb blockAccountSource, block *b.Block) error {
// resolveAddrTableLookups resolves the block's lookups in place. Tables are
// read from the parent state at block.Slot and judged with slotHashes, the
// SlotHashes of the block's own bank; reading the parent gives the same
// active addresses the slot's own state would. A lookup that fails to resolve
// makes the transaction unprocessable and the block invalid.
func resolveAddrTableLookups(accountsDb blockAccountSource, block *b.Block, slotHashes sealevel.SysvarSlotHashes) error {
tables := make(map[solana.PublicKey]solana.PublicKeySlice)

for _, tx := range block.Transactions {
Expand All @@ -280,6 +285,9 @@ func resolveAddrTableLookups(accountsDb blockAccountSource, block *b.Block) erro
tables[addrTableKey] = nil
}
}
if len(tables) == 0 {
return nil
}

tablesSlice := make([]solana.PublicKey, 0, len(tables))
for t := range tables {
Expand All @@ -290,26 +298,28 @@ func resolveAddrTableLookups(accountsDb blockAccountSource, block *b.Block) erro
return err
}

for i := range tablesSlice {
if len(accts[i].Data) == 0 {
delete(tables, tablesSlice[i])
tableErrs := make(map[solana.PublicKey]*TransactionError)
for i, key := range tablesSlice {
active, txErr := activeLookupTable(accts[i], block.Slot, slotHashes)
if txErr != nil {
tableErrs[key] = txErr
continue
}
addrLookupTable, err := sealevel.UnmarshalAddressLookupTable(accts[i].Data)
if err != nil {
return err
}
tables[tablesSlice[i]] = addrLookupTable.Addresses
tables[key] = active
}

txResolveLoop:
for _, tx := range block.Transactions {
for idx, tx := range block.Transactions {
if tx.Message.GetVersion() != solana.MessageVersionV0 || tx.Message.AddressTableLookups.NumLookups() == 0 {
continue
}
for _, addrTableKey := range tx.Message.GetAddressTableLookups().GetTableIDs() {
if _, ok := tables[addrTableKey]; !ok {
continue txResolveLoop
for i := range tx.Message.AddressTableLookups {
lookup := &tx.Message.AddressTableLookups[i]
txErr := tableErrs[lookup.AccountKey]
if txErr == nil {
txErr = checkLookupIndexes(tables[lookup.AccountKey], lookup)
}
if txErr != nil {
return fmt.Errorf("resolve address table lookups for transaction %d: %w", idx, txErr.InstructionError)
}
}
err := tx.Message.SetAddressTables(tables)
Expand Down Expand Up @@ -559,6 +569,34 @@ func recordSysvarAccountReadStats(dst *metrics.AccountLoader, src accountsdb.Acc
}
}

// blockSlotHashes returns the SlotHashes of the block's bank before its
// sysvars are installed: the parent's, from the same sources the SlotHashes
// update below uses, with the parent slot's hash at the front, exactly as
// that update and PrepareLeaderSlotSysvars derive them.
func blockSlotHashes(accountsDb blockAccountSource, block *b.Block, parentBankSysvars *sealevel.BankSysvars) (sealevel.SysvarSlotHashes, error) {
var slotHashes sealevel.SysvarSlotHashes
switch {
case parentBankSysvars != nil:
parentSlotHashes, ok := parentBankSysvars.SlotHashes()
if !ok {
return nil, errors.New("SlotHashes sysvar is absent from parent bank snapshot")
}
slotHashes = append(slotHashes, parentSlotHashes...)
case sealevel.SysvarCache.SlotHashes.Sysvar != nil:
slotHashes = append(slotHashes, *sealevel.SysvarCache.SlotHashes.Sysvar...)
default:
acct, err := accountsDb.GetAccount(block.Slot, sealevel.SysvarSlotHashesAddr)
if err != nil {
return nil, fmt.Errorf("load SlotHashes sysvar: %w", err)
}
if err := slotHashes.UnmarshalWithDecoder(bin.NewBinDecoder(acct.Data)); err != nil {
return nil, fmt.Errorf("decode SlotHashes sysvar: %w", err)
}
}
slotHashes.Update(block.Slot, block.ParentSlot, block.ParentBankhash)
return slotHashes, nil
}

func loadBlockAccountsAndUpdateSysvars(
accountsDb blockAccountSource,
block *b.Block,
Expand All @@ -569,7 +607,11 @@ func loadBlockAccountsAndUpdateSysvars(
) (accounts.Accounts, accounts.Accounts, int, *sealevel.BankSysvars, error) {
var bankSysvars *sealevel.BankSysvars
phaseStart := time.Now()
err := resolveAddrTableLookups(accountsDb, block)
slotHashes, err := blockSlotHashes(accountsDb, block, parentBankSysvars)
if err != nil {
return nil, nil, 0, bankSysvars, err
}
err = resolveAddrTableLookups(accountsDb, block, slotHashes)
metrics.GlobalBlockReplay.AccountLoader.AddressTableLookups.AddTimingSince(phaseStart)
if err != nil {
return nil, nil, 0, bankSysvars, err
Expand Down Expand Up @@ -4350,7 +4392,7 @@ func ProcessBlock(
accts, parentAccts, accountMapCapacity, bankSysvars, err := loadBlockAccountsAndUpdateSysvars(exec.blockSrc, block, epochSchedule, alpenglowClock, parentBankSysvars, planner)
loadAcctsRegion.End()
if err != nil {
panic(fmt.Sprintf("unable to load slot accounts and update sysvars: %s", err))
return nil, fmt.Errorf("load slot accounts and update sysvars at slot %d: %w", block.Slot, err)
}
if err := bankSysvars.ValidateForExecution(); err != nil {
return nil, fmt.Errorf("invalid bank sysvar snapshot at slot %d: %w", block.Slot, err)
Expand Down
6 changes: 5 additions & 1 deletion pkg/replay/block_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,11 @@ func (exec *blockExecution) captureAccountLoader() func() {
func (exec *blockExecution) loadTransactionAccounts(view *b.Block) error {
defer exec.captureAccountLoader()()
phaseStart := time.Now()
if err := resolveAddrTableLookups(exec.blockSrc, view); err != nil {
slotHashes, ok := exec.bankSysvars.SlotHashes()
if !ok {
return fmt.Errorf("resolve address table lookups at slot %d: bank has no SlotHashes sysvar", view.Slot)
}
if err := resolveAddrTableLookups(exec.blockSrc, view, slotHashes); err != nil {
return fmt.Errorf("resolve address table lookups at slot %d: %w", view.Slot, err)
}
metrics.GlobalBlockReplay.AccountLoader.AddressTableLookups.AddTimingSince(phaseStart)
Expand Down
7 changes: 6 additions & 1 deletion pkg/replay/block_execution_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ func newGroupExecutionEnv(t *testing.T, txParallelism int, payerLamports uint64)
VoteTimestamps: make(map[solana.PublicKey]sealevel.BlockTimestamp),
Replay: true,
}
slotHashes := sealevel.SysvarSlotHashes{{Slot: block.Slot - 1, Hash: [32]byte{0x61}}}
bankSysvars, err := sealevel.NewBankSysvars(block.Slot,
&accounts.Account{Key: sealevel.SysvarSlotHashesAddr, Lamports: 1, Data: slotHashes.MustMarshal()})
require.NoError(t, err)
source := &memBlockSource{mem: durable}
exec := &blockExecution{
block: block,
Expand All @@ -99,6 +103,7 @@ func newGroupExecutionEnv(t *testing.T, txParallelism int, payerLamports uint64)
slotCtx: slotCtx,
parentAccts: parent,
accts: overlay,
bankSysvars: bankSysvars,
ctx: context.Background(),
setReplayStage: func(string) {},
seenMessages: make(map[[32]byte]int),
Expand Down Expand Up @@ -327,7 +332,7 @@ func TestStreamingGroupRejectsUnresolvedLookupsBeforeExecution(t *testing.T) {
defer env.cleanup()
txs := decodeTransactions(t, [][]byte{signedV0TransferViaTableWire(t, 1)})
err := env.exec.executeTransactionGroup(txs, nil, false)
require.ErrorContains(t, err, "unresolved address tables")
require.ErrorIs(t, err, TxErrAddressLookupTableNotFound)
require.Zero(t, env.exec.slotCtx.TotalComputeUnitsConsumed)
}
}
78 changes: 77 additions & 1 deletion pkg/replay/streaming_lifecycle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,13 @@ func systemTransferData(lamports uint64) []byte {
// to entry 0 of lifecycleTableKey (a writable lookup): static keys are the
// payer and the System program, so the destination is account index 2.
func signedV0TransferViaTableWire(t *testing.T, seq uint64) []byte {
t.Helper()
return signedV0TransferViaTableIndexWire(t, seq, 0)
}

// signedV0TransferViaTableIndexWire is signedV0TransferViaTableWire to entry
// index of the table.
func signedV0TransferViaTableIndexWire(t *testing.T, seq uint64, index uint8) []byte {
t.Helper()
msg := solana.Message{
Header: solana.MessageHeader{NumRequiredSignatures: 1, NumReadonlyUnsignedAccounts: 1},
Expand All @@ -447,7 +454,7 @@ func signedV0TransferViaTableWire(t *testing.T, seq uint64) []byte {
Accounts: []uint16{0, 2},
Data: systemTransferData(1_000 + seq),
}},
AddressTableLookups: solana.MessageAddressTableLookupSlice{{AccountKey: lifecycleTableKey, WritableIndexes: []uint8{0}}},
AddressTableLookups: solana.MessageAddressTableLookupSlice{{AccountKey: lifecycleTableKey, WritableIndexes: []uint8{index}}},
}
_, err := msg.SetVersion(solana.MessageVersionV0)
require.NoError(t, err)
Expand Down Expand Up @@ -591,3 +598,72 @@ func TestStreamingLifecycleDiscardedV0PrefixResolvesAgainstTheNewParent(t *testi
reference := lifecycleWholeBlock(t, newLifecycleEnvWithTable(t, destB), decodeTransactions(t, wires), 2)
requireSameLifecycleOutcome(t, reference, whole)
}

// Every lookup-table case gets the Solana protocol's verdict from ProcessBlock
// (both tx loops) and from a streaming group. Tables are judged against the
// block's SlotHashes: the parent's ([6] here) with the parent slot (7) added.
func TestReplayResolvesLookupTablesByProtocolRules(t *testing.T) {
dest := solana.PublicKey{0xDA}
deactivatedAt := func(slot uint64) func(*accounts.Account) {
return func(a *accounts.Account) { binary.LittleEndian.PutUint64(a.Data[4:12], slot) }
}
cases := []struct {
name string
table func(*accounts.Account)
index uint8
want error // nil: the block is accepted
}{
{"active", func(*accounts.Account) {}, 0, nil},
{"deactivating in the current slot", deactivatedAt(lifecycleSlot), 0, nil},
{"deactivated at the parent slot", deactivatedAt(lifecycleParentSlot), 0, nil},
{"deactivating, still in SlotHashes", deactivatedAt(lifecycleParentSlot - 1), 0, nil},
{"deactivated, gone from SlotHashes", deactivatedAt(2), 0, TxErrAddressLookupTableNotFound},
// The source's view of an absent account.
{"missing", func(a *accounts.Account) { *a = accounts.Account{Key: lifecycleTableKey} }, 0, TxErrAddressLookupTableNotFound},
{"zero lamports with data", func(a *accounts.Account) { a.Lamports = 0 }, 0, TxErrAddressLookupTableNotFound},
{"wrong owner", func(a *accounts.Account) { a.Owner = addresses.SystemProgramAddr }, 0, TxErrInvalidAddressLookupTableOwner},
{"uninitialized data", func(a *accounts.Account) { a.Data = make([]byte, sealevel.AddressLookupTableMetaSize) }, 0, TxErrInvalidAddressLookupTableData},
{"undecodable data", func(a *accounts.Account) { a.Data = []byte{9, 9, 9} }, 0, TxErrInvalidAddressLookupTableData},
{"index past the end", func(*accounts.Account) {}, 5, TxErrInvalidAddressLookupTableIndex},
}
newEnv := func(t *testing.T, tc int) (*lifecycleEnv, []*solana.Transaction) {
env := newLifecycleEnvWithTable(t, dest)
table := lookupTableAccount(t, dest)
cases[tc].table(table)
_ = env.durable.SetAccountWithoutLock(lifecycleTableKey, table)
return env, decodeTransactions(t, [][]byte{signedV0TransferViaTableIndexWire(t, 1, cases[tc].index)})
}
for tc := range cases {
name, want := cases[tc].name, cases[tc].want
for _, txParallelism := range []int{0, 4} {
env, txs := newEnv(t, tc)
tail := &lifecycleTail{durable: env.durable}
block := env.block(txs)
block.MarkTransactionSignaturesVerified()
slotCtx, err := ProcessBlock(env.acctsDb, block, env.epochSchedule, txParallelism, nil, &persistedTracker{}, tail, NewTransactionStatusCache(), false, env.parent)
if want == nil {
require.NoError(t, err, "%s txpar %d", name, txParallelism)
require.Equal(t, uint64(10_000_000+1_001), lifecycleOutcomeOf(t, slotCtx, tail).delta[dest].Lamports, "%s txpar %d", name, txParallelism)
continue
}
require.ErrorIs(t, err, want, "%s txpar %d", name, txParallelism)
require.Nil(t, slotCtx, "%s txpar %d", name, txParallelism)
require.Empty(t, tail.added, "%s txpar %d: a rejected bank commits nothing", name, txParallelism)
}

env, txs := newEnv(t, tc)
exec := newBlockExecution(env.acctsDb, env.block(nil), env.epochSchedule, 2, nil, &persistedTracker{}, &lifecycleTail{durable: env.durable}, NewTransactionStatusCache(), false, env.parent)
require.NoError(t, exec.open())
err := exec.executeTransactionGroup(txs, nil, false)
exec.close()
if want == nil {
require.NoError(t, err, "%s streaming", name)
destAcct, err := exec.slotCtx.GetAccountShared(dest)
require.NoError(t, err)
require.Equal(t, uint64(10_000_000+1_001), destAcct.Lamports, "%s streaming", name)
continue
}
require.ErrorIs(t, err, want, "%s streaming", name)
require.Zero(t, exec.slotCtx.TotalComputeUnitsConsumed, "%s streaming: nothing executed", name)
}
}
Loading