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
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ LDFLAGS := -X github.com/Overclock-Validator/mithril/pkg/version.Version=$(VERSI
-X github.com/Overclock-Validator/mithril/pkg/version.GitBranch=$(GIT_BRANCH) \
-X github.com/Overclock-Validator/mithril/pkg/version.BuildDate=$(BUILD_DATE)

.PHONY: build release clean server-setup disk-setup tune test-conformance-elf
.PHONY: build release clean server-setup disk-setup tune test-conformance-elf test-conformance-vm-programs test-conformance-sbpf

build:
go build -ldflags "$(LDFLAGS)" -o mithril ./cmd/mithril
Expand All @@ -31,3 +31,9 @@ tune:

test-conformance-elf:
go test ./conformance/ -run TestConformance_ElfLoader_Firedancer -v

test-conformance-vm-programs:
go test ./conformance/ -run TestConformance_VMPrograms_Firedancer -v

test-conformance-sbpf:
go test ./conformance/ -run '^(TestConformance_ElfLoader_Firedancer|TestConformance_VMPrograms_Firedancer)$$' -v
132 changes: 94 additions & 38 deletions cmd/mithril/node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,87 @@ var (
lightbringerQuiet bool
)

func snapshotEpochForState(manifest *snapshot.SnapshotManifest) uint64 {
if manifest == nil || manifest.Bank == nil {
return 0
}
if manifest.Bank.EpochSchedule.SlotsPerEpoch != 0 {
epoch := manifest.Bank.EpochSchedule.GetEpoch(manifest.Bank.Slot)
if manifest.Bank.Epoch != epoch {
mlog.Log.Warnf("manifest bank epoch %d differs from manifest epoch schedule epoch %d at slot %d; using schedule-derived epoch",
manifest.Bank.Epoch, epoch, manifest.Bank.Slot)
}
return epoch
}

return manifest.Bank.Epoch
}

func epochScheduleFromState(s *state.MithrilState) *sealevel.SysvarEpochSchedule {
if s != nil && s.ManifestEpochSchedule != nil && s.ManifestEpochSchedule.SlotsPerEpoch != 0 {
return &sealevel.SysvarEpochSchedule{
SlotsPerEpoch: s.ManifestEpochSchedule.SlotsPerEpoch,
LeaderScheduleSlotOffset: s.ManifestEpochSchedule.LeaderScheduleSlotOffset,
Warmup: s.ManifestEpochSchedule.Warmup,
FirstNormalEpoch: s.ManifestEpochSchedule.FirstNormalEpoch,
FirstNormalSlot: s.ManifestEpochSchedule.FirstNormalSlot,
}
}
return sealevel.SysvarCache.EpochSchedule.Sysvar
}

func epochForStateSlot(s *state.MithrilState, slot uint64) uint64 {
if epochSchedule := epochScheduleFromState(s); epochSchedule != nil {
return epochSchedule.GetEpoch(slot)
}
return 0
}

func manifestEpochScheduleSeedMatches(s *state.MithrilState, manifest *snapshot.SnapshotManifest) bool {
if s == nil || s.ManifestEpochSchedule == nil || manifest == nil || manifest.Bank == nil {
return false
}
m := manifest.Bank.EpochSchedule
return s.ManifestEpochSchedule.SlotsPerEpoch == m.SlotsPerEpoch &&
s.ManifestEpochSchedule.LeaderScheduleSlotOffset == m.LeaderScheduleSlotOffset &&
s.ManifestEpochSchedule.Warmup == m.Warmup &&
s.ManifestEpochSchedule.FirstNormalEpoch == m.FirstNormalEpoch &&
s.ManifestEpochSchedule.FirstNormalSlot == m.FirstNormalSlot
}

func refreshManifestSeedFromManifest(accountsPath string, s *state.MithrilState, manifest *snapshot.SnapshotManifest) {
if s == nil || manifest == nil || manifest.Bank == nil {
return
}

snapshotEpoch := snapshotEpochForState(manifest)
if manifestEpochScheduleSeedMatches(s, manifest) && s.SnapshotEpoch == snapshotEpoch {
return
}

oldSnapshotEpoch := s.SnapshotEpoch
if oldSnapshotEpoch != 0 && oldSnapshotEpoch != snapshotEpoch && s.LastSlot > s.SnapshotSlot {
reason := fmt.Sprintf("snapshot epoch frame changed from %d to %d after replay had already persisted slot %d; rebuild AccountsDB from snapshot",
oldSnapshotEpoch, snapshotEpoch, s.LastSlot)
if err := s.MarkCorrupted(accountsPath, reason); err != nil {
mlog.Log.Errorf("failed to mark state as corrupted: %v", err)
}
klog.Fatalf(reason)
}

s.SnapshotEpoch = snapshotEpoch
snapshot.PopulateManifestSeed(s, manifest)
if s.LastSlot > 0 {
s.LastEpoch = epochForStateSlot(s, s.LastSlot)
}
if err := s.Save(accountsPath); err != nil {
mlog.Log.Errorf("failed to refresh manifest seed data in state file: %v", err)
return
}
mlog.Log.Warnf("refreshed manifest-derived state seed data from snapshot manifest (snapshot_epoch %d -> %d)",
oldSnapshotEpoch, snapshotEpoch)
}

func init() {
// [bootstrap] section flags
Run.Flags().StringVar(&bootstrapMode, "bootstrap", "auto", "Bootstrap mode: 'auto' (use AccountsDB if exists, else snapshot), 'accountsdb' (require existing), 'snapshot' (rebuild from snapshot), 'new-snapshot' (always download fresh)")
Expand Down Expand Up @@ -894,10 +975,7 @@ func runLive(c *cobra.Command, args []string) {
}

// Write state file
var snapshotEpoch uint64
if sealevel.SysvarCache.EpochSchedule.Sysvar != nil {
snapshotEpoch = sealevel.SysvarCache.EpochSchedule.Sysvar.GetEpoch(manifest.Bank.Slot)
}
snapshotEpoch := snapshotEpochForState(manifest)
mithrilState = state.NewReadyState(manifest.Bank.Slot, snapshotEpoch, "", "", 0, 0)
// Populate manifest seed data so replay doesn't need manifest at runtime
snapshot.PopulateManifestSeed(mithrilState, manifest)
Expand Down Expand Up @@ -926,6 +1004,7 @@ func runLive(c *cobra.Command, args []string) {
if err != nil {
klog.Fatalf("failed to load manifest: %v", err)
}
refreshManifestSeedFromManifest(accountsPath, mithrilState, manifest)
// Run integrity check if we have a state file (warn only, don't fail - user chose force mode)
if hasValidState {
if err := mithrilState.ValidateAgainstBankhashDB(accountsDb); err != nil {
Expand Down Expand Up @@ -964,10 +1043,7 @@ func runLive(c *cobra.Command, args []string) {
klog.Fatalf("failed to build AccountsDB from snapshot: %v", err)
}
// Write state file to mark build as complete
var snapshotEpoch uint64
if sealevel.SysvarCache.EpochSchedule.Sysvar != nil {
snapshotEpoch = sealevel.SysvarCache.EpochSchedule.Sysvar.GetEpoch(manifest.Bank.Slot)
}
snapshotEpoch := snapshotEpochForState(manifest)
mithrilState = state.NewReadyState(manifest.Bank.Slot, snapshotEpoch, "", "", 0, 0)
// Populate manifest seed data so replay doesn't need manifest at runtime
snapshot.PopulateManifestSeed(mithrilState, manifest)
Expand Down Expand Up @@ -1022,10 +1098,7 @@ func runLive(c *cobra.Command, args []string) {
klog.Fatalf("failed to build AccountsDB from snapshot: %v", err)
}
// Write state file to mark build as complete
var snapshotEpoch uint64
if sealevel.SysvarCache.EpochSchedule.Sysvar != nil {
snapshotEpoch = sealevel.SysvarCache.EpochSchedule.Sysvar.GetEpoch(manifest.Bank.Slot)
}
snapshotEpoch := snapshotEpochForState(manifest)
mithrilState = state.NewReadyState(manifest.Bank.Slot, snapshotEpoch, "", "", 0, 0)
// Populate manifest seed data so replay doesn't need manifest at runtime
snapshot.PopulateManifestSeed(mithrilState, manifest)
Expand Down Expand Up @@ -1094,10 +1167,7 @@ func runLive(c *cobra.Command, args []string) {
if err != nil {
klog.Fatalf("failed to build AccountsDB from snapshot: %v", err)
}
var snapshotEpoch uint64
if sealevel.SysvarCache.EpochSchedule.Sysvar != nil {
snapshotEpoch = sealevel.SysvarCache.EpochSchedule.Sysvar.GetEpoch(manifest.Bank.Slot)
}
snapshotEpoch := snapshotEpochForState(manifest)
mithrilState = state.NewReadyState(manifest.Bank.Slot, snapshotEpoch, "", "", 0, 0)
// Populate manifest seed data so replay doesn't need manifest at runtime
snapshot.PopulateManifestSeed(mithrilState, manifest)
Expand All @@ -1122,6 +1192,7 @@ func runLive(c *cobra.Command, args []string) {
if err != nil {
klog.Fatalf("failed to load manifest: %v", err)
}
refreshManifestSeedFromManifest(accountsPath, mithrilState, manifest)

// Validate state file matches AccountsDB (detect Ctrl+Z / kill -9 corruption)
if err := mithrilState.ValidateAgainstBankhashDB(accountsDb); err != nil {
Expand Down Expand Up @@ -1190,10 +1261,7 @@ func runLive(c *cobra.Command, args []string) {
klog.Fatalf("failed to build AccountsDB from snapshot: %v", err)
}
// Write state file to mark build as complete
var snapshotEpoch uint64
if sealevel.SysvarCache.EpochSchedule.Sysvar != nil {
snapshotEpoch = sealevel.SysvarCache.EpochSchedule.Sysvar.GetEpoch(manifest.Bank.Slot)
}
snapshotEpoch := snapshotEpochForState(manifest)
mithrilState = state.NewReadyStateWithOpts(state.NewReadyStateOpts{
SnapshotSlot: manifest.Bank.Slot,
SnapshotEpoch: snapshotEpoch,
Expand Down Expand Up @@ -1312,10 +1380,7 @@ postBootstrap:

if mithrilState == nil {
// Initialize state for this session
var snapshotEpoch uint64
if sealevel.SysvarCache.EpochSchedule.Sysvar != nil {
snapshotEpoch = sealevel.SysvarCache.EpochSchedule.Sysvar.GetEpoch(manifest.Bank.Slot)
}
snapshotEpoch := snapshotEpochForState(manifest)
mithrilState = state.NewReadyState(manifest.Bank.Slot, snapshotEpoch, "", "", 0, 0)
// Populate manifest seed data so replay doesn't need manifest at runtime
snapshot.PopulateManifestSeed(mithrilState, manifest)
Expand Down Expand Up @@ -1363,7 +1428,7 @@ postBootstrap:
if rpcPort < 0 || rpcPort > 65535 {
klog.Fatalf("invalid port: %d", rpcPort)
} else if rpcPort != 0 {
rpcServer = rpcserver.NewRpcServer(accountsDb, uint16(rpcPort))
rpcServer = rpcserver.NewRpcServer(accountsDb, uint16(rpcPort), epochScheduleFromState(mithrilState))
rpcServer.Start()
mlog.Log.Infof("Started RPC server on port %d", rpcPort)
}
Expand Down Expand Up @@ -1433,10 +1498,7 @@ postBootstrap:
var shutdownCtx *state.ShutdownContext
if result.LastAcctsLtHash != nil {
// Calculate epoch for the last persisted slot
var lastEpoch uint64
if sealevel.SysvarCache.EpochSchedule.Sysvar != nil {
lastEpoch = sealevel.SysvarCache.EpochSchedule.Sysvar.GetEpoch(result.LastPersistedSlot)
}
lastEpoch := epochForStateSlot(mithrilState, result.LastPersistedSlot)
// Determine shutdown reason
shutdownReason := state.ShutdownReasonCompleted
if result.WasCancelled {
Expand Down Expand Up @@ -1503,11 +1565,8 @@ postBootstrap:
// Print shutdown summary if cancelled or error
if (result.WasCancelled || result.Error != nil) && result.LastPersistedSlot > 0 {
// Calculate epoch from slot using epoch schedule
var epoch, snapshotEpoch uint64
if sealevel.SysvarCache.EpochSchedule.Sysvar != nil {
epoch = sealevel.SysvarCache.EpochSchedule.Sysvar.GetEpoch(result.LastPersistedSlot)
snapshotEpoch = sealevel.SysvarCache.EpochSchedule.Sysvar.GetEpoch(snapshotBaseSlot)
}
epoch := epochForStateSlot(mithrilState, result.LastPersistedSlot)
snapshotEpoch := epochForStateSlot(mithrilState, snapshotBaseSlot)
progress.PrintShutdownSummary(progress.ShutdownInfo{
LastSlot: result.LastPersistedSlot,
LastBankhash: result.LastPersistedBankhash,
Expand Down Expand Up @@ -2309,10 +2368,7 @@ func runReplayWithRecovery(
}

// Calculate epoch for the last persisted slot
var lastEpoch uint64
if sealevel.SysvarCache.EpochSchedule.Sysvar != nil {
lastEpoch = sealevel.SysvarCache.EpochSchedule.Sysvar.GetEpoch(r.LastPersistedSlot)
}
lastEpoch := epochForStateSlot(mithrilState, r.LastPersistedSlot)

// Build shutdown context
var shutdownCtx *state.ShutdownContext
Expand Down
147 changes: 147 additions & 0 deletions conformance/debug_fixture_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package conformance

import (
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"testing"

"github.com/Overclock-Validator/mithril/pkg/sbpf"
"github.com/Overclock-Validator/mithril/pkg/sbpf/loader"
sealevelPkg "github.com/Overclock-Validator/mithril/pkg/sealevel"
"github.com/gagliardetto/solana-go"
)

func TestDebugDumpVMProgramFixture(t *testing.T) {
filter := os.Getenv("MITHRIL_CONFORMANCE_DUMP_FIXTURE")
if filter == "" {
t.Skip("set MITHRIL_CONFORMANCE_DUMP_FIXTURE")
}

basePath := "test-vectors/instr/fixtures/vm-programs"
entries, err := os.ReadDir(basePath)
if err != nil {
t.Skipf("test-vectors not available: %v", err)
}

var fixtures []string
for _, entry := range entries {
if strings.HasSuffix(entry.Name(), ".fix") && strings.Contains(entry.Name(), filter) {
fixtures = append(fixtures, filepath.Join(basePath, entry.Name()))
}
}
sort.Strings(fixtures)
if len(fixtures) == 0 {
t.Fatalf("no fixture matching %q", filter)
}

data, err := os.ReadFile(fixtures[0])
if err != nil {
t.Fatal(err)
}
fixture, err := unmarshalFiredancerInstrFixture(data)
if err != nil {
t.Fatal(err)
}

programID := solana.PublicKeyFromBytes(fixture.GetInput().GetProgramId())
t.Logf("fixture=%s program_id=%s input_cu=%d output_cu=%d output_result=%d custom=%d data=%x", filepath.Base(fixtures[0]), programID, fixture.GetInput().GetCuAvail(), fixture.GetOutput().GetCuAvail(), fixture.GetOutput().GetResult(), fixture.GetOutput().GetCustomErr(), fixture.GetInput().GetData())
t.Logf("%s", fixtureProgramSummary(fixture))
for i, acct := range fixture.GetInput().GetAccounts() {
key := solana.PublicKeyFromBytes(acct.GetAddress())
owner := solana.PublicKeyFromBytes(acct.GetOwner())
prefixLen := min(len(acct.GetData()), 8)
prefix := acct.GetData()[:prefixLen]
t.Logf("acct[%d] key=%s owner=%s exec=%v lamports=%d data_len=%d data_prefix=%x", i, key, owner, acct.GetExecutable(), acct.GetLamports(), len(acct.GetData()), prefix)
}
for i, acct := range fixture.GetInput().GetInstrAccounts() {
t.Logf("instr_acct[%d] index=%d writable=%v signer=%v", i, acct.GetIndex(), acct.GetIsWritable(), acct.GetIsSigner())
}

execCtx, instrAccts, programIndices, err := newVMProgramExecCtxAndInstrAccts(fixture)
if err != nil {
t.Fatal(err)
}
t.Logf("features=%v", execCtx.Features.AllEnabled())

if idxStr := os.Getenv("MITHRIL_CONFORMANCE_DUMP_PROGRAM_INDEX"); idxStr != "" {
for i, acct := range fixture.GetInput().GetAccounts() {
if idxStr == strconv.Itoa(i) {
programID = solana.PublicKeyFromBytes(acct.GetAddress())
break
}
}
}
var programBytes []byte
for _, acct := range fixture.GetInput().GetAccounts() {
if solana.PublicKeyFromBytes(acct.GetAddress()) == programID {
programBytes = acct.GetData()
break
}
}
if len(programBytes) == 0 {
t.Fatalf("program account %s has no bytes", programID)
}

var program *sbpf.Program
if os.Getenv("MITHRIL_CONFORMANCE_DUMP_NO_DISASM") == "" {
syscalls := func(u uint32) (sbpf.Syscall, bool) {
syscall, ok := sealevelPkg.Syscalls(&execCtx.Features, false, u)
if !ok {
return nil, false
}
return debugSyscall{t: t, hash: u, inner: syscall}, true
}
l, err := loader.NewLoaderWithSyscalls(programBytes, syscalls, false, &execCtx.Features)
if err != nil {
t.Fatalf("loader: %v", err)
}
program, err = l.Load()
if err != nil {
t.Fatalf("load: %v", err)
}

t.Logf("sbpf_version=%v entry=%d text_slots=%d funcs=%d", program.SbpfVersion, program.Entrypoint, len(program.Text), len(program.Funcs))
t.Logf("verify_err=%v", program.Verify())
}
execCtx.RecordInnerInstructions = true
execCtx.SetCurrentTopLevelInstr(0)
runErr := execCtx.ProcessInstruction(fixture.GetInput().GetData(), instrAccts, programIndices)
t.Logf("run_err=%v translated_result=%d remaining_cu=%d", runErr, instrResultFromErr(runErr), execCtx.ComputeMeter.Remaining())
if recorder, ok := execCtx.Log.(*sealevelPkg.LogRecorder); ok {
for i, log := range recorder.Logs {
t.Logf("log[%d]=%s", i, log)
}
}
for i, inner := range execCtx.InnerInstrs {
t.Logf("inner[%d] stack=%d program_index=%d accounts=%v data=%x", i, inner.StackHeight, inner.ProgramIdIndex, inner.Accounts, inner.Data)
}
if program != nil {
limit := len(program.Text)
for pc := 0; pc < limit; pc++ {
slot := program.Text[pc]
t.Logf("%03d raw=%016x op=%02x dst=%d src=%d off=%d imm=%d uimm=%d", pc, uint64(slot), slot.Op(), slot.Dst(), slot.Src(), slot.Off(), slot.Imm(), slot.Uimm())
}
}
}

type debugSyscall struct {
t *testing.T
hash uint32
inner sbpf.Syscall
}

func (d debugSyscall) Invoke(vm sbpf.VM, r1, r2, r3, r4, r5 uint64) (uint64, error) {
before := vm.ComputeMeter().Remaining()
r0, err := d.inner.Invoke(vm, r1, r2, r3, r4, r5)
after := vm.ComputeMeter().Remaining()
d.t.Logf("syscall hash=0x%08x args=[0x%x 0x%x 0x%x 0x%x 0x%x] before_cu=%d after_cu=%d r0=%d err=%v", d.hash, r1, r2, r3, r4, r5, before, after, r0, err)
return r0, err
}

func (d debugSyscall) String() string {
return fmt.Sprintf("debugSyscall(0x%08x)", d.hash)
}
Loading
Loading