diff --git a/Makefile b/Makefile index 2a579c80..164155b7 100644 --- a/Makefile +++ b/Makefile @@ -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 test-conformance-vm-programs test-conformance-sbpf +.PHONY: build release clean server-setup disk-setup tune conformance-vectors test-conformance-elf test-conformance-vm-programs test-conformance-sbpf test-conformance-precompiles build: go build -ldflags "$(LDFLAGS)" -o mithril ./cmd/mithril @@ -29,6 +29,24 @@ disk-setup: tune: ./scripts/performance-tune.sh $(ARGS) +# Firedancer's fixture corpus is ~7 GB and gitignored, so it is fetched rather +# than vendored. The revision is pinned: the corpus moves, and both its schema +# and its per-suite result counts move with it, so tracking main would make a +# passing run unreproducible and a regression indistinguishable from an upstream +# edit. Bump this deliberately and re-record the counts when you do. +CONFORMANCE_VECTORS_REV ?= a87fc430 +conformance-vectors: + @if [ ! -d conformance/test-vectors/.git ]; then \ + git clone --filter=blob:none --no-checkout \ + https://github.com/firedancer-io/test-vectors.git conformance/test-vectors; \ + fi + @git -C conformance/test-vectors fetch --depth 1 origin $(CONFORMANCE_VECTORS_REV) + @git -C conformance/test-vectors checkout --force --detach $(CONFORMANCE_VECTORS_REV) + @echo "conformance corpus pinned at $(CONFORMANCE_VECTORS_REV)" + +test-conformance-precompiles: + go test ./conformance/ -run 'TestConformance_Precompile_' -timeout 90m -v + test-conformance-elf: go test ./conformance/ -run TestConformance_ElfLoader_Firedancer -v diff --git a/cmd/mithril/configcmd/configcmd.go b/cmd/mithril/configcmd/configcmd.go index 7fa51f46..62badcaf 100644 --- a/cmd/mithril/configcmd/configcmd.go +++ b/cmd/mithril/configcmd/configcmd.go @@ -260,6 +260,7 @@ max_rps = 8 # Verifier's own RPC budget (never shares the block-fe # ── Replay tuning ──────────────────────────────────────────────────────── [tuning] txpar = 24 # Validator auto-defaults to 2x CPU cores only when unset; explicit 0 = sequential +sigverify_backend = "auto" # auto|r51|generic|stdlib; stdlib uses Go's crypto/ed25519 impl after strict checks. # ── Mithril's RPC server ───────────────────────────────────────────────── [rpc] diff --git a/cmd/mithril/node/node.go b/cmd/mithril/node/node.go index 43b2faaf..2c15a588 100644 --- a/cmd/mithril/node/node.go +++ b/cmd/mithril/node/node.go @@ -44,6 +44,7 @@ import ( "github.com/Overclock-Validator/mithril/pkg/rpcserver" "github.com/Overclock-Validator/mithril/pkg/sbpf" "github.com/Overclock-Validator/mithril/pkg/sealevel" + "github.com/Overclock-Validator/mithril/pkg/sigverify" "github.com/Overclock-Validator/mithril/pkg/snapshot" "github.com/Overclock-Validator/mithril/pkg/snapshotdl" "github.com/Overclock-Validator/mithril/pkg/state" @@ -124,6 +125,10 @@ var ( debugDumpEpochVotingRewardDiff bool cpuprofPath string + // resolvedSigverifyBackend is what the backend selector actually chose. It + // differs from the configured value under "auto", which is exactly when an + // operator needs to be told, so it is reported at startup. + resolvedSigverifyBackend string paramArenaSizeMB uint64 borrowedAccountArenaSize uint64 @@ -417,6 +422,8 @@ func init() { Run.Flags().IntVar(&snapshot.SnapshotIndexEntryCommitterWorkers, "snapshot-index-committer-workers", snapshot.DefaultSnapshotIndexEntryCommitterWorkers, "Snapshot bootstrap account-index shard enqueue workers") Run.Flags().IntVar(&snapshot.SnapshotIndexShards, "snapshot-index-shards", snapshot.DefaultSnapshotIndexShards, "Snapshot bootstrap account-index shard count") Run.Flags().StringVar(&snapshot.SnapshotIndexTempDir, "snapshot-index-temp-dir", "", "Optional directory for snapshot index shard logs/SST staging") + Run.Flags().StringVar(&sigverify.Cfg.Backend, "sigverify-backend", sigverify.Defaults().Backend, + "ed25519 verification backend: auto|r51|generic|stdlib") Run.Flags().BoolVar(&sbpf.UsePool, "use-pool", true, "Disable to allocate fresh slices") Run.Flags().IntVar(&accountsdb.StoreAccountsWorkers, "store-accounts-workers", 128, "Number of workers to write account updates") Run.Flags().IntVar(&accountsdb.ProgramCacheMaxMB, "program-cache-max-mb", accountsdb.DefaultProgramCacheMaxMB, "Maximum approximate SBPF program cache size in MiB") @@ -974,6 +981,16 @@ func initConfigAndBindFlags(cmd *cobra.Command) error { if snapshot.SnapshotIndexShards <= 0 || snapshot.SnapshotIndexShards > 1000 { return fmt.Errorf("tuning.snapshot_index_shards must be between 1 and 1000") } + // Resolve and install the signature-verification backend here rather than + // later: narya pins its backend on first use, and selecting it explicitly + // doubles as a startup health check, so a machine that cannot run the + // requested backend fails now instead of at the first block. + sigverify.Cfg.Backend = getString("sigverify-backend", "tuning.sigverify_backend") + resolved, err := sigverify.Configure(sigverify.Cfg) + if err != nil { + return fmt.Errorf("tuning.sigverify_backend: %w", err) + } + resolvedSigverifyBackend = resolved sbpf.UsePool = getBool("use-pool", "tuning.use_pool") accountsdb.StoreAccountsWorkers = getInt("store-accounts-workers", "tuning.store_accounts_workers") accountsdb.ProgramCacheMaxMB = getInt("program-cache-max-mb", "tuning.program_cache_max_mb") @@ -2632,6 +2649,8 @@ postBootstrap: }() } + startSigverifyReporter(ctx) + turbineAlpenglowAddr := "" if alpenglowMode { turbineAlpenglowAddr = alpenglowAddrForGossip(alpenglowObserverBindAddr) @@ -2931,6 +2950,18 @@ func printStartupInfo(commandName string) { fmt.Printf(" Bootstrap: %s%s%s\n", green, bootstrapMode, reset) } + if resolvedSigverifyBackend != "" { + sigverifyDesc := "AVX-512 accelerated" + switch resolvedSigverifyBackend { + case sigverify.BackendGeneric: + sigverifyDesc = "portable; no AVX512-IFMA on this CPU" + case sigverify.BackendStdlib: + sigverifyDesc = "Go's crypto/ed25519, but also uses Narya's strictness checks" + } + fmt.Printf(" Sigverify: %s%s%s %s(%s)%s\n", + green, resolvedSigverifyBackend, reset, dim, sigverifyDesc, reset) + } + // Load state file for detailed info (only show for modes that use existing AccountsDB) // In snapshot/new-snapshot modes, we're rebuilding so state info is not relevant willUseExistingAccountsDB := bootstrapMode == "auto" || bootstrapMode == "accountsdb" diff --git a/cmd/mithril/node/sigverify_reporter.go b/cmd/mithril/node/sigverify_reporter.go new file mode 100644 index 00000000..d374d93b --- /dev/null +++ b/cmd/mithril/node/sigverify_reporter.go @@ -0,0 +1,104 @@ +package node + +import ( + "context" + "time" + + "github.com/Overclock-Validator/mithril/pkg/mlog" + "github.com/Overclock-Validator/mithril/pkg/sigverify" +) + +// sigverifyReportInterval is deliberately coarse. Batch width is a property of +// the workload's shape, not of any one block, so a short interval would report +// noise; the question this answers is "are we filling groups over a stretch of +// replay", and that changes on the scale of minutes. +const sigverifyReportInterval = 5 * time.Minute + +// startSigverifyReporter periodically records verification behaviour to the +// per-run log directory. +// +// It writes only to /sigverify.log via NamedFilef, never to the +// terminal. Operator-facing stderr already carries replay progress, and batch +// width is diagnostic rather than something to watch live; adding it to the +// console would cost attention from the output that matters and change the +// startup display that people read at a glance. +// +// Three things go in, and each answers a question the others cannot: +// +// - the resolved backend, because with backend=auto the name is the only way +// to learn whether this machine got the accelerated path; +// - InternalFaultFallbacks, which should be zero forever -- a nonzero value +// is a bug in the accelerated backend rather than an input-dependent +// condition, so it is worth alerting on rather than merely recording; +// - the batch-width distribution, because a signature total cannot show +// whether the drain policy is filling groups, and filling them is worth a +// ~3.7x factor per signature. +func startSigverifyReporter(ctx context.Context) { + go func() { + ticker := time.NewTicker(sigverifyReportInterval) + defer ticker.Stop() + + // Baseline at startup so the first interval has something to difference + // against, and so the resolved backend is recorded even on a node that + // exits before the first tick. + previous := sigverify.Stats() + mlog.NamedFilef("sigverify", "startup: %s", previous) + + for { + select { + case <-ctx.Done(): + // A final snapshot: a short run would otherwise leave no record + // of what it did, and a run that ends because of a verification + // problem is exactly when this is worth having. + mlog.NamedFilef("sigverify", "shutdown: %s", sigverify.Stats()) + return + + case <-ticker.C: + current := sigverify.Stats() + + // Counters are monotonic for the process lifetime, so an + // interval line needs the delta. Reporting only cumulative + // values would let a long-healthy run hide a recent collapse in + // batch width. + interval := sigverify.Snapshot{ + Backend: current.Backend, + InternalFaultFallbacks: current.InternalFaultFallbacks, + Batches: current.Batches - previous.Batches, + Signatures: current.Signatures - previous.Signatures, + EmptyBatches: current.EmptyBatches - previous.EmptyBatches, + Widths: diffWidths(previous.Widths, current.Widths), + } + mlog.NamedFilef("sigverify", "interval: %s", interval) + + if current.InternalFaultFallbacks > previous.InternalFaultFallbacks { + // This one does reach the operator log, because it means the + // accelerated backend produced a result it could not trust + // and recomputed on the portable path. It should never + // happen; if it does, silence is the wrong default. + mlog.Log.Warnf("sigverify: accelerated backend fell back on an internal fault %d time(s) total; this is a backend bug, not an input condition", + current.InternalFaultFallbacks) + } + + previous = current + } + } + }() +} + +// diffWidths subtracts an earlier histogram from a later one. Buckets are +// matched by upper bound rather than by position, because Stats omits empty +// buckets and the two snapshots need not have the same shape. +func diffWidths(before, after []sigverify.WidthBucket) []sigverify.WidthBucket { + earlier := make(map[int]uint64, len(before)) + for _, b := range before { + earlier[b.Upper] = b.Batches + } + + delta := make([]sigverify.WidthBucket, 0, len(after)) + for _, b := range after { + if n := b.Batches - earlier[b.Upper]; n > 0 { + delta = append(delta, sigverify.WidthBucket{Upper: b.Upper, Batches: n}) + } + } + return delta +} diff --git a/cmd/mithril/node/sigverify_reporter_test.go b/cmd/mithril/node/sigverify_reporter_test.go new file mode 100644 index 00000000..f55c1501 --- /dev/null +++ b/cmd/mithril/node/sigverify_reporter_test.go @@ -0,0 +1,45 @@ +package node + +import ( + "testing" + + "github.com/Overclock-Validator/mithril/pkg/sigverify" + "github.com/stretchr/testify/assert" +) + +// Interval lines are differences between monotonic snapshots. Getting this +// wrong is not loud: the reporter keeps writing plausible-looking lines, and a +// collapse in batch width during one interval stays hidden behind a healthy +// cumulative total. +func TestDiffWidthsSubtractsByBucketNotPosition(t *testing.T) { + // Stats omits empty buckets, so the two snapshots need not share a shape. + // Matching by position would misattribute counts across bucket boundaries. + before := []sigverify.WidthBucket{ + {Upper: 1, Batches: 10}, + {Upper: 8, Batches: 4}, + } + after := []sigverify.WidthBucket{ + {Upper: 1, Batches: 12}, + {Upper: 4, Batches: 7}, // a bucket absent from `before` entirely + {Upper: 8, Batches: 9}, + } + + assert.ElementsMatch(t, []sigverify.WidthBucket{ + {Upper: 1, Batches: 2}, + {Upper: 4, Batches: 7}, + {Upper: 8, Batches: 5}, + }, diffWidths(before, after)) +} + +// A bucket that saw no traffic during the interval must be dropped rather than +// reported as zero, or every line carries every bucket forever and the shape of +// the distribution stops being readable at a glance. +func TestDiffWidthsOmitsUnchangedBuckets(t *testing.T) { + same := []sigverify.WidthBucket{{Upper: 8, Batches: 5}} + assert.Empty(t, diffWidths(same, same)) +} + +func TestDiffWidthsHandlesAnEmptyBaseline(t *testing.T) { + after := []sigverify.WidthBucket{{Upper: 2, Batches: 3}} + assert.Equal(t, after, diffWidths(nil, after)) +} diff --git a/config.example.toml b/config.example.toml index c8119353..72eb0af7 100644 --- a/config.example.toml +++ b/config.example.toml @@ -519,6 +519,14 @@ name = "mithril" # Number of borrowed accounts to preallocate in arena (0 to disable) borrowed_account_arena_size = 1024 + # ed25519 signature verification backend. + # auto - use the AVX-512 accelerated backend when the CPU has + # AVX512-IFMA (Zen 4/5, Ice Lake and newer), else portable + # r51 - force the accelerated backend; startup fails without AVX512-IFMA + # generic - force the portable pure-Go backend + # stdlib - use Go’s crypto/ed25519 implementation after the mandatory strict rejection checks. + sigverify_backend = "auto" + # Enable/disable pool allocator for slices use_pool = true diff --git a/conformance/bpf_loader_program_test.go b/conformance/bpf_loader_program_test.go index 8190f13f..4fed88b8 100644 --- a/conformance/bpf_loader_program_test.go +++ b/conformance/bpf_loader_program_test.go @@ -38,9 +38,7 @@ func bpfLoaderTestAccountStateChangesMatch(t *testing.T, execCtx *sealevel.Execu if fixtureModifiedAcct.Executable != mithrilModifiedAcct.Executable { return false } - if fixtureModifiedAcct.RentEpoch != mithrilModifiedAcct.RentEpoch { - return false - } + // AcctState dropped rent_epoch in protosol v5.4.0. if solana.PublicKeyFromBytes(fixtureModifiedAcct.Owner[:]) != solana.PublicKeyFromBytes(mithrilModifiedAcct.Owner[:]) { return false } diff --git a/conformance/context.pb.go b/conformance/context.pb.go index 49173228..cc86796d 100644 --- a/conformance/context.pb.go +++ b/conformance/context.pb.go @@ -71,22 +71,24 @@ func (x *FeatureSet) GetFeatures() []uint64 { return nil } -// A seed address. This is not a PDA. -type SeedAddress struct { +// The complete state of an account excluding its public key. +type AcctState struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // The seed address base. (32 bytes) - Base []byte `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` - // The seed path (<= 32 bytes) - Seed []byte `protobuf:"bytes,2,opt,name=seed,proto3" json:"seed,omitempty"` - // The seed address owner. (32 bytes) - Owner []byte `protobuf:"bytes,3,opt,name=owner,proto3" json:"owner,omitempty"` + // The account address. (32 bytes) + Address []byte `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + Lamports uint64 `protobuf:"varint,2,opt,name=lamports,proto3" json:"lamports,omitempty"` + // Account data is limited to 10 MiB on Solana mainnet as of 2024-Feb. + Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` + Executable bool `protobuf:"varint,4,opt,name=executable,proto3" json:"executable,omitempty"` + // Address of the program that owns this account. (32 bytes) + Owner []byte `protobuf:"bytes,6,opt,name=owner,proto3" json:"owner,omitempty"` } -func (x *SeedAddress) Reset() { - *x = SeedAddress{} +func (x *AcctState) Reset() { + *x = AcctState{} if protoimpl.UnsafeEnabled { mi := &file_context_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -94,13 +96,13 @@ func (x *SeedAddress) Reset() { } } -func (x *SeedAddress) String() string { +func (x *AcctState) String() string { return protoimpl.X.MessageStringOf(x) } -func (*SeedAddress) ProtoMessage() {} +func (*AcctState) ProtoMessage() {} -func (x *SeedAddress) ProtoReflect() protoreflect.Message { +func (x *AcctState) ProtoReflect() protoreflect.Message { mi := &file_context_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -112,58 +114,61 @@ func (x *SeedAddress) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use SeedAddress.ProtoReflect.Descriptor instead. -func (*SeedAddress) Descriptor() ([]byte, []int) { +// Deprecated: Use AcctState.ProtoReflect.Descriptor instead. +func (*AcctState) Descriptor() ([]byte, []int) { return file_context_proto_rawDescGZIP(), []int{1} } -func (x *SeedAddress) GetBase() []byte { +func (x *AcctState) GetAddress() []byte { if x != nil { - return x.Base + return x.Address } return nil } -func (x *SeedAddress) GetSeed() []byte { +func (x *AcctState) GetLamports() uint64 { + if x != nil { + return x.Lamports + } + return 0 +} + +func (x *AcctState) GetData() []byte { if x != nil { - return x.Seed + return x.Data } return nil } -func (x *SeedAddress) GetOwner() []byte { +func (x *AcctState) GetExecutable() bool { + if x != nil { + return x.Executable + } + return false +} + +func (x *AcctState) GetOwner() []byte { if x != nil { return x.Owner } return nil } -// The complete state of an account excluding its public key. -type AcctState struct { +// Fee rate governor parameters +type FeeRateGovernor struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // The account address. (32 bytes) - Address []byte `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` - Lamports uint64 `protobuf:"varint,2,opt,name=lamports,proto3" json:"lamports,omitempty"` - // Account data is limited to 10 MiB on Solana mainnet as of 2024-Feb. - Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` - Executable bool `protobuf:"varint,4,opt,name=executable,proto3" json:"executable,omitempty"` - // The rent epoch is deprecated on Solana mainnet as of 2024-Feb. - // If ommitted, implies a value of UINT64_MAX. - RentEpoch uint64 `protobuf:"varint,5,opt,name=rent_epoch,json=rentEpoch,proto3" json:"rent_epoch,omitempty"` - // Address of the program that owns this account. (32 bytes) - Owner []byte `protobuf:"bytes,6,opt,name=owner,proto3" json:"owner,omitempty"` - // The account address, but derived as a seed address. Overrides - // `address` if present. - // TODO: This is a solfuzz specific extension and is not compliant - // with the org.solana.sealevel.v1 API. - SeedAddr *SeedAddress `protobuf:"bytes,7,opt,name=seed_addr,json=seedAddr,proto3" json:"seed_addr,omitempty"` + TargetLamportsPerSignature uint64 `protobuf:"varint,1,opt,name=target_lamports_per_signature,json=targetLamportsPerSignature,proto3" json:"target_lamports_per_signature,omitempty"` + TargetSignaturesPerSlot uint64 `protobuf:"varint,2,opt,name=target_signatures_per_slot,json=targetSignaturesPerSlot,proto3" json:"target_signatures_per_slot,omitempty"` + MinLamportsPerSignature uint64 `protobuf:"varint,3,opt,name=min_lamports_per_signature,json=minLamportsPerSignature,proto3" json:"min_lamports_per_signature,omitempty"` + MaxLamportsPerSignature uint64 `protobuf:"varint,4,opt,name=max_lamports_per_signature,json=maxLamportsPerSignature,proto3" json:"max_lamports_per_signature,omitempty"` + BurnPercent uint32 `protobuf:"varint,5,opt,name=burn_percent,json=burnPercent,proto3" json:"burn_percent,omitempty"` } -func (x *AcctState) Reset() { - *x = AcctState{} +func (x *FeeRateGovernor) Reset() { + *x = FeeRateGovernor{} if protoimpl.UnsafeEnabled { mi := &file_context_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -171,13 +176,13 @@ func (x *AcctState) Reset() { } } -func (x *AcctState) String() string { +func (x *FeeRateGovernor) String() string { return protoimpl.X.MessageStringOf(x) } -func (*AcctState) ProtoMessage() {} +func (*FeeRateGovernor) ProtoMessage() {} -func (x *AcctState) ProtoReflect() protoreflect.Message { +func (x *FeeRateGovernor) ProtoReflect() protoreflect.Message { mi := &file_context_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -189,72 +194,66 @@ func (x *AcctState) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use AcctState.ProtoReflect.Descriptor instead. -func (*AcctState) Descriptor() ([]byte, []int) { +// Deprecated: Use FeeRateGovernor.ProtoReflect.Descriptor instead. +func (*FeeRateGovernor) Descriptor() ([]byte, []int) { return file_context_proto_rawDescGZIP(), []int{2} } -func (x *AcctState) GetAddress() []byte { +func (x *FeeRateGovernor) GetTargetLamportsPerSignature() uint64 { if x != nil { - return x.Address - } - return nil -} - -func (x *AcctState) GetLamports() uint64 { - if x != nil { - return x.Lamports + return x.TargetLamportsPerSignature } return 0 } -func (x *AcctState) GetData() []byte { - if x != nil { - return x.Data - } - return nil -} - -func (x *AcctState) GetExecutable() bool { +func (x *FeeRateGovernor) GetTargetSignaturesPerSlot() uint64 { if x != nil { - return x.Executable + return x.TargetSignaturesPerSlot } - return false + return 0 } -func (x *AcctState) GetRentEpoch() uint64 { +func (x *FeeRateGovernor) GetMinLamportsPerSignature() uint64 { if x != nil { - return x.RentEpoch + return x.MinLamportsPerSignature } return 0 } -func (x *AcctState) GetOwner() []byte { +func (x *FeeRateGovernor) GetMaxLamportsPerSignature() uint64 { if x != nil { - return x.Owner + return x.MaxLamportsPerSignature } - return nil + return 0 } -func (x *AcctState) GetSeedAddr() *SeedAddress { +func (x *FeeRateGovernor) GetBurnPercent() uint32 { if x != nil { - return x.SeedAddr + return x.BurnPercent } - return nil + return 0 } -// EpochContext includes context scoped to an epoch. -// On "real" ledgers, it is created during the epoch boundary. -type EpochContext struct { +type EpochSchedule struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Features *FeatureSet `protobuf:"bytes,1,opt,name=features,proto3" json:"features,omitempty"` -} - -func (x *EpochContext) Reset() { - *x = EpochContext{} + // The maximum number of slots in each epoch. + SlotsPerEpoch uint64 `protobuf:"varint,1,opt,name=slots_per_epoch,json=slotsPerEpoch,proto3" json:"slots_per_epoch,omitempty"` + // A number of slots before beginning of an epoch to calculate + // a leader schedule for that epoch. + LeaderScheduleSlotOffset uint64 `protobuf:"varint,2,opt,name=leader_schedule_slot_offset,json=leaderScheduleSlotOffset,proto3" json:"leader_schedule_slot_offset,omitempty"` + // Whether epochs start short and grow. + Warmup bool `protobuf:"varint,3,opt,name=warmup,proto3" json:"warmup,omitempty"` + // The first epoch after the warmup period. + FirstNormalEpoch uint64 `protobuf:"varint,4,opt,name=first_normal_epoch,json=firstNormalEpoch,proto3" json:"first_normal_epoch,omitempty"` + // The first slot after the warmup period. + FirstNormalSlot uint64 `protobuf:"varint,5,opt,name=first_normal_slot,json=firstNormalSlot,proto3" json:"first_normal_slot,omitempty"` +} + +func (x *EpochSchedule) Reset() { + *x = EpochSchedule{} if protoimpl.UnsafeEnabled { mi := &file_context_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -262,13 +261,13 @@ func (x *EpochContext) Reset() { } } -func (x *EpochContext) String() string { +func (x *EpochSchedule) String() string { return protoimpl.X.MessageStringOf(x) } -func (*EpochContext) ProtoMessage() {} +func (*EpochSchedule) ProtoMessage() {} -func (x *EpochContext) ProtoReflect() protoreflect.Message { +func (x *EpochSchedule) ProtoReflect() protoreflect.Message { mi := &file_context_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -280,31 +279,58 @@ func (x *EpochContext) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use EpochContext.ProtoReflect.Descriptor instead. -func (*EpochContext) Descriptor() ([]byte, []int) { +// Deprecated: Use EpochSchedule.ProtoReflect.Descriptor instead. +func (*EpochSchedule) Descriptor() ([]byte, []int) { return file_context_proto_rawDescGZIP(), []int{3} } -func (x *EpochContext) GetFeatures() *FeatureSet { +func (x *EpochSchedule) GetSlotsPerEpoch() uint64 { if x != nil { - return x.Features + return x.SlotsPerEpoch } - return nil + return 0 } -// SlotContext includes context scoped to a block. -// On "real" ledgers, it is created during the slot boundary. -type SlotContext struct { +func (x *EpochSchedule) GetLeaderScheduleSlotOffset() uint64 { + if x != nil { + return x.LeaderScheduleSlotOffset + } + return 0 +} + +func (x *EpochSchedule) GetWarmup() bool { + if x != nil { + return x.Warmup + } + return false +} + +func (x *EpochSchedule) GetFirstNormalEpoch() uint64 { + if x != nil { + return x.FirstNormalEpoch + } + return 0 +} + +func (x *EpochSchedule) GetFirstNormalSlot() uint64 { + if x != nil { + return x.FirstNormalSlot + } + return 0 +} + +// A single entry in the blockhash queue. +type BlockhashQueueEntry struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // Slot number - Slot uint64 `protobuf:"fixed64,1,opt,name=slot,proto3" json:"slot,omitempty"` + Blockhash []byte `protobuf:"bytes,1,opt,name=blockhash,proto3" json:"blockhash,omitempty"` + LamportsPerSignature uint64 `protobuf:"varint,2,opt,name=lamports_per_signature,json=lamportsPerSignature,proto3" json:"lamports_per_signature,omitempty"` } -func (x *SlotContext) Reset() { - *x = SlotContext{} +func (x *BlockhashQueueEntry) Reset() { + *x = BlockhashQueueEntry{} if protoimpl.UnsafeEnabled { mi := &file_context_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -312,13 +338,13 @@ func (x *SlotContext) Reset() { } } -func (x *SlotContext) String() string { +func (x *BlockhashQueueEntry) String() string { return protoimpl.X.MessageStringOf(x) } -func (*SlotContext) ProtoMessage() {} +func (*BlockhashQueueEntry) ProtoMessage() {} -func (x *SlotContext) ProtoReflect() protoreflect.Message { +func (x *BlockhashQueueEntry) ProtoReflect() protoreflect.Message { mi := &file_context_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -330,14 +356,21 @@ func (x *SlotContext) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use SlotContext.ProtoReflect.Descriptor instead. -func (*SlotContext) Descriptor() ([]byte, []int) { +// Deprecated: Use BlockhashQueueEntry.ProtoReflect.Descriptor instead. +func (*BlockhashQueueEntry) Descriptor() ([]byte, []int) { return file_context_proto_rawDescGZIP(), []int{4} } -func (x *SlotContext) GetSlot() uint64 { +func (x *BlockhashQueueEntry) GetBlockhash() []byte { if x != nil { - return x.Slot + return x.Blockhash + } + return nil +} + +func (x *BlockhashQueueEntry) GetLamportsPerSignature() uint64 { + if x != nil { + return x.LamportsPerSignature } return 0 } @@ -350,35 +383,57 @@ var file_context_proto_rawDesc = []byte{ 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x22, 0x28, 0x0a, 0x0a, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x53, 0x65, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x06, 0x52, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, - 0x73, 0x22, 0x4b, 0x0a, 0x0b, 0x53, 0x65, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x12, 0x12, 0x0a, 0x04, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, - 0x62, 0x61, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x65, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x04, 0x73, 0x65, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6f, 0x77, 0x6e, 0x65, - 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x22, 0xec, - 0x01, 0x0a, 0x09, 0x41, 0x63, 0x63, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, - 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x61, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x61, 0x6d, 0x70, 0x6f, 0x72, - 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x6c, 0x61, 0x6d, 0x70, 0x6f, 0x72, - 0x74, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1e, 0x0a, 0x0a, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, - 0x61, 0x62, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x65, 0x78, 0x65, 0x63, - 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x65, - 0x70, 0x6f, 0x63, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x72, 0x65, 0x6e, 0x74, - 0x45, 0x70, 0x6f, 0x63, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x12, 0x40, 0x0a, 0x09, 0x73, - 0x65, 0x65, 0x64, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, - 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, - 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x52, 0x08, 0x73, 0x65, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x22, 0x4e, 0x0a, - 0x0c, 0x45, 0x70, 0x6f, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x3e, 0x0a, - 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x22, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, - 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, - 0x53, 0x65, 0x74, 0x52, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x22, 0x21, 0x0a, - 0x0b, 0x53, 0x6c, 0x6f, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x12, 0x0a, 0x04, - 0x73, 0x6c, 0x6f, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x06, 0x52, 0x04, 0x73, 0x6c, 0x6f, 0x74, - 0x42, 0x0f, 0x5a, 0x0d, 0x2e, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, - 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x22, 0x97, 0x01, 0x0a, 0x09, 0x41, 0x63, 0x63, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, + 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x61, 0x6d, + 0x70, 0x6f, 0x72, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x6c, 0x61, 0x6d, + 0x70, 0x6f, 0x72, 0x74, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1e, 0x0a, 0x0a, 0x65, 0x78, 0x65, + 0x63, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x65, + 0x78, 0x65, 0x63, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6f, 0x77, 0x6e, + 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x4a, + 0x04, 0x08, 0x05, 0x10, 0x06, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x22, 0xae, 0x02, 0x0a, 0x0f, + 0x46, 0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x47, 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x12, + 0x41, 0x0a, 0x1d, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x6c, 0x61, 0x6d, 0x70, 0x6f, 0x72, + 0x74, 0x73, 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x1a, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x4c, 0x61, + 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x50, 0x65, 0x72, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, + 0x72, 0x65, 0x12, 0x3b, 0x0a, 0x1a, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x73, 0x69, 0x67, + 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x73, 0x6c, 0x6f, 0x74, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x17, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x69, + 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x50, 0x65, 0x72, 0x53, 0x6c, 0x6f, 0x74, 0x12, + 0x3b, 0x0a, 0x1a, 0x6d, 0x69, 0x6e, 0x5f, 0x6c, 0x61, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x5f, + 0x70, 0x65, 0x72, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x17, 0x6d, 0x69, 0x6e, 0x4c, 0x61, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x73, + 0x50, 0x65, 0x72, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x3b, 0x0a, 0x1a, + 0x6d, 0x61, 0x78, 0x5f, 0x6c, 0x61, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x5f, 0x70, 0x65, 0x72, + 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x17, 0x6d, 0x61, 0x78, 0x4c, 0x61, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x50, 0x65, 0x72, + 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x75, 0x72, + 0x6e, 0x5f, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x0b, 0x62, 0x75, 0x72, 0x6e, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x22, 0xe8, 0x01, 0x0a, + 0x0d, 0x45, 0x70, 0x6f, 0x63, 0x68, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x26, + 0x0a, 0x0f, 0x73, 0x6c, 0x6f, 0x74, 0x73, 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x65, 0x70, 0x6f, 0x63, + 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x73, 0x6c, 0x6f, 0x74, 0x73, 0x50, 0x65, + 0x72, 0x45, 0x70, 0x6f, 0x63, 0x68, 0x12, 0x3d, 0x0a, 0x1b, 0x6c, 0x65, 0x61, 0x64, 0x65, 0x72, + 0x5f, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x5f, 0x73, 0x6c, 0x6f, 0x74, 0x5f, 0x6f, + 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x18, 0x6c, 0x65, 0x61, + 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x53, 0x6c, 0x6f, 0x74, 0x4f, + 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x77, 0x61, 0x72, 0x6d, 0x75, 0x70, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x77, 0x61, 0x72, 0x6d, 0x75, 0x70, 0x12, 0x2c, 0x0a, + 0x12, 0x66, 0x69, 0x72, 0x73, 0x74, 0x5f, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x5f, 0x65, 0x70, + 0x6f, 0x63, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x10, 0x66, 0x69, 0x72, 0x73, 0x74, + 0x4e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x45, 0x70, 0x6f, 0x63, 0x68, 0x12, 0x2a, 0x0a, 0x11, 0x66, + 0x69, 0x72, 0x73, 0x74, 0x5f, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x5f, 0x73, 0x6c, 0x6f, 0x74, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x66, 0x69, 0x72, 0x73, 0x74, 0x4e, 0x6f, 0x72, + 0x6d, 0x61, 0x6c, 0x53, 0x6c, 0x6f, 0x74, 0x22, 0x69, 0x0a, 0x13, 0x42, 0x6c, 0x6f, 0x63, 0x6b, + 0x68, 0x61, 0x73, 0x68, 0x51, 0x75, 0x65, 0x75, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x1c, + 0x0a, 0x09, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x09, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x68, 0x61, 0x73, 0x68, 0x12, 0x34, 0x0a, 0x16, + 0x6c, 0x61, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x73, 0x69, 0x67, + 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x14, 0x6c, 0x61, + 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x50, 0x65, 0x72, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, + 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -395,20 +450,18 @@ func file_context_proto_rawDescGZIP() []byte { var file_context_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_context_proto_goTypes = []any{ - (*FeatureSet)(nil), // 0: org.solana.sealevel.v1.FeatureSet - (*SeedAddress)(nil), // 1: org.solana.sealevel.v1.SeedAddress - (*AcctState)(nil), // 2: org.solana.sealevel.v1.AcctState - (*EpochContext)(nil), // 3: org.solana.sealevel.v1.EpochContext - (*SlotContext)(nil), // 4: org.solana.sealevel.v1.SlotContext + (*FeatureSet)(nil), // 0: org.solana.sealevel.v1.FeatureSet + (*AcctState)(nil), // 1: org.solana.sealevel.v1.AcctState + (*FeeRateGovernor)(nil), // 2: org.solana.sealevel.v1.FeeRateGovernor + (*EpochSchedule)(nil), // 3: org.solana.sealevel.v1.EpochSchedule + (*BlockhashQueueEntry)(nil), // 4: org.solana.sealevel.v1.BlockhashQueueEntry } var file_context_proto_depIdxs = []int32{ - 1, // 0: org.solana.sealevel.v1.AcctState.seed_addr:type_name -> org.solana.sealevel.v1.SeedAddress - 0, // 1: org.solana.sealevel.v1.EpochContext.features:type_name -> org.solana.sealevel.v1.FeatureSet - 2, // [2:2] is the sub-list for method output_type - 2, // [2:2] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name } func init() { file_context_proto_init() } @@ -430,7 +483,7 @@ func file_context_proto_init() { } } file_context_proto_msgTypes[1].Exporter = func(v any, i int) any { - switch v := v.(*SeedAddress); i { + switch v := v.(*AcctState); i { case 0: return &v.state case 1: @@ -442,7 +495,7 @@ func file_context_proto_init() { } } file_context_proto_msgTypes[2].Exporter = func(v any, i int) any { - switch v := v.(*AcctState); i { + switch v := v.(*FeeRateGovernor); i { case 0: return &v.state case 1: @@ -454,7 +507,7 @@ func file_context_proto_init() { } } file_context_proto_msgTypes[3].Exporter = func(v any, i int) any { - switch v := v.(*EpochContext); i { + switch v := v.(*EpochSchedule); i { case 0: return &v.state case 1: @@ -466,7 +519,7 @@ func file_context_proto_init() { } } file_context_proto_msgTypes[4].Exporter = func(v any, i int) any { - switch v := v.(*SlotContext); i { + switch v := v.(*BlockhashQueueEntry); i { case 0: return &v.state case 1: diff --git a/conformance/ed25519_precompile_test.go b/conformance/ed25519_precompile_test.go index dfe25118..3064dfe7 100644 --- a/conformance/ed25519_precompile_test.go +++ b/conformance/ed25519_precompile_test.go @@ -1,67 +1,8 @@ package conformance -import ( - "fmt" - "io/ioutil" - "log" - "testing" - - "github.com/stretchr/testify/assert" - "google.golang.org/protobuf/proto" -) +import "testing" func TestConformance_Precompile_Ed25519_Program(t *testing.T) { - basePath := "test-vectors/precompile/fixtures/ed25519" - fileInfos, err := ioutil.ReadDir(basePath) - assert.NoError(t, err) - - var fnames []string - for _, fileInfo := range fileInfos { - filePath := fmt.Sprintf("%s/%s", basePath, fileInfo.Name()) - fnames = append(fnames, filePath) - } - - failedTestcases := make([]string, 0) - var testcaseCounter uint64 - - for _, fname := range fnames { - testcaseCounter++ - in, err := ioutil.ReadFile(fname) - if err != nil { - log.Fatalln("Error reading file:", err) - } - - fixture := &InstrFixture{} - if err := proto.Unmarshal(in, fixture); err != nil { - log.Fatalln("Failed to parse fixture:", err) - } - - execCtx, instrAccts := newExecCtxAndInstrAcctsFromFixture(fixture) - - printFixtureInfo(fixture) - - err = execCtx.ProcessInstruction(fixture.Input.Data, instrAccts, []uint64{0}) - - if err == nil && fixture.Output.Result != 0 { - failedTestcases = append(failedTestcases, fmt.Sprintf("failed testcase: %s. ed25519 returned success, but fixture reports %d\n", fname, fixture.Output.Result-1)) - } else if fixture.Output.Result == 0 && err != nil { - failedTestcases = append(failedTestcases, fmt.Sprintf("failed testcase: %s. ed25519 returned %s, but fixture reports success\n", fname, err)) - } - } - - fmt.Printf("\n\n") - - for _, fn := range failedTestcases { - fmt.Printf("%s\n", fn) - } - - hasFailedTestcases := len(failedTestcases) != 0 - - for _, errMsg := range failedTestcases { - fmt.Printf("%s\n", errMsg) - } - - fmt.Printf("\n\nfailed testcases %d / %d:\n", len(failedTestcases), len(fnames)) - - assert.Equal(t, false, hasFailedTestcases, "failing testcases found") + runPrecompileFixtures(t, "ed25519", + loadPrecompileFixtures(t, ed25519PrecompileProgram)) } diff --git a/conformance/firedancer_fixture_test.go b/conformance/firedancer_fixture_test.go index aca1e009..49afd117 100644 --- a/conformance/firedancer_fixture_test.go +++ b/conformance/firedancer_fixture_test.go @@ -182,13 +182,13 @@ func unmarshalFiredancerInstrFixture(data []byte) (*InstrFixture, error) { } features, ok := currentInstrFeatures(data) if ok { - currentFixture.Input.EpochContext = &EpochContext{Features: &FeatureSet{Features: features}} - } else if currentFixture.Input.EpochContext == nil { - currentFixture.Input.EpochContext = &EpochContext{} - } - if currentFixture.Input.SlotContext == nil { - currentFixture.Input.SlotContext = &SlotContext{} + // protosol v5.4.0 flattened epoch_context away: InstrContext now + // carries the FeatureSet directly at field 10. + currentFixture.Input.Features = &FeatureSet{Features: features} + } else if currentFixture.Input.Features == nil { + currentFixture.Input.Features = &FeatureSet{} } + // slot_context was removed from InstrContext in protosol v5.4.0. return &InstrFixture{ Input: currentFixture.Input, Output: currentFixture.Output, diff --git a/conformance/invoke.pb.go b/conformance/invoke.pb.go index b52893c5..6925f406 100644 --- a/conformance/invoke.pb.go +++ b/conformance/invoke.pb.go @@ -100,10 +100,10 @@ type InstrContext struct { // Account access list for this instruction (refers to above accounts list) InstrAccounts []*InstrAcct `protobuf:"bytes,4,rep,name=instr_accounts,json=instrAccounts,proto3" json:"instr_accounts,omitempty"` // The input data passed to program execution. - Data []byte `protobuf:"bytes,5,opt,name=data,proto3" json:"data,omitempty"` - CuAvail uint64 `protobuf:"varint,6,opt,name=cu_avail,json=cuAvail,proto3" json:"cu_avail,omitempty"` - SlotContext *SlotContext `protobuf:"bytes,8,opt,name=slot_context,json=slotContext,proto3" json:"slot_context,omitempty"` - EpochContext *EpochContext `protobuf:"bytes,9,opt,name=epoch_context,json=epochContext,proto3" json:"epoch_context,omitempty"` + Data []byte `protobuf:"bytes,5,opt,name=data,proto3" json:"data,omitempty"` + CuAvail uint64 `protobuf:"varint,6,opt,name=cu_avail,json=cuAvail,proto3" json:"cu_avail,omitempty"` + // Active feature set + Features *FeatureSet `protobuf:"bytes,10,opt,name=features,proto3" json:"features,omitempty"` } func (x *InstrContext) Reset() { @@ -173,16 +173,9 @@ func (x *InstrContext) GetCuAvail() uint64 { return 0 } -func (x *InstrContext) GetSlotContext() *SlotContext { +func (x *InstrContext) GetFeatures() *FeatureSet { if x != nil { - return x.SlotContext - } - return nil -} - -func (x *InstrContext) GetEpochContext() *EpochContext { - if x != nil { - return x.EpochContext + return x.Features } return nil } @@ -283,8 +276,9 @@ type InstrFixture struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Input *InstrContext `protobuf:"bytes,1,opt,name=input,proto3" json:"input,omitempty"` - Output *InstrEffects `protobuf:"bytes,2,opt,name=output,proto3" json:"output,omitempty"` + Metadata *FixtureMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + Input *InstrContext `protobuf:"bytes,2,opt,name=input,proto3" json:"input,omitempty"` + Output *InstrEffects `protobuf:"bytes,3,opt,name=output,proto3" json:"output,omitempty"` } func (x *InstrFixture) Reset() { @@ -319,6 +313,13 @@ func (*InstrFixture) Descriptor() ([]byte, []int) { return file_invoke_proto_rawDescGZIP(), []int{3} } +func (x *InstrFixture) GetMetadata() *FixtureMetadata { + if x != nil { + return x.Metadata + } + return nil +} + func (x *InstrFixture) GetInput() *InstrContext { if x != nil { return x.Input @@ -339,13 +340,14 @@ var file_invoke_proto_rawDesc = []byte{ 0x0a, 0x0c, 0x69, 0x6e, 0x76, 0x6f, 0x6b, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x16, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x1a, 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x5f, 0x0a, 0x09, 0x49, 0x6e, 0x73, 0x74, 0x72, 0x41, 0x63, 0x63, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x73, 0x5f, 0x77, 0x72, 0x69, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, 0x73, 0x57, 0x72, 0x69, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x73, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x73, - 0x53, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x22, 0xf8, 0x02, 0x0a, 0x0c, 0x49, 0x6e, 0x73, 0x74, 0x72, + 0x53, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x22, 0xb7, 0x02, 0x0a, 0x0c, 0x49, 0x6e, 0x73, 0x74, 0x72, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x61, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x61, 0x6d, 0x49, 0x64, 0x12, 0x3d, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, @@ -359,40 +361,39 @@ var file_invoke_proto_rawDesc = []byte{ 0x52, 0x0d, 0x69, 0x6e, 0x73, 0x74, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x75, 0x5f, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x63, 0x75, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x12, 0x46, - 0x0a, 0x0c, 0x73, 0x6c, 0x6f, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x08, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, - 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x6c, - 0x6f, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x0b, 0x73, 0x6c, 0x6f, 0x74, 0x43, - 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x49, 0x0a, 0x0d, 0x65, 0x70, 0x6f, 0x63, 0x68, 0x5f, - 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, - 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, - 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x70, 0x6f, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x74, - 0x65, 0x78, 0x74, 0x52, 0x0c, 0x65, 0x70, 0x6f, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, - 0x74, 0x22, 0xd1, 0x01, 0x0a, 0x0c, 0x49, 0x6e, 0x73, 0x74, 0x72, 0x45, 0x66, 0x66, 0x65, 0x63, - 0x74, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x75, - 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x65, 0x72, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, - 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x45, 0x72, 0x72, 0x12, 0x4e, 0x0a, 0x11, 0x6d, 0x6f, 0x64, - 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x18, 0x03, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, - 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, - 0x63, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x10, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, - 0x64, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x75, 0x5f, - 0x61, 0x76, 0x61, 0x69, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x63, 0x75, 0x41, - 0x76, 0x61, 0x69, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x5f, 0x64, - 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x72, 0x65, 0x74, 0x75, 0x72, - 0x6e, 0x44, 0x61, 0x74, 0x61, 0x22, 0x88, 0x01, 0x0a, 0x0c, 0x49, 0x6e, 0x73, 0x74, 0x72, 0x46, - 0x69, 0x78, 0x74, 0x75, 0x72, 0x65, 0x12, 0x3a, 0x0a, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, - 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x49, - 0x6e, 0x73, 0x74, 0x72, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x05, 0x69, 0x6e, 0x70, - 0x75, 0x74, 0x12, 0x3c, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, - 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x73, 0x74, - 0x72, 0x45, 0x66, 0x66, 0x65, 0x63, 0x74, 0x73, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, - 0x42, 0x0f, 0x5a, 0x0d, 0x2e, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, - 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x63, 0x75, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x12, 0x3e, + 0x0a, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x22, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, + 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x53, 0x65, 0x74, 0x52, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x4a, 0x04, + 0x08, 0x07, 0x10, 0x08, 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, 0x4a, 0x04, 0x08, 0x09, 0x10, 0x0a, + 0x22, 0xd1, 0x01, 0x0a, 0x0c, 0x49, 0x6e, 0x73, 0x74, 0x72, 0x45, 0x66, 0x66, 0x65, 0x63, 0x74, + 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x75, 0x73, + 0x74, 0x6f, 0x6d, 0x5f, 0x65, 0x72, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x63, + 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x45, 0x72, 0x72, 0x12, 0x4e, 0x0a, 0x11, 0x6d, 0x6f, 0x64, 0x69, + 0x66, 0x69, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, + 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x63, + 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x10, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, + 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x75, 0x5f, 0x61, + 0x76, 0x61, 0x69, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x63, 0x75, 0x41, 0x76, + 0x61, 0x69, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x5f, 0x64, 0x61, + 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, + 0x44, 0x61, 0x74, 0x61, 0x22, 0xcd, 0x01, 0x0a, 0x0c, 0x49, 0x6e, 0x73, 0x74, 0x72, 0x46, 0x69, + 0x78, 0x74, 0x75, 0x72, 0x65, 0x12, 0x43, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, + 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, + 0x2e, 0x46, 0x69, 0x78, 0x74, 0x75, 0x72, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x3a, 0x0a, 0x05, 0x69, 0x6e, + 0x70, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6f, 0x72, 0x67, 0x2e, + 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, + 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x73, 0x74, 0x72, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, + 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x3c, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, + 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, + 0x49, 0x6e, 0x73, 0x74, 0x72, 0x45, 0x66, 0x66, 0x65, 0x63, 0x74, 0x73, 0x52, 0x06, 0x6f, 0x75, + 0x74, 0x70, 0x75, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -409,20 +410,20 @@ func file_invoke_proto_rawDescGZIP() []byte { var file_invoke_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_invoke_proto_goTypes = []any{ - (*InstrAcct)(nil), // 0: org.solana.sealevel.v1.InstrAcct - (*InstrContext)(nil), // 1: org.solana.sealevel.v1.InstrContext - (*InstrEffects)(nil), // 2: org.solana.sealevel.v1.InstrEffects - (*InstrFixture)(nil), // 3: org.solana.sealevel.v1.InstrFixture - (*AcctState)(nil), // 4: org.solana.sealevel.v1.AcctState - (*SlotContext)(nil), // 5: org.solana.sealevel.v1.SlotContext - (*EpochContext)(nil), // 6: org.solana.sealevel.v1.EpochContext + (*InstrAcct)(nil), // 0: org.solana.sealevel.v1.InstrAcct + (*InstrContext)(nil), // 1: org.solana.sealevel.v1.InstrContext + (*InstrEffects)(nil), // 2: org.solana.sealevel.v1.InstrEffects + (*InstrFixture)(nil), // 3: org.solana.sealevel.v1.InstrFixture + (*AcctState)(nil), // 4: org.solana.sealevel.v1.AcctState + (*FeatureSet)(nil), // 5: org.solana.sealevel.v1.FeatureSet + (*FixtureMetadata)(nil), // 6: org.solana.sealevel.v1.FixtureMetadata } var file_invoke_proto_depIdxs = []int32{ 4, // 0: org.solana.sealevel.v1.InstrContext.accounts:type_name -> org.solana.sealevel.v1.AcctState 0, // 1: org.solana.sealevel.v1.InstrContext.instr_accounts:type_name -> org.solana.sealevel.v1.InstrAcct - 5, // 2: org.solana.sealevel.v1.InstrContext.slot_context:type_name -> org.solana.sealevel.v1.SlotContext - 6, // 3: org.solana.sealevel.v1.InstrContext.epoch_context:type_name -> org.solana.sealevel.v1.EpochContext - 4, // 4: org.solana.sealevel.v1.InstrEffects.modified_accounts:type_name -> org.solana.sealevel.v1.AcctState + 5, // 2: org.solana.sealevel.v1.InstrContext.features:type_name -> org.solana.sealevel.v1.FeatureSet + 4, // 3: org.solana.sealevel.v1.InstrEffects.modified_accounts:type_name -> org.solana.sealevel.v1.AcctState + 6, // 4: org.solana.sealevel.v1.InstrFixture.metadata:type_name -> org.solana.sealevel.v1.FixtureMetadata 1, // 5: org.solana.sealevel.v1.InstrFixture.input:type_name -> org.solana.sealevel.v1.InstrContext 2, // 6: org.solana.sealevel.v1.InstrFixture.output:type_name -> org.solana.sealevel.v1.InstrEffects 7, // [7:7] is the sub-list for method output_type @@ -438,6 +439,7 @@ func file_invoke_proto_init() { return } file_context_proto_init() + file_metadata_proto_init() if !protoimpl.UnsafeEnabled { file_invoke_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*InstrAcct); i { diff --git a/conformance/metadata.pb.go b/conformance/metadata.pb.go new file mode 100644 index 00000000..9807e0b8 --- /dev/null +++ b/conformance/metadata.pb.go @@ -0,0 +1,144 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.2 +// protoc v3.21.12 +// source: metadata.proto + +package conformance + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// FixtureMetadata includes the metadata for the fixture +type FixtureMetadata struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FnEntrypoint string `protobuf:"bytes,1,opt,name=fn_entrypoint,json=fnEntrypoint,proto3" json:"fn_entrypoint,omitempty"` +} + +func (x *FixtureMetadata) Reset() { + *x = FixtureMetadata{} + if protoimpl.UnsafeEnabled { + mi := &file_metadata_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FixtureMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FixtureMetadata) ProtoMessage() {} + +func (x *FixtureMetadata) ProtoReflect() protoreflect.Message { + mi := &file_metadata_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FixtureMetadata.ProtoReflect.Descriptor instead. +func (*FixtureMetadata) Descriptor() ([]byte, []int) { + return file_metadata_proto_rawDescGZIP(), []int{0} +} + +func (x *FixtureMetadata) GetFnEntrypoint() string { + if x != nil { + return x.FnEntrypoint + } + return "" +} + +var File_metadata_proto protoreflect.FileDescriptor + +var file_metadata_proto_rawDesc = []byte{ + 0x0a, 0x0e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x12, 0x16, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, + 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x22, 0x36, 0x0a, 0x0f, 0x46, 0x69, 0x78, 0x74, + 0x75, 0x72, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x23, 0x0a, 0x0d, 0x66, + 0x6e, 0x5f, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0c, 0x66, 0x6e, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x70, 0x6f, 0x69, 0x6e, 0x74, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_metadata_proto_rawDescOnce sync.Once + file_metadata_proto_rawDescData = file_metadata_proto_rawDesc +) + +func file_metadata_proto_rawDescGZIP() []byte { + file_metadata_proto_rawDescOnce.Do(func() { + file_metadata_proto_rawDescData = protoimpl.X.CompressGZIP(file_metadata_proto_rawDescData) + }) + return file_metadata_proto_rawDescData +} + +var file_metadata_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_metadata_proto_goTypes = []any{ + (*FixtureMetadata)(nil), // 0: org.solana.sealevel.v1.FixtureMetadata +} +var file_metadata_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_metadata_proto_init() } +func file_metadata_proto_init() { + if File_metadata_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_metadata_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*FixtureMetadata); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_metadata_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_metadata_proto_goTypes, + DependencyIndexes: file_metadata_proto_depIdxs, + MessageInfos: file_metadata_proto_msgTypes, + }.Build() + File_metadata_proto = out.File + file_metadata_proto_rawDesc = nil + file_metadata_proto_goTypes = nil + file_metadata_proto_depIdxs = nil +} diff --git a/conformance/precompile_common_test.go b/conformance/precompile_common_test.go new file mode 100644 index 00000000..3132d1e6 --- /dev/null +++ b/conformance/precompile_common_test.go @@ -0,0 +1,107 @@ +package conformance + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/gagliardetto/solana-go" + "google.golang.org/protobuf/proto" +) + +// Firedancer publishes every precompile fixture in one flat directory and +// distinguishes them by the program id inside the fixture, not by a per-program +// subdirectory. Filtering therefore has to parse each fixture rather than glob a +// path. An earlier revision of these tests read +// test-vectors/precompile/fixtures/, a layout that no longer exists +// upstream, so they failed on a missing directory before running a single case. +const precompileFixtureDir = "test-vectors/instr/fixtures/precompile" + +var ( + ed25519PrecompileProgram = solana.MustPublicKeyFromBase58("Ed25519SigVerify111111111111111111111111111") + secp256k1PrecompileProgram = solana.MustPublicKeyFromBase58("KeccakSecp256k11111111111111111111111111111") + secp256r1PrecompileProgram = solana.MustPublicKeyFromBase58("Secp256r1SigVerify1111111111111111111111111") +) + +// loadPrecompileFixtures returns every fixture whose instruction targets the +// given precompile. It skips rather than fails when the corpus is absent: the +// vectors are a multi-gigabyte external checkout that is deliberately +// gitignored, so a developer without them should not see a red build. +func loadPrecompileFixtures(t *testing.T, program solana.PublicKey) []string { + t.Helper() + + entries, err := os.ReadDir(precompileFixtureDir) + if err != nil { + t.Skipf("conformance corpus not available at %s (%v); run 'make conformance-vectors' to fetch it", + precompileFixtureDir, err) + } + + var matched []string + for _, entry := range entries { + if entry.IsDir() || filepath.Ext(entry.Name()) != ".fix" { + continue + } + path := filepath.Join(precompileFixtureDir, entry.Name()) + raw, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read fixture %s: %v", path, err) + } + fixture := &InstrFixture{} + if err := proto.Unmarshal(raw, fixture); err != nil { + t.Fatalf("decode fixture %s: %v", path, err) + } + if fixture.Input == nil || !bytes.Equal(fixture.Input.ProgramId, program[:]) { + continue + } + matched = append(matched, path) + } + + if len(matched) == 0 { + t.Skipf("no fixtures in %s target %s", precompileFixtureDir, program) + } + return matched +} + +// runPrecompileFixtures executes each fixture and reports every disagreement +// rather than stopping at the first, so a run reports a rate instead of one +// arbitrary case. +func runPrecompileFixtures(t *testing.T, label string, paths []string) { + t.Helper() + + verbose := os.Getenv("MITHRIL_CONFORMANCE_VERBOSE") != "" + var failures []string + + for _, path := range paths { + raw, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read fixture %s: %v", path, err) + } + fixture := &InstrFixture{} + if err := proto.Unmarshal(raw, fixture); err != nil { + t.Fatalf("decode fixture %s: %v", path, err) + } + + execCtx, instrAccts := newExecCtxAndInstrAcctsFromFixture(fixture) + if verbose { + printFixtureInfo(fixture) + } + + err = execCtx.ProcessInstruction(fixture.Input.Data, instrAccts, []uint64{0}) + + switch { + case err == nil && fixture.Output.Result != 0: + failures = append(failures, fmt.Sprintf( + "%s: we accepted, fixture expects error %d", path, fixture.Output.Result-1)) + case err != nil && fixture.Output.Result == 0: + failures = append(failures, fmt.Sprintf( + "%s: we returned %v, fixture expects success", path, err)) + } + } + + for _, failure := range failures { + t.Errorf("%s", failure) + } + t.Logf("%s: %d/%d fixtures matched", label, len(paths)-len(failures), len(paths)) +} diff --git a/conformance/secp256k1_precompile_test.go b/conformance/secp256k1_precompile_test.go index 6c823b1c..1dc90e6d 100644 --- a/conformance/secp256k1_precompile_test.go +++ b/conformance/secp256k1_precompile_test.go @@ -1,67 +1,8 @@ package conformance -import ( - "fmt" - "io/ioutil" - "log" - "testing" - - "github.com/stretchr/testify/assert" - "google.golang.org/protobuf/proto" -) +import "testing" func TestConformance_Precompile_Secp256k1_Program(t *testing.T) { - basePath := "test-vectors/precompile/fixtures/secp256k1" - fileInfos, err := ioutil.ReadDir(basePath) - assert.NoError(t, err) - - var fnames []string - for _, fileInfo := range fileInfos { - filePath := fmt.Sprintf("%s/%s", basePath, fileInfo.Name()) - fnames = append(fnames, filePath) - } - - failedTestcases := make([]string, 0) - var testcaseCounter uint64 - - for _, fname := range fnames { - testcaseCounter++ - in, err := ioutil.ReadFile(fname) - if err != nil { - log.Fatalln("Error reading file:", err) - } - - fixture := &InstrFixture{} - if err := proto.Unmarshal(in, fixture); err != nil { - log.Fatalln("Failed to parse fixture:", err) - } - - execCtx, instrAccts := newExecCtxAndInstrAcctsFromFixture(fixture) - - printFixtureInfo(fixture) - - err = execCtx.ProcessInstruction(fixture.Input.Data, instrAccts, []uint64{0}) - - if err == nil && fixture.Output.Result != 0 { - failedTestcases = append(failedTestcases, fmt.Sprintf("failed testcase: %s. secp256k1 returned success, but fixture reports %d\n", fname, fixture.Output.Result-1)) - } else if fixture.Output.Result == 0 && err != nil { - failedTestcases = append(failedTestcases, fmt.Sprintf("failed testcase: %s. secp256k1 returned %s, but fixture reports success\n", fname, err)) - } - } - - fmt.Printf("\n\n") - - for _, fn := range failedTestcases { - fmt.Printf("%s\n", fn) - } - - hasFailedTestcases := len(failedTestcases) != 0 - - for _, errMsg := range failedTestcases { - fmt.Printf("%s\n", errMsg) - } - - fmt.Printf("\n\nfailed testcases %d / %d:\n", len(failedTestcases), len(fnames)) - - assert.Equal(t, false, hasFailedTestcases, "failing testcases found") + runPrecompileFixtures(t, "secp256k1", + loadPrecompileFixtures(t, secp256k1PrecompileProgram)) } diff --git a/conformance/secp256r1_precompile_test.go b/conformance/secp256r1_precompile_test.go new file mode 100644 index 00000000..987afcda --- /dev/null +++ b/conformance/secp256r1_precompile_test.go @@ -0,0 +1,10 @@ +package conformance + +import "testing" + +// secp256r1 is the newest precompile and carries the largest fixture set in the +// corpus, but had no conformance test at all until now. +func TestConformance_Precompile_Secp256r1_Program(t *testing.T) { + runPrecompileFixtures(t, "secp256r1", + loadPrecompileFixtures(t, secp256r1PrecompileProgram)) +} diff --git a/conformance/serialize.pb.go b/conformance/serialize.pb.go index 7e37d348..7690d82c 100644 --- a/conformance/serialize.pb.go +++ b/conformance/serialize.pb.go @@ -156,9 +156,8 @@ var file_serialize_proto_rawDesc = []byte{ 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x6d, 0x4d, 0x65, 0x6d, 0x52, 0x65, 0x67, 0x69, - 0x6f, 0x6e, 0x52, 0x07, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x0f, 0x5a, 0x0d, 0x2e, - 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x6f, 0x6e, 0x52, 0x07, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( diff --git a/conformance/test_common.go b/conformance/test_common.go index d082ace7..c3978fce 100644 --- a/conformance/test_common.go +++ b/conformance/test_common.go @@ -4,6 +4,8 @@ import ( "bytes" "encoding/binary" "fmt" + "math" + "os" "testing" "github.com/Overclock-Validator/mithril/pkg/accounts" @@ -18,18 +20,23 @@ import ( func fixtureAcctStateToAccount(acctState *AcctState) accounts.Account { var acct accounts.Account + // See createProgramAcct: the field is gone from the schema, and its implied + // value for a converted account is the maximum, not zero. + acct.RentEpoch = math.MaxUint64 acct.Key = solana.PublicKeyFromBytes(acctState.Address[:]) acct.Lamports = acctState.Lamports acct.Data = acctState.Data acct.Executable = acctState.Executable - acct.RentEpoch = acctState.RentEpoch copy(acct.Owner[:], acctState.Owner) return acct } func createProgramAcct(programId []byte) accounts.Account { programKey := solana.PublicKeyFromBytes(programId) - programAcct := accounts.Account{Key: programKey, Lamports: 100000000, Data: make([]byte, 0), Owner: a.NativeLoaderAddr, Executable: true, RentEpoch: 100} + // protosol v5.4.0 dropped rent_epoch from AcctState, but converted accounts + // are defined as carrying the maximum value rather than zero. Leaving the Go + // zero here would silently model every account as rent-paying. + programAcct := accounts.Account{Key: programKey, Lamports: 100000000, Data: make([]byte, 0), Owner: a.NativeLoaderAddr, Executable: true, RentEpoch: math.MaxUint64} return programAcct } @@ -211,11 +218,21 @@ func parseAndConfigureFeatures(execCtx *sealevel.ExecutionCtx, fixture *InstrFix f := features.NewFeaturesDefault() execCtx.Features = *f - for _, ftr := range fixture.Input.EpochContext.Features.Features { + // Not every fixture carries an epoch context; the precompile corpus mostly + // does not. Treat a missing one as "no features beyond the defaults" rather + // than dereferencing through it. + if fixture.Input == nil || fixture.Input.Features == nil { + return + } + + verbose := os.Getenv("MITHRIL_CONFORMANCE_VERBOSE") != "" + for _, ftr := range fixture.Input.Features.Features { for _, featureGate := range features.AllFeatureGates { featureIdInt := binary.LittleEndian.Uint64(featureGate.Address[:8]) if featureIdInt == ftr { - fmt.Printf("enabling feature %s\n", featureGate.Name) + if verbose { + fmt.Printf("enabling feature %s\n", featureGate.Name) + } execCtx.Features.EnableFeature(featureGate, 0) } } @@ -323,9 +340,8 @@ func accountStateChangesMatch(t *testing.T, execCtx *sealevel.ExecutionCtx, fixt if fixtureModifiedAcct.Executable != mithrilModifiedAcct.Executable { return false } - if fixtureModifiedAcct.RentEpoch != mithrilModifiedAcct.RentEpoch { - return false - } + // AcctState dropped rent_epoch in protosol v5.4.0, so the + // corpus no longer carries an expected value to compare. if solana.PublicKeyFromBytes(fixtureModifiedAcct.Owner[:]) != solana.PublicKeyFromBytes(mithrilModifiedAcct.Owner[:]) { return false } diff --git a/conformance/txn.pb.go b/conformance/txn.pb.go index 1e9d33af..906720ad 100644 --- a/conformance/txn.pb.go +++ b/conformance/txn.pb.go @@ -214,62 +214,6 @@ func (x *MessageAddressTableLookup) GetReadonlyIndexes() []uint32 { return nil } -// Addresses loaded with on-chain lookup tables -type LoadedAddresses struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Writable [][]byte `protobuf:"bytes,1,rep,name=writable,proto3" json:"writable,omitempty"` - Readonly [][]byte `protobuf:"bytes,2,rep,name=readonly,proto3" json:"readonly,omitempty"` -} - -func (x *LoadedAddresses) Reset() { - *x = LoadedAddresses{} - if protoimpl.UnsafeEnabled { - mi := &file_txn_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *LoadedAddresses) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*LoadedAddresses) ProtoMessage() {} - -func (x *LoadedAddresses) ProtoReflect() protoreflect.Message { - mi := &file_txn_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use LoadedAddresses.ProtoReflect.Descriptor instead. -func (*LoadedAddresses) Descriptor() ([]byte, []int) { - return file_txn_proto_rawDescGZIP(), []int{3} -} - -func (x *LoadedAddresses) GetWritable() [][]byte { - if x != nil { - return x.Writable - } - return nil -} - -func (x *LoadedAddresses) GetReadonly() [][]byte { - if x != nil { - return x.Readonly - } - return nil -} - // Message contains the transaction data type TransactionMessage struct { state protoimpl.MessageState @@ -281,22 +225,18 @@ type TransactionMessage struct { Header *MessageHeader `protobuf:"bytes,2,opt,name=header,proto3" json:"header,omitempty"` // Vector of pubkeys AccountKeys [][]byte `protobuf:"bytes,3,rep,name=account_keys,json=accountKeys,proto3" json:"account_keys,omitempty"` - // Data associated with the accounts referred above. Not all accounts need to be here. - AccountSharedData []*AcctState `protobuf:"bytes,4,rep,name=account_shared_data,json=accountSharedData,proto3" json:"account_shared_data,omitempty"` // Recent blockhash provided in message RecentBlockhash []byte `protobuf:"bytes,5,opt,name=recent_blockhash,json=recentBlockhash,proto3" json:"recent_blockhash,omitempty"` // The instructions this transaction executes Instructions []*CompiledInstruction `protobuf:"bytes,6,rep,name=instructions,proto3" json:"instructions,omitempty"` // Not available in legacy message AddressTableLookups []*MessageAddressTableLookup `protobuf:"bytes,7,rep,name=address_table_lookups,json=addressTableLookups,proto3" json:"address_table_lookups,omitempty"` - // Not available in legacy messages - LoadedAddresses *LoadedAddresses `protobuf:"bytes,8,opt,name=loaded_addresses,json=loadedAddresses,proto3" json:"loaded_addresses,omitempty"` } func (x *TransactionMessage) Reset() { *x = TransactionMessage{} if protoimpl.UnsafeEnabled { - mi := &file_txn_proto_msgTypes[4] + mi := &file_txn_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -309,7 +249,7 @@ func (x *TransactionMessage) String() string { func (*TransactionMessage) ProtoMessage() {} func (x *TransactionMessage) ProtoReflect() protoreflect.Message { - mi := &file_txn_proto_msgTypes[4] + mi := &file_txn_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -322,7 +262,7 @@ func (x *TransactionMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use TransactionMessage.ProtoReflect.Descriptor instead. func (*TransactionMessage) Descriptor() ([]byte, []int) { - return file_txn_proto_rawDescGZIP(), []int{4} + return file_txn_proto_rawDescGZIP(), []int{3} } func (x *TransactionMessage) GetIsLegacy() bool { @@ -346,13 +286,6 @@ func (x *TransactionMessage) GetAccountKeys() [][]byte { return nil } -func (x *TransactionMessage) GetAccountSharedData() []*AcctState { - if x != nil { - return x.AccountSharedData - } - return nil -} - func (x *TransactionMessage) GetRecentBlockhash() []byte { if x != nil { return x.RecentBlockhash @@ -374,13 +307,6 @@ func (x *TransactionMessage) GetAddressTableLookups() []*MessageAddressTableLook return nil } -func (x *TransactionMessage) GetLoadedAddresses() *LoadedAddresses { - if x != nil { - return x.LoadedAddresses - } - return nil -} - // A valid verified transaction type SanitizedTransaction struct { state protoimpl.MessageState @@ -391,8 +317,6 @@ type SanitizedTransaction struct { Message *TransactionMessage `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` // The message hash MessageHash []byte `protobuf:"bytes,2,opt,name=message_hash,json=messageHash,proto3" json:"message_hash,omitempty"` - // Is this a voting transaction? - IsSimpleVoteTx bool `protobuf:"varint,3,opt,name=is_simple_vote_tx,json=isSimpleVoteTx,proto3" json:"is_simple_vote_tx,omitempty"` // The signatures needed in the transaction Signatures [][]byte `protobuf:"bytes,4,rep,name=signatures,proto3" json:"signatures,omitempty"` } @@ -400,7 +324,7 @@ type SanitizedTransaction struct { func (x *SanitizedTransaction) Reset() { *x = SanitizedTransaction{} if protoimpl.UnsafeEnabled { - mi := &file_txn_proto_msgTypes[5] + mi := &file_txn_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -413,7 +337,7 @@ func (x *SanitizedTransaction) String() string { func (*SanitizedTransaction) ProtoMessage() {} func (x *SanitizedTransaction) ProtoReflect() protoreflect.Message { - mi := &file_txn_proto_msgTypes[5] + mi := &file_txn_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -426,7 +350,7 @@ func (x *SanitizedTransaction) ProtoReflect() protoreflect.Message { // Deprecated: Use SanitizedTransaction.ProtoReflect.Descriptor instead. func (*SanitizedTransaction) Descriptor() ([]byte, []int) { - return file_txn_proto_rawDescGZIP(), []int{5} + return file_txn_proto_rawDescGZIP(), []int{4} } func (x *SanitizedTransaction) GetMessage() *TransactionMessage { @@ -443,13 +367,6 @@ func (x *SanitizedTransaction) GetMessageHash() []byte { return nil } -func (x *SanitizedTransaction) GetIsSimpleVoteTx() bool { - if x != nil { - return x.IsSimpleVoteTx - } - return false -} - func (x *SanitizedTransaction) GetSignatures() [][]byte { if x != nil { return x.Signatures @@ -457,41 +374,38 @@ func (x *SanitizedTransaction) GetSignatures() [][]byte { return nil } -// This Transaction context be used to fuzz either `load_execute_and_commit_transactions`, -// `load_and_execute_transactions` in `bank.rs` or `load_and_execute_sanitized_transactions` -// in `svm/transaction_processor.rs` -type TxnContext struct { +// Bank fields relevant to transaction execution +type TxnBank struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // The transaction data - Tx *SanitizedTransaction `protobuf:"bytes,1,opt,name=tx,proto3" json:"tx,omitempty"` - // The maximum age allowed for this transaction - MaxAge uint64 `protobuf:"varint,2,opt,name=max_age,json=maxAge,proto3" json:"max_age,omitempty"` // Up to 300 (actually 301) most recent blockhashes (ordered from oldest to newest) - BlockhashQueue [][]byte `protobuf:"bytes,3,rep,name=blockhash_queue,json=blockhashQueue,proto3" json:"blockhash_queue,omitempty"` - EpochCtx *EpochContext `protobuf:"bytes,4,opt,name=epoch_ctx,json=epochCtx,proto3" json:"epoch_ctx,omitempty"` - SlotCtx *SlotContext `protobuf:"bytes,5,opt,name=slot_ctx,json=slotCtx,proto3" json:"slot_ctx,omitempty"` + BlockhashQueue []*BlockhashQueueEntry `protobuf:"bytes,1,rep,name=blockhash_queue,json=blockhashQueue,proto3" json:"blockhash_queue,omitempty"` + RbhLamportsPerSignature uint32 `protobuf:"varint,2,opt,name=rbh_lamports_per_signature,json=rbhLamportsPerSignature,proto3" json:"rbh_lamports_per_signature,omitempty"` + FeeRateGovernor *FeeRateGovernor `protobuf:"bytes,3,opt,name=fee_rate_governor,json=feeRateGovernor,proto3" json:"fee_rate_governor,omitempty"` + TotalEpochStake uint64 `protobuf:"varint,4,opt,name=total_epoch_stake,json=totalEpochStake,proto3" json:"total_epoch_stake,omitempty"` + EpochSchedule *EpochSchedule `protobuf:"bytes,5,opt,name=epoch_schedule,json=epochSchedule,proto3" json:"epoch_schedule,omitempty"` + Features *FeatureSet `protobuf:"bytes,7,opt,name=features,proto3" json:"features,omitempty"` } -func (x *TxnContext) Reset() { - *x = TxnContext{} +func (x *TxnBank) Reset() { + *x = TxnBank{} if protoimpl.UnsafeEnabled { - mi := &file_txn_proto_msgTypes[6] + mi := &file_txn_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *TxnContext) String() string { +func (x *TxnBank) String() string { return protoimpl.X.MessageStringOf(x) } -func (*TxnContext) ProtoMessage() {} +func (*TxnBank) ProtoMessage() {} -func (x *TxnContext) ProtoReflect() protoreflect.Message { - mi := &file_txn_proto_msgTypes[6] +func (x *TxnBank) ProtoReflect() protoreflect.Message { + mi := &file_txn_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -502,74 +416,86 @@ func (x *TxnContext) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use TxnContext.ProtoReflect.Descriptor instead. -func (*TxnContext) Descriptor() ([]byte, []int) { - return file_txn_proto_rawDescGZIP(), []int{6} +// Deprecated: Use TxnBank.ProtoReflect.Descriptor instead. +func (*TxnBank) Descriptor() ([]byte, []int) { + return file_txn_proto_rawDescGZIP(), []int{5} } -func (x *TxnContext) GetTx() *SanitizedTransaction { +func (x *TxnBank) GetBlockhashQueue() []*BlockhashQueueEntry { if x != nil { - return x.Tx + return x.BlockhashQueue } return nil } -func (x *TxnContext) GetMaxAge() uint64 { +func (x *TxnBank) GetRbhLamportsPerSignature() uint32 { if x != nil { - return x.MaxAge + return x.RbhLamportsPerSignature } return 0 } -func (x *TxnContext) GetBlockhashQueue() [][]byte { +func (x *TxnBank) GetFeeRateGovernor() *FeeRateGovernor { if x != nil { - return x.BlockhashQueue + return x.FeeRateGovernor } return nil } -func (x *TxnContext) GetEpochCtx() *EpochContext { +func (x *TxnBank) GetTotalEpochStake() uint64 { + if x != nil { + return x.TotalEpochStake + } + return 0 +} + +func (x *TxnBank) GetEpochSchedule() *EpochSchedule { if x != nil { - return x.EpochCtx + return x.EpochSchedule } return nil } -func (x *TxnContext) GetSlotCtx() *SlotContext { +func (x *TxnBank) GetFeatures() *FeatureSet { if x != nil { - return x.SlotCtx + return x.Features } return nil } -// The resulting state of an account after a transaction -type ResultingState struct { +// This Transaction context be used to fuzz either `load_execute_and_commit_transactions`, +// `load_and_execute_transactions` in `bank.rs` or `load_and_execute_sanitized_transactions` +// in `svm/transaction_processor.rs` +type TxnContext struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - AcctStates []*AcctState `protobuf:"bytes,1,rep,name=acct_states,json=acctStates,proto3" json:"acct_states,omitempty"` - RentDebits []*RentDebits `protobuf:"bytes,2,rep,name=rent_debits,json=rentDebits,proto3" json:"rent_debits,omitempty"` - TransactionRent uint64 `protobuf:"varint,3,opt,name=transaction_rent,json=transactionRent,proto3" json:"transaction_rent,omitempty"` + // The transaction data + Tx *SanitizedTransaction `protobuf:"bytes,1,opt,name=tx,proto3" json:"tx,omitempty"` + // Data associated with transaction accounts, sysvars, etc. + AccountSharedData []*AcctState `protobuf:"bytes,2,rep,name=account_shared_data,json=accountSharedData,proto3" json:"account_shared_data,omitempty"` + // Bank fields for the transaction fuzzer + Bank *TxnBank `protobuf:"bytes,6,opt,name=bank,proto3" json:"bank,omitempty"` } -func (x *ResultingState) Reset() { - *x = ResultingState{} +func (x *TxnContext) Reset() { + *x = TxnContext{} if protoimpl.UnsafeEnabled { - mi := &file_txn_proto_msgTypes[7] + mi := &file_txn_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *ResultingState) String() string { +func (x *TxnContext) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ResultingState) ProtoMessage() {} +func (*TxnContext) ProtoMessage() {} -func (x *ResultingState) ProtoReflect() protoreflect.Message { - mi := &file_txn_proto_msgTypes[7] +func (x *TxnContext) ProtoReflect() protoreflect.Message { + mi := &file_txn_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -580,88 +506,32 @@ func (x *ResultingState) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ResultingState.ProtoReflect.Descriptor instead. -func (*ResultingState) Descriptor() ([]byte, []int) { - return file_txn_proto_rawDescGZIP(), []int{7} +// Deprecated: Use TxnContext.ProtoReflect.Descriptor instead. +func (*TxnContext) Descriptor() ([]byte, []int) { + return file_txn_proto_rawDescGZIP(), []int{6} } -func (x *ResultingState) GetAcctStates() []*AcctState { +func (x *TxnContext) GetTx() *SanitizedTransaction { if x != nil { - return x.AcctStates + return x.Tx } return nil } -func (x *ResultingState) GetRentDebits() []*RentDebits { +func (x *TxnContext) GetAccountSharedData() []*AcctState { if x != nil { - return x.RentDebits + return x.AccountSharedData } return nil } -func (x *ResultingState) GetTransactionRent() uint64 { +func (x *TxnContext) GetBank() *TxnBank { if x != nil { - return x.TransactionRent - } - return 0 -} - -// The rent state for an account after a transaction -type RentDebits struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Pubkey []byte `protobuf:"bytes,1,opt,name=pubkey,proto3" json:"pubkey,omitempty"` - RentCollected int64 `protobuf:"varint,2,opt,name=rent_collected,json=rentCollected,proto3" json:"rent_collected,omitempty"` -} - -func (x *RentDebits) Reset() { - *x = RentDebits{} - if protoimpl.UnsafeEnabled { - mi := &file_txn_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *RentDebits) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RentDebits) ProtoMessage() {} - -func (x *RentDebits) ProtoReflect() protoreflect.Message { - mi := &file_txn_proto_msgTypes[8] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use RentDebits.ProtoReflect.Descriptor instead. -func (*RentDebits) Descriptor() ([]byte, []int) { - return file_txn_proto_rawDescGZIP(), []int{8} -} - -func (x *RentDebits) GetPubkey() []byte { - if x != nil { - return x.Pubkey + return x.Bank } return nil } -func (x *RentDebits) GetRentCollected() int64 { - if x != nil { - return x.RentCollected - } - return 0 -} - type FeeDetails struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -674,7 +544,7 @@ type FeeDetails struct { func (x *FeeDetails) Reset() { *x = FeeDetails{} if protoimpl.UnsafeEnabled { - mi := &file_txn_proto_msgTypes[9] + mi := &file_txn_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -687,7 +557,7 @@ func (x *FeeDetails) String() string { func (*FeeDetails) ProtoMessage() {} func (x *FeeDetails) ProtoReflect() protoreflect.Message { - mi := &file_txn_proto_msgTypes[9] + mi := &file_txn_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -700,7 +570,7 @@ func (x *FeeDetails) ProtoReflect() protoreflect.Message { // Deprecated: Use FeeDetails.ProtoReflect.Descriptor instead. func (*FeeDetails) Descriptor() ([]byte, []int) { - return file_txn_proto_rawDescGZIP(), []int{9} + return file_txn_proto_rawDescGZIP(), []int{7} } func (x *FeeDetails) GetTransactionFee() uint64 { @@ -727,27 +597,32 @@ type TxnResult struct { Executed bool `protobuf:"varint,1,opt,name=executed,proto3" json:"executed,omitempty"` // Whether there was a sanitization error SanitizationError bool `protobuf:"varint,2,opt,name=sanitization_error,json=sanitizationError,proto3" json:"sanitization_error,omitempty"` - // The state of each account after the transaction - ResultingState *ResultingState `protobuf:"bytes,3,opt,name=resulting_state,json=resultingState,proto3" json:"resulting_state,omitempty"` - Rent uint64 `protobuf:"varint,4,opt,name=rent,proto3" json:"rent,omitempty"` // If an executed transaction has no error IsOk bool `protobuf:"varint,5,opt,name=is_ok,json=isOk,proto3" json:"is_ok,omitempty"` // The transaction status (error code) Status uint32 `protobuf:"varint,6,opt,name=status,proto3" json:"status,omitempty"` + // The instruction error, if any + InstructionError uint32 `protobuf:"varint,7,opt,name=instruction_error,json=instructionError,proto3" json:"instruction_error,omitempty"` + // The instruction error index, if any + InstructionErrorIndex uint32 `protobuf:"varint,8,opt,name=instruction_error_index,json=instructionErrorIndex,proto3" json:"instruction_error_index,omitempty"` + // Custom error, if any + CustomError uint32 `protobuf:"varint,9,opt,name=custom_error,json=customError,proto3" json:"custom_error,omitempty"` // The return data from this transaction, if any - ReturnData []byte `protobuf:"bytes,7,opt,name=return_data,json=returnData,proto3" json:"return_data,omitempty"` + ReturnData []byte `protobuf:"bytes,10,opt,name=return_data,json=returnData,proto3" json:"return_data,omitempty"` // Number of executed compute units - ExecutedUnits uint64 `protobuf:"varint,8,opt,name=executed_units,json=executedUnits,proto3" json:"executed_units,omitempty"` - // The change in accounts data len for this transaction - AccountsDataLenDelta int64 `protobuf:"varint,9,opt,name=accounts_data_len_delta,json=accountsDataLenDelta,proto3" json:"accounts_data_len_delta,omitempty"` + ExecutedUnits uint64 `protobuf:"varint,11,opt,name=executed_units,json=executedUnits,proto3" json:"executed_units,omitempty"` // The collected fees in this transaction - FeeDetails *FeeDetails `protobuf:"bytes,10,opt,name=fee_details,json=feeDetails,proto3" json:"fee_details,omitempty"` + FeeDetails *FeeDetails `protobuf:"bytes,12,opt,name=fee_details,json=feeDetails,proto3" json:"fee_details,omitempty"` + // Loaded accounts data size + LoadedAccountsDataSize uint64 `protobuf:"varint,13,opt,name=loaded_accounts_data_size,json=loadedAccountsDataSize,proto3" json:"loaded_accounts_data_size,omitempty"` + ModifiedAccounts []*AcctState `protobuf:"bytes,14,rep,name=modified_accounts,json=modifiedAccounts,proto3" json:"modified_accounts,omitempty"` + RollbackAccounts []*AcctState `protobuf:"bytes,15,rep,name=rollback_accounts,json=rollbackAccounts,proto3" json:"rollback_accounts,omitempty"` } func (x *TxnResult) Reset() { *x = TxnResult{} if protoimpl.UnsafeEnabled { - mi := &file_txn_proto_msgTypes[10] + mi := &file_txn_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -760,7 +635,7 @@ func (x *TxnResult) String() string { func (*TxnResult) ProtoMessage() {} func (x *TxnResult) ProtoReflect() protoreflect.Message { - mi := &file_txn_proto_msgTypes[10] + mi := &file_txn_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -773,7 +648,7 @@ func (x *TxnResult) ProtoReflect() protoreflect.Message { // Deprecated: Use TxnResult.ProtoReflect.Descriptor instead. func (*TxnResult) Descriptor() ([]byte, []int) { - return file_txn_proto_rawDescGZIP(), []int{10} + return file_txn_proto_rawDescGZIP(), []int{8} } func (x *TxnResult) GetExecuted() bool { @@ -790,30 +665,37 @@ func (x *TxnResult) GetSanitizationError() bool { return false } -func (x *TxnResult) GetResultingState() *ResultingState { +func (x *TxnResult) GetIsOk() bool { if x != nil { - return x.ResultingState + return x.IsOk } - return nil + return false } -func (x *TxnResult) GetRent() uint64 { +func (x *TxnResult) GetStatus() uint32 { if x != nil { - return x.Rent + return x.Status } return 0 } -func (x *TxnResult) GetIsOk() bool { +func (x *TxnResult) GetInstructionError() uint32 { if x != nil { - return x.IsOk + return x.InstructionError } - return false + return 0 } -func (x *TxnResult) GetStatus() uint32 { +func (x *TxnResult) GetInstructionErrorIndex() uint32 { if x != nil { - return x.Status + return x.InstructionErrorIndex + } + return 0 +} + +func (x *TxnResult) GetCustomError() uint32 { + if x != nil { + return x.CustomError } return 0 } @@ -832,16 +714,30 @@ func (x *TxnResult) GetExecutedUnits() uint64 { return 0 } -func (x *TxnResult) GetAccountsDataLenDelta() int64 { +func (x *TxnResult) GetFeeDetails() *FeeDetails { + if x != nil { + return x.FeeDetails + } + return nil +} + +func (x *TxnResult) GetLoadedAccountsDataSize() uint64 { if x != nil { - return x.AccountsDataLenDelta + return x.LoadedAccountsDataSize } return 0 } -func (x *TxnResult) GetFeeDetails() *FeeDetails { +func (x *TxnResult) GetModifiedAccounts() []*AcctState { if x != nil { - return x.FeeDetails + return x.ModifiedAccounts + } + return nil +} + +func (x *TxnResult) GetRollbackAccounts() []*AcctState { + if x != nil { + return x.RollbackAccounts } return nil } @@ -852,16 +748,17 @@ type TxnFixture struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields + Metadata *FixtureMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` // Context - Input *TxnContext `protobuf:"bytes,1,opt,name=input,proto3" json:"input,omitempty"` + Input *TxnContext `protobuf:"bytes,2,opt,name=input,proto3" json:"input,omitempty"` // Effects - Output *TxnResult `protobuf:"bytes,2,opt,name=output,proto3" json:"output,omitempty"` + Output *TxnResult `protobuf:"bytes,3,opt,name=output,proto3" json:"output,omitempty"` } func (x *TxnFixture) Reset() { *x = TxnFixture{} if protoimpl.UnsafeEnabled { - mi := &file_txn_proto_msgTypes[11] + mi := &file_txn_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -874,7 +771,7 @@ func (x *TxnFixture) String() string { func (*TxnFixture) ProtoMessage() {} func (x *TxnFixture) ProtoReflect() protoreflect.Message { - mi := &file_txn_proto_msgTypes[11] + mi := &file_txn_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -887,7 +784,14 @@ func (x *TxnFixture) ProtoReflect() protoreflect.Message { // Deprecated: Use TxnFixture.ProtoReflect.Descriptor instead. func (*TxnFixture) Descriptor() ([]byte, []int) { - return file_txn_proto_rawDescGZIP(), []int{11} + return file_txn_proto_rawDescGZIP(), []int{9} +} + +func (x *TxnFixture) GetMetadata() *FixtureMetadata { + if x != nil { + return x.Metadata + } + return nil } func (x *TxnFixture) GetInput() *TxnContext { @@ -910,6 +814,7 @@ var file_txn_proto_rawDesc = []byte{ 0x0a, 0x09, 0x74, 0x78, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x16, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x1a, 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x1a, 0x0e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xcd, 0x01, 0x0a, 0x0d, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x36, 0x0a, 0x17, 0x6e, 0x75, 0x6d, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, @@ -939,136 +844,142 @@ var file_txn_proto_rawDesc = []byte{ 0x69, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x61, 0x64, 0x6f, 0x6e, 0x6c, 0x79, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x0f, 0x72, 0x65, 0x61, 0x64, 0x6f, 0x6e, 0x6c, - 0x79, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x22, 0x49, 0x0a, 0x0f, 0x4c, 0x6f, 0x61, 0x64, - 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x77, - 0x72, 0x69, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x08, 0x77, - 0x72, 0x69, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x61, 0x64, 0x6f, - 0x6e, 0x6c, 0x79, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x08, 0x72, 0x65, 0x61, 0x64, 0x6f, - 0x6e, 0x6c, 0x79, 0x22, 0x9d, 0x04, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x73, - 0x5f, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, - 0x73, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x12, 0x3d, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, - 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, - 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, - 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x06, - 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0b, 0x61, 0x63, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x51, 0x0a, 0x13, 0x61, 0x63, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x5f, 0x64, 0x61, 0x74, 0x61, - 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, - 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, - 0x41, 0x63, 0x63, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x11, 0x61, 0x63, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x12, 0x29, 0x0a, 0x10, - 0x72, 0x65, 0x63, 0x65, 0x6e, 0x74, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x68, 0x61, 0x73, 0x68, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x72, 0x65, 0x63, 0x65, 0x6e, 0x74, 0x42, 0x6c, - 0x6f, 0x63, 0x6b, 0x68, 0x61, 0x73, 0x68, 0x12, 0x4f, 0x0a, 0x0c, 0x69, 0x6e, 0x73, 0x74, 0x72, - 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, - 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, - 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x49, - 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0c, 0x69, 0x6e, 0x73, 0x74, - 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x65, 0x0a, 0x15, 0x61, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x5f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, - 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, - 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, - 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x54, - 0x61, 0x62, 0x6c, 0x65, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x52, 0x13, 0x61, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x73, 0x12, - 0x52, 0x0a, 0x10, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, - 0x73, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6f, 0x72, 0x67, 0x2e, + 0x79, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x22, 0xf6, 0x02, 0x0a, 0x12, 0x54, 0x72, 0x61, + 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, + 0x1b, 0x0a, 0x09, 0x69, 0x73, 0x5f, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x08, 0x69, 0x73, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x12, 0x3d, 0x0a, 0x06, + 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6f, + 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, + 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x48, 0x65, 0x61, + 0x64, 0x65, 0x72, 0x52, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x61, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x0c, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x29, + 0x0a, 0x10, 0x72, 0x65, 0x63, 0x65, 0x6e, 0x74, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x68, 0x61, + 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x72, 0x65, 0x63, 0x65, 0x6e, 0x74, + 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x68, 0x61, 0x73, 0x68, 0x12, 0x4f, 0x0a, 0x0c, 0x69, 0x6e, 0x73, + 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x2b, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, + 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, + 0x64, 0x49, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0c, 0x69, 0x6e, + 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x65, 0x0a, 0x15, 0x61, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6c, 0x6f, 0x6f, 0x6b, + 0x75, 0x70, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, - 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x65, 0x73, 0x52, 0x0f, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, - 0x73, 0x65, 0x73, 0x22, 0xca, 0x01, 0x0a, 0x14, 0x53, 0x61, 0x6e, 0x69, 0x74, 0x69, 0x7a, 0x65, - 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x44, 0x0a, 0x07, - 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, + 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x52, 0x13, 0x61, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, + 0x73, 0x22, 0x9f, 0x01, 0x0a, 0x14, 0x53, 0x61, 0x6e, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x64, 0x54, + 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x44, 0x0a, 0x07, 0x6d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x6f, 0x72, + 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, + 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x48, + 0x61, 0x73, 0x68, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0a, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, + 0x72, 0x65, 0x73, 0x22, 0xb7, 0x03, 0x0a, 0x07, 0x54, 0x78, 0x6e, 0x42, 0x61, 0x6e, 0x6b, 0x12, + 0x54, 0x0a, 0x0f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x68, 0x61, 0x73, 0x68, 0x5f, 0x71, 0x75, 0x65, + 0x75, 0x65, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, + 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, + 0x31, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x68, 0x61, 0x73, 0x68, 0x51, 0x75, 0x65, 0x75, 0x65, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x68, 0x61, 0x73, 0x68, + 0x51, 0x75, 0x65, 0x75, 0x65, 0x12, 0x3b, 0x0a, 0x1a, 0x72, 0x62, 0x68, 0x5f, 0x6c, 0x61, 0x6d, + 0x70, 0x6f, 0x72, 0x74, 0x73, 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, + 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x17, 0x72, 0x62, 0x68, 0x4c, 0x61, + 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x50, 0x65, 0x72, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, + 0x72, 0x65, 0x12, 0x53, 0x0a, 0x11, 0x66, 0x65, 0x65, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x67, + 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, - 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x68, 0x61, - 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x29, 0x0a, 0x11, 0x69, 0x73, 0x5f, 0x73, 0x69, 0x6d, 0x70, - 0x6c, 0x65, 0x5f, 0x76, 0x6f, 0x74, 0x65, 0x5f, 0x74, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x0e, 0x69, 0x73, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x56, 0x6f, 0x74, 0x65, 0x54, 0x78, - 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x04, - 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0a, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, - 0x22, 0x8f, 0x02, 0x0a, 0x0a, 0x54, 0x78, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, - 0x3c, 0x0a, 0x02, 0x74, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x6f, 0x72, + 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x47, 0x6f, + 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x52, 0x0f, 0x66, 0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x47, + 0x6f, 0x76, 0x65, 0x72, 0x6e, 0x6f, 0x72, 0x12, 0x2a, 0x0a, 0x11, 0x74, 0x6f, 0x74, 0x61, 0x6c, + 0x5f, 0x65, 0x70, 0x6f, 0x63, 0x68, 0x5f, 0x73, 0x74, 0x61, 0x6b, 0x65, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x0f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x45, 0x70, 0x6f, 0x63, 0x68, 0x53, 0x74, + 0x61, 0x6b, 0x65, 0x12, 0x4c, 0x0a, 0x0e, 0x65, 0x70, 0x6f, 0x63, 0x68, 0x5f, 0x73, 0x63, 0x68, + 0x65, 0x64, 0x75, 0x6c, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, - 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x61, 0x6e, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x64, 0x54, 0x72, - 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x02, 0x74, 0x78, 0x12, 0x17, 0x0a, - 0x07, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, - 0x6d, 0x61, 0x78, 0x41, 0x67, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x68, - 0x61, 0x73, 0x68, 0x5f, 0x71, 0x75, 0x65, 0x75, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, - 0x0e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x68, 0x61, 0x73, 0x68, 0x51, 0x75, 0x65, 0x75, 0x65, 0x12, - 0x41, 0x0a, 0x09, 0x65, 0x70, 0x6f, 0x63, 0x68, 0x5f, 0x63, 0x74, 0x78, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, - 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x70, 0x6f, 0x63, - 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x08, 0x65, 0x70, 0x6f, 0x63, 0x68, 0x43, - 0x74, 0x78, 0x12, 0x3e, 0x0a, 0x08, 0x73, 0x6c, 0x6f, 0x74, 0x5f, 0x63, 0x74, 0x78, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, - 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x6c, - 0x6f, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x07, 0x73, 0x6c, 0x6f, 0x74, 0x43, - 0x74, 0x78, 0x22, 0xc4, 0x01, 0x0a, 0x0e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x69, 0x6e, 0x67, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x42, 0x0a, 0x0b, 0x61, 0x63, 0x63, 0x74, 0x5f, 0x73, 0x74, - 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6f, 0x72, 0x67, - 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, - 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x63, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0a, 0x61, - 0x63, 0x63, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x12, 0x43, 0x0a, 0x0b, 0x72, 0x65, 0x6e, - 0x74, 0x5f, 0x64, 0x65, 0x62, 0x69, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, - 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, - 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6e, 0x74, 0x44, 0x65, 0x62, 0x69, - 0x74, 0x73, 0x52, 0x0a, 0x72, 0x65, 0x6e, 0x74, 0x44, 0x65, 0x62, 0x69, 0x74, 0x73, 0x12, 0x29, - 0x0a, 0x10, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, - 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x6e, 0x74, 0x22, 0x4b, 0x0a, 0x0a, 0x52, 0x65, 0x6e, - 0x74, 0x44, 0x65, 0x62, 0x69, 0x74, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x12, - 0x25, 0x0a, 0x0e, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65, - 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x72, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x22, 0x64, 0x0a, 0x0a, 0x46, 0x65, 0x65, 0x44, 0x65, 0x74, - 0x61, 0x69, 0x6c, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x5f, 0x66, 0x65, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0e, 0x74, - 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x65, 0x65, 0x12, 0x2d, 0x0a, - 0x12, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, - 0x66, 0x65, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x11, 0x70, 0x72, 0x69, 0x6f, 0x72, - 0x69, 0x74, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x65, 0x65, 0x22, 0xac, 0x03, 0x0a, - 0x09, 0x54, 0x78, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x78, - 0x65, 0x63, 0x75, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x65, 0x78, - 0x65, 0x63, 0x75, 0x74, 0x65, 0x64, 0x12, 0x2d, 0x0a, 0x12, 0x73, 0x61, 0x6e, 0x69, 0x74, 0x69, - 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x11, 0x73, 0x61, 0x6e, 0x69, 0x74, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x4f, 0x0a, 0x0f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x69, - 0x6e, 0x67, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, - 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, - 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x69, 0x6e, - 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x69, 0x6e, - 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x13, 0x0a, 0x05, 0x69, 0x73, - 0x5f, 0x6f, 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x69, 0x73, 0x4f, 0x6b, 0x12, - 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x74, 0x75, 0x72, - 0x6e, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x72, 0x65, - 0x74, 0x75, 0x72, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x12, 0x25, 0x0a, 0x0e, 0x65, 0x78, 0x65, 0x63, - 0x75, 0x74, 0x65, 0x64, 0x5f, 0x75, 0x6e, 0x69, 0x74, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x0d, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x64, 0x55, 0x6e, 0x69, 0x74, 0x73, 0x12, - 0x35, 0x0a, 0x17, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x5f, 0x64, 0x61, 0x74, 0x61, - 0x5f, 0x6c, 0x65, 0x6e, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x14, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x44, 0x61, 0x74, 0x61, 0x4c, 0x65, - 0x6e, 0x44, 0x65, 0x6c, 0x74, 0x61, 0x12, 0x43, 0x0a, 0x0b, 0x66, 0x65, 0x65, 0x5f, 0x64, 0x65, - 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6f, 0x72, + 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x70, 0x6f, 0x63, 0x68, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, + 0x6c, 0x65, 0x52, 0x0d, 0x65, 0x70, 0x6f, 0x63, 0x68, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, + 0x65, 0x12, 0x3e, 0x0a, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, + 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x65, 0x61, + 0x74, 0x75, 0x72, 0x65, 0x53, 0x65, 0x74, 0x52, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x73, 0x4a, 0x04, 0x08, 0x06, 0x10, 0x07, 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, 0x22, 0xe4, 0x01, + 0x0a, 0x0a, 0x54, 0x78, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x3c, 0x0a, 0x02, + 0x74, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, + 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, + 0x31, 0x2e, 0x53, 0x61, 0x6e, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x02, 0x74, 0x78, 0x12, 0x51, 0x0a, 0x13, 0x61, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x5f, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, + 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, + 0x2e, 0x41, 0x63, 0x63, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x11, 0x61, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x12, 0x33, 0x0a, + 0x04, 0x62, 0x61, 0x6e, 0x6b, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, - 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x65, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, - 0x0a, 0x66, 0x65, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x81, 0x01, 0x0a, 0x0a, - 0x54, 0x78, 0x6e, 0x46, 0x69, 0x78, 0x74, 0x75, 0x72, 0x65, 0x12, 0x38, 0x0a, 0x05, 0x69, 0x6e, - 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6f, 0x72, 0x67, 0x2e, - 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, - 0x76, 0x31, 0x2e, 0x54, 0x78, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x05, 0x69, - 0x6e, 0x70, 0x75, 0x74, 0x12, 0x39, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, - 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x78, - 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x42, - 0x0f, 0x5a, 0x0d, 0x2e, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x78, 0x6e, 0x42, 0x61, 0x6e, 0x6b, 0x52, 0x04, 0x62, 0x61, + 0x6e, 0x6b, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x4a, 0x04, + 0x08, 0x05, 0x10, 0x06, 0x22, 0x64, 0x0a, 0x0a, 0x46, 0x65, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, + 0x6c, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x66, 0x65, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0e, 0x74, 0x72, 0x61, + 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x65, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x70, + 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x66, 0x65, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x11, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, + 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x65, 0x65, 0x22, 0xff, 0x04, 0x0a, 0x09, 0x54, + 0x78, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x78, 0x65, 0x63, + 0x75, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x65, 0x78, 0x65, 0x63, + 0x75, 0x74, 0x65, 0x64, 0x12, 0x2d, 0x0a, 0x12, 0x73, 0x61, 0x6e, 0x69, 0x74, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x11, 0x73, 0x61, 0x6e, 0x69, 0x74, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, + 0x72, 0x6f, 0x72, 0x12, 0x13, 0x0a, 0x05, 0x69, 0x73, 0x5f, 0x6f, 0x6b, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x04, 0x69, 0x73, 0x4f, 0x6b, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x12, 0x2b, 0x0a, 0x11, 0x69, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x10, 0x69, 0x6e, 0x73, + 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x36, 0x0a, + 0x17, 0x69, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x15, + 0x69, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, + 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x63, 0x75, 0x73, + 0x74, 0x6f, 0x6d, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x74, 0x75, + 0x72, 0x6e, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x72, + 0x65, 0x74, 0x75, 0x72, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x12, 0x25, 0x0a, 0x0e, 0x65, 0x78, 0x65, + 0x63, 0x75, 0x74, 0x65, 0x64, 0x5f, 0x75, 0x6e, 0x69, 0x74, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x0d, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x64, 0x55, 0x6e, 0x69, 0x74, 0x73, + 0x12, 0x43, 0x0a, 0x0b, 0x66, 0x65, 0x65, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, + 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, + 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x46, + 0x65, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x0a, 0x66, 0x65, 0x65, 0x44, 0x65, + 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x39, 0x0a, 0x19, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x5f, + 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x73, 0x69, + 0x7a, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x04, 0x52, 0x16, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, + 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x44, 0x61, 0x74, 0x61, 0x53, 0x69, 0x7a, 0x65, + 0x12, 0x4e, 0x0a, 0x11, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x18, 0x0e, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6f, 0x72, + 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, + 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x63, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x10, + 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, + 0x12, 0x4e, 0x0a, 0x11, 0x72, 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x5f, 0x61, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6f, 0x72, + 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, + 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x63, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x10, + 0x72, 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, + 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0xc6, 0x01, 0x0a, + 0x0a, 0x54, 0x78, 0x6e, 0x46, 0x69, 0x78, 0x74, 0x75, 0x72, 0x65, 0x12, 0x43, 0x0a, 0x08, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, + 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, + 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x78, 0x74, 0x75, 0x72, 0x65, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x12, 0x38, 0x0a, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x22, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, + 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x78, 0x6e, 0x43, 0x6f, 0x6e, 0x74, + 0x65, 0x78, 0x74, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x39, 0x0a, 0x06, 0x6f, 0x75, + 0x74, 0x70, 0x75, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6f, 0x72, 0x67, + 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, + 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x78, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x06, 0x6f, + 0x75, 0x74, 0x70, 0x75, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1083,45 +994,48 @@ func file_txn_proto_rawDescGZIP() []byte { return file_txn_proto_rawDescData } -var file_txn_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_txn_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_txn_proto_goTypes = []any{ (*MessageHeader)(nil), // 0: org.solana.sealevel.v1.MessageHeader (*CompiledInstruction)(nil), // 1: org.solana.sealevel.v1.CompiledInstruction (*MessageAddressTableLookup)(nil), // 2: org.solana.sealevel.v1.MessageAddressTableLookup - (*LoadedAddresses)(nil), // 3: org.solana.sealevel.v1.LoadedAddresses - (*TransactionMessage)(nil), // 4: org.solana.sealevel.v1.TransactionMessage - (*SanitizedTransaction)(nil), // 5: org.solana.sealevel.v1.SanitizedTransaction + (*TransactionMessage)(nil), // 3: org.solana.sealevel.v1.TransactionMessage + (*SanitizedTransaction)(nil), // 4: org.solana.sealevel.v1.SanitizedTransaction + (*TxnBank)(nil), // 5: org.solana.sealevel.v1.TxnBank (*TxnContext)(nil), // 6: org.solana.sealevel.v1.TxnContext - (*ResultingState)(nil), // 7: org.solana.sealevel.v1.ResultingState - (*RentDebits)(nil), // 8: org.solana.sealevel.v1.RentDebits - (*FeeDetails)(nil), // 9: org.solana.sealevel.v1.FeeDetails - (*TxnResult)(nil), // 10: org.solana.sealevel.v1.TxnResult - (*TxnFixture)(nil), // 11: org.solana.sealevel.v1.TxnFixture - (*AcctState)(nil), // 12: org.solana.sealevel.v1.AcctState - (*EpochContext)(nil), // 13: org.solana.sealevel.v1.EpochContext - (*SlotContext)(nil), // 14: org.solana.sealevel.v1.SlotContext + (*FeeDetails)(nil), // 7: org.solana.sealevel.v1.FeeDetails + (*TxnResult)(nil), // 8: org.solana.sealevel.v1.TxnResult + (*TxnFixture)(nil), // 9: org.solana.sealevel.v1.TxnFixture + (*BlockhashQueueEntry)(nil), // 10: org.solana.sealevel.v1.BlockhashQueueEntry + (*FeeRateGovernor)(nil), // 11: org.solana.sealevel.v1.FeeRateGovernor + (*EpochSchedule)(nil), // 12: org.solana.sealevel.v1.EpochSchedule + (*FeatureSet)(nil), // 13: org.solana.sealevel.v1.FeatureSet + (*AcctState)(nil), // 14: org.solana.sealevel.v1.AcctState + (*FixtureMetadata)(nil), // 15: org.solana.sealevel.v1.FixtureMetadata } var file_txn_proto_depIdxs = []int32{ 0, // 0: org.solana.sealevel.v1.TransactionMessage.header:type_name -> org.solana.sealevel.v1.MessageHeader - 12, // 1: org.solana.sealevel.v1.TransactionMessage.account_shared_data:type_name -> org.solana.sealevel.v1.AcctState - 1, // 2: org.solana.sealevel.v1.TransactionMessage.instructions:type_name -> org.solana.sealevel.v1.CompiledInstruction - 2, // 3: org.solana.sealevel.v1.TransactionMessage.address_table_lookups:type_name -> org.solana.sealevel.v1.MessageAddressTableLookup - 3, // 4: org.solana.sealevel.v1.TransactionMessage.loaded_addresses:type_name -> org.solana.sealevel.v1.LoadedAddresses - 4, // 5: org.solana.sealevel.v1.SanitizedTransaction.message:type_name -> org.solana.sealevel.v1.TransactionMessage - 5, // 6: org.solana.sealevel.v1.TxnContext.tx:type_name -> org.solana.sealevel.v1.SanitizedTransaction - 13, // 7: org.solana.sealevel.v1.TxnContext.epoch_ctx:type_name -> org.solana.sealevel.v1.EpochContext - 14, // 8: org.solana.sealevel.v1.TxnContext.slot_ctx:type_name -> org.solana.sealevel.v1.SlotContext - 12, // 9: org.solana.sealevel.v1.ResultingState.acct_states:type_name -> org.solana.sealevel.v1.AcctState - 8, // 10: org.solana.sealevel.v1.ResultingState.rent_debits:type_name -> org.solana.sealevel.v1.RentDebits - 7, // 11: org.solana.sealevel.v1.TxnResult.resulting_state:type_name -> org.solana.sealevel.v1.ResultingState - 9, // 12: org.solana.sealevel.v1.TxnResult.fee_details:type_name -> org.solana.sealevel.v1.FeeDetails - 6, // 13: org.solana.sealevel.v1.TxnFixture.input:type_name -> org.solana.sealevel.v1.TxnContext - 10, // 14: org.solana.sealevel.v1.TxnFixture.output:type_name -> org.solana.sealevel.v1.TxnResult - 15, // [15:15] is the sub-list for method output_type - 15, // [15:15] is the sub-list for method input_type - 15, // [15:15] is the sub-list for extension type_name - 15, // [15:15] is the sub-list for extension extendee - 0, // [0:15] is the sub-list for field type_name + 1, // 1: org.solana.sealevel.v1.TransactionMessage.instructions:type_name -> org.solana.sealevel.v1.CompiledInstruction + 2, // 2: org.solana.sealevel.v1.TransactionMessage.address_table_lookups:type_name -> org.solana.sealevel.v1.MessageAddressTableLookup + 3, // 3: org.solana.sealevel.v1.SanitizedTransaction.message:type_name -> org.solana.sealevel.v1.TransactionMessage + 10, // 4: org.solana.sealevel.v1.TxnBank.blockhash_queue:type_name -> org.solana.sealevel.v1.BlockhashQueueEntry + 11, // 5: org.solana.sealevel.v1.TxnBank.fee_rate_governor:type_name -> org.solana.sealevel.v1.FeeRateGovernor + 12, // 6: org.solana.sealevel.v1.TxnBank.epoch_schedule:type_name -> org.solana.sealevel.v1.EpochSchedule + 13, // 7: org.solana.sealevel.v1.TxnBank.features:type_name -> org.solana.sealevel.v1.FeatureSet + 4, // 8: org.solana.sealevel.v1.TxnContext.tx:type_name -> org.solana.sealevel.v1.SanitizedTransaction + 14, // 9: org.solana.sealevel.v1.TxnContext.account_shared_data:type_name -> org.solana.sealevel.v1.AcctState + 5, // 10: org.solana.sealevel.v1.TxnContext.bank:type_name -> org.solana.sealevel.v1.TxnBank + 7, // 11: org.solana.sealevel.v1.TxnResult.fee_details:type_name -> org.solana.sealevel.v1.FeeDetails + 14, // 12: org.solana.sealevel.v1.TxnResult.modified_accounts:type_name -> org.solana.sealevel.v1.AcctState + 14, // 13: org.solana.sealevel.v1.TxnResult.rollback_accounts:type_name -> org.solana.sealevel.v1.AcctState + 15, // 14: org.solana.sealevel.v1.TxnFixture.metadata:type_name -> org.solana.sealevel.v1.FixtureMetadata + 6, // 15: org.solana.sealevel.v1.TxnFixture.input:type_name -> org.solana.sealevel.v1.TxnContext + 8, // 16: org.solana.sealevel.v1.TxnFixture.output:type_name -> org.solana.sealevel.v1.TxnResult + 17, // [17:17] is the sub-list for method output_type + 17, // [17:17] is the sub-list for method input_type + 17, // [17:17] is the sub-list for extension type_name + 17, // [17:17] is the sub-list for extension extendee + 0, // [0:17] is the sub-list for field type_name } func init() { file_txn_proto_init() } @@ -1130,6 +1044,7 @@ func file_txn_proto_init() { return } file_context_proto_init() + file_metadata_proto_init() if !protoimpl.UnsafeEnabled { file_txn_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*MessageHeader); i { @@ -1168,7 +1083,7 @@ func file_txn_proto_init() { } } file_txn_proto_msgTypes[3].Exporter = func(v any, i int) any { - switch v := v.(*LoadedAddresses); i { + switch v := v.(*TransactionMessage); i { case 0: return &v.state case 1: @@ -1180,7 +1095,7 @@ func file_txn_proto_init() { } } file_txn_proto_msgTypes[4].Exporter = func(v any, i int) any { - switch v := v.(*TransactionMessage); i { + switch v := v.(*SanitizedTransaction); i { case 0: return &v.state case 1: @@ -1192,7 +1107,7 @@ func file_txn_proto_init() { } } file_txn_proto_msgTypes[5].Exporter = func(v any, i int) any { - switch v := v.(*SanitizedTransaction); i { + switch v := v.(*TxnBank); i { case 0: return &v.state case 1: @@ -1216,30 +1131,6 @@ func file_txn_proto_init() { } } file_txn_proto_msgTypes[7].Exporter = func(v any, i int) any { - switch v := v.(*ResultingState); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_txn_proto_msgTypes[8].Exporter = func(v any, i int) any { - switch v := v.(*RentDebits); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_txn_proto_msgTypes[9].Exporter = func(v any, i int) any { switch v := v.(*FeeDetails); i { case 0: return &v.state @@ -1251,7 +1142,7 @@ func file_txn_proto_init() { return nil } } - file_txn_proto_msgTypes[10].Exporter = func(v any, i int) any { + file_txn_proto_msgTypes[8].Exporter = func(v any, i int) any { switch v := v.(*TxnResult); i { case 0: return &v.state @@ -1263,7 +1154,7 @@ func file_txn_proto_init() { return nil } } - file_txn_proto_msgTypes[11].Exporter = func(v any, i int) any { + file_txn_proto_msgTypes[9].Exporter = func(v any, i int) any { switch v := v.(*TxnFixture); i { case 0: return &v.state @@ -1282,7 +1173,7 @@ func file_txn_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_txn_proto_rawDesc, NumEnums: 0, - NumMessages: 12, + NumMessages: 10, NumExtensions: 0, NumServices: 0, }, diff --git a/conformance/vm.pb.go b/conformance/vm.pb.go index 901db5f1..a4e68ed7 100644 --- a/conformance/vm.pb.go +++ b/conformance/vm.pb.go @@ -20,6 +20,60 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// We are only concerned with these error kinds as the syscall/VM fuzzers don't +// hit higher level error kinds (e.g., transaction errors) +type ErrKind int32 + +const ( + ErrKind_UNSPECIFIED ErrKind = 0 + ErrKind_EBPF ErrKind = 1 + ErrKind_SYSCALL ErrKind = 2 + ErrKind_INSTRUCTION ErrKind = 3 +) + +// Enum value maps for ErrKind. +var ( + ErrKind_name = map[int32]string{ + 0: "UNSPECIFIED", + 1: "EBPF", + 2: "SYSCALL", + 3: "INSTRUCTION", + } + ErrKind_value = map[string]int32{ + "UNSPECIFIED": 0, + "EBPF": 1, + "SYSCALL": 2, + "INSTRUCTION": 3, + } +) + +func (x ErrKind) Enum() *ErrKind { + p := new(ErrKind) + *p = x + return p +} + +func (x ErrKind) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ErrKind) Descriptor() protoreflect.EnumDescriptor { + return file_vm_proto_enumTypes[0].Descriptor() +} + +func (ErrKind) Type() protoreflect.EnumType { + return &file_vm_proto_enumTypes[0] +} + +func (x ErrKind) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ErrKind.Descriptor instead. +func (ErrKind) EnumDescriptor() ([]byte, []int) { + return file_vm_proto_rawDescGZIP(), []int{0} +} + // Describes an input data region. Agave's memory mapping sets up a series of // memory mapped regions, which combine to make the input data region. type InputDataRegion struct { @@ -102,13 +156,6 @@ type VmContext struct { HeapMax uint64 `protobuf:"varint,1,opt,name=heap_max,json=heapMax,proto3" json:"heap_max,omitempty"` // Program read-only data Rodata []byte `protobuf:"bytes,2,opt,name=rodata,proto3" json:"rodata,omitempty"` - // Offset of the text section from the start of the program rodata segment - // (0x100000000) - RodataTextSectionOffset uint64 `protobuf:"varint,3,opt,name=rodata_text_section_offset,json=rodataTextSectionOffset,proto3" json:"rodata_text_section_offset,omitempty"` - // Length of the text section in the program rodata region, in bytes. - RodataTextSectionLength uint64 `protobuf:"varint,4,opt,name=rodata_text_section_length,json=rodataTextSectionLength,proto3" json:"rodata_text_section_length,omitempty"` - // The input data regions - InputDataRegions []*InputDataRegion `protobuf:"bytes,5,rep,name=input_data_regions,json=inputDataRegions,proto3" json:"input_data_regions,omitempty"` // Registers R0 uint64 `protobuf:"varint,6,opt,name=r0,proto3" json:"r0,omitempty"` R1 uint64 `protobuf:"varint,7,opt,name=r1,proto3" json:"r1,omitempty"` @@ -122,6 +169,14 @@ type VmContext struct { R9 uint64 `protobuf:"varint,15,opt,name=r9,proto3" json:"r9,omitempty"` R10 uint64 `protobuf:"varint,16,opt,name=r10,proto3" json:"r10,omitempty"` R11 uint64 `protobuf:"varint,17,opt,name=r11,proto3" json:"r11,omitempty"` + // for vm execution + EntryPc uint64 `protobuf:"varint,20,opt,name=entry_pc,json=entryPc,proto3" json:"entry_pc,omitempty"` + // Bitset of valid call destinations (in terms of pc). + // This model is used by the Firedancer VM for CALL_IMMs + CallWhitelist []byte `protobuf:"bytes,21,opt,name=call_whitelist,json=callWhitelist,proto3" json:"call_whitelist,omitempty"` + ReturnData *ReturnData `protobuf:"bytes,23,opt,name=return_data,json=returnData,proto3" json:"return_data,omitempty"` + // SBPF version + SbpfVersion uint32 `protobuf:"varint,24,opt,name=sbpf_version,json=sbpfVersion,proto3" json:"sbpf_version,omitempty"` } func (x *VmContext) Reset() { @@ -170,27 +225,6 @@ func (x *VmContext) GetRodata() []byte { return nil } -func (x *VmContext) GetRodataTextSectionOffset() uint64 { - if x != nil { - return x.RodataTextSectionOffset - } - return 0 -} - -func (x *VmContext) GetRodataTextSectionLength() uint64 { - if x != nil { - return x.RodataTextSectionLength - } - return 0 -} - -func (x *VmContext) GetInputDataRegions() []*InputDataRegion { - if x != nil { - return x.InputDataRegions - } - return nil -} - func (x *VmContext) GetR0() uint64 { if x != nil { return x.R0 @@ -275,6 +309,34 @@ func (x *VmContext) GetR11() uint64 { return 0 } +func (x *VmContext) GetEntryPc() uint64 { + if x != nil { + return x.EntryPc + } + return 0 +} + +func (x *VmContext) GetCallWhitelist() []byte { + if x != nil { + return x.CallWhitelist + } + return nil +} + +func (x *VmContext) GetReturnData() *ReturnData { + if x != nil { + return x.ReturnData + } + return nil +} + +func (x *VmContext) GetSbpfVersion() uint32 { + if x != nil { + return x.SbpfVersion + } + return 0 +} + // A single invocation of a syscall type SyscallInvocation struct { state protoimpl.MessageState @@ -285,6 +347,8 @@ type SyscallInvocation struct { FunctionName []byte `protobuf:"bytes,1,opt,name=function_name,json=functionName,proto3" json:"function_name,omitempty"` // The initial portion of the heap, for example to store syscall inputs HeapPrefix []byte `protobuf:"bytes,2,opt,name=heap_prefix,json=heapPrefix,proto3" json:"heap_prefix,omitempty"` + // The initial portion of the stack, for example to store syscall inputs + StackPrefix []byte `protobuf:"bytes,3,opt,name=stack_prefix,json=stackPrefix,proto3" json:"stack_prefix,omitempty"` } func (x *SyscallInvocation) Reset() { @@ -333,6 +397,13 @@ func (x *SyscallInvocation) GetHeapPrefix() []byte { return nil } +func (x *SyscallInvocation) GetStackPrefix() []byte { + if x != nil { + return x.StackPrefix + } + return nil +} + // Execution context for a VM Syscall execution. type SyscallContext struct { state protoimpl.MessageState @@ -406,18 +477,34 @@ type SyscallEffects struct { // EBPF error code, if the invocation was unsuccessful Error int64 `protobuf:"varint,1,opt,name=error,proto3" json:"error,omitempty"` + // Error Kind (should be used along with error code) + ErrorKind ErrKind `protobuf:"varint,12,opt,name=error_kind,json=errorKind,proto3,enum=org.solana.sealevel.v1.ErrKind" json:"error_kind,omitempty"` // Registers R0 uint64 `protobuf:"varint,2,opt,name=r0,proto3" json:"r0,omitempty"` // Result of a successful execution // CU's remaining CuAvail uint64 `protobuf:"varint,3,opt,name=cu_avail,json=cuAvail,proto3" json:"cu_avail,omitempty"` // Memory regions - Heap []byte `protobuf:"bytes,4,opt,name=heap,proto3" json:"heap,omitempty"` - Stack []byte `protobuf:"bytes,5,opt,name=stack,proto3" json:"stack,omitempty"` - Inputdata []byte `protobuf:"bytes,6,opt,name=inputdata,proto3" json:"inputdata,omitempty"` + Heap []byte `protobuf:"bytes,4,opt,name=heap,proto3" json:"heap,omitempty"` + Stack []byte `protobuf:"bytes,5,opt,name=stack,proto3" json:"stack,omitempty"` + InputDataRegions []*InputDataRegion `protobuf:"bytes,11,rep,name=input_data_regions,json=inputDataRegions,proto3" json:"input_data_regions,omitempty"` // Current number of stack frames pushed FrameCount uint64 `protobuf:"varint,7,opt,name=frame_count,json=frameCount,proto3" json:"frame_count,omitempty"` // Syscall log - Log []byte `protobuf:"bytes,8,opt,name=log,proto3" json:"log,omitempty"` + Log []byte `protobuf:"bytes,8,opt,name=log,proto3" json:"log,omitempty"` + Rodata []byte `protobuf:"bytes,9,opt,name=rodata,proto3" json:"rodata,omitempty"` + // VM state + Pc uint64 `protobuf:"varint,10,opt,name=pc,proto3" json:"pc,omitempty"` + // Output registers (to test interpreter) + R1 uint64 `protobuf:"varint,107,opt,name=r1,proto3" json:"r1,omitempty"` + R2 uint64 `protobuf:"varint,108,opt,name=r2,proto3" json:"r2,omitempty"` + R3 uint64 `protobuf:"varint,109,opt,name=r3,proto3" json:"r3,omitempty"` + R4 uint64 `protobuf:"varint,110,opt,name=r4,proto3" json:"r4,omitempty"` + R5 uint64 `protobuf:"varint,111,opt,name=r5,proto3" json:"r5,omitempty"` + R6 uint64 `protobuf:"varint,112,opt,name=r6,proto3" json:"r6,omitempty"` + R7 uint64 `protobuf:"varint,113,opt,name=r7,proto3" json:"r7,omitempty"` + R8 uint64 `protobuf:"varint,114,opt,name=r8,proto3" json:"r8,omitempty"` + R9 uint64 `protobuf:"varint,115,opt,name=r9,proto3" json:"r9,omitempty"` + R10 uint64 `protobuf:"varint,116,opt,name=r10,proto3" json:"r10,omitempty"` } func (x *SyscallEffects) Reset() { @@ -459,6 +546,13 @@ func (x *SyscallEffects) GetError() int64 { return 0 } +func (x *SyscallEffects) GetErrorKind() ErrKind { + if x != nil { + return x.ErrorKind + } + return ErrKind_UNSPECIFIED +} + func (x *SyscallEffects) GetR0() uint64 { if x != nil { return x.R0 @@ -487,9 +581,9 @@ func (x *SyscallEffects) GetStack() []byte { return nil } -func (x *SyscallEffects) GetInputdata() []byte { +func (x *SyscallEffects) GetInputDataRegions() []*InputDataRegion { if x != nil { - return x.Inputdata + return x.InputDataRegions } return nil } @@ -508,14 +602,99 @@ func (x *SyscallEffects) GetLog() []byte { return nil } +func (x *SyscallEffects) GetRodata() []byte { + if x != nil { + return x.Rodata + } + return nil +} + +func (x *SyscallEffects) GetPc() uint64 { + if x != nil { + return x.Pc + } + return 0 +} + +func (x *SyscallEffects) GetR1() uint64 { + if x != nil { + return x.R1 + } + return 0 +} + +func (x *SyscallEffects) GetR2() uint64 { + if x != nil { + return x.R2 + } + return 0 +} + +func (x *SyscallEffects) GetR3() uint64 { + if x != nil { + return x.R3 + } + return 0 +} + +func (x *SyscallEffects) GetR4() uint64 { + if x != nil { + return x.R4 + } + return 0 +} + +func (x *SyscallEffects) GetR5() uint64 { + if x != nil { + return x.R5 + } + return 0 +} + +func (x *SyscallEffects) GetR6() uint64 { + if x != nil { + return x.R6 + } + return 0 +} + +func (x *SyscallEffects) GetR7() uint64 { + if x != nil { + return x.R7 + } + return 0 +} + +func (x *SyscallEffects) GetR8() uint64 { + if x != nil { + return x.R8 + } + return 0 +} + +func (x *SyscallEffects) GetR9() uint64 { + if x != nil { + return x.R9 + } + return 0 +} + +func (x *SyscallEffects) GetR10() uint64 { + if x != nil { + return x.R10 + } + return 0 +} + // A syscall processing test fixture. type SyscallFixture struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Input *SyscallContext `protobuf:"bytes,1,opt,name=input,proto3" json:"input,omitempty"` - Output *SyscallEffects `protobuf:"bytes,2,opt,name=output,proto3" json:"output,omitempty"` + Metadata *FixtureMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + Input *SyscallContext `protobuf:"bytes,2,opt,name=input,proto3" json:"input,omitempty"` + Output *SyscallEffects `protobuf:"bytes,3,opt,name=output,proto3" json:"output,omitempty"` } func (x *SyscallFixture) Reset() { @@ -550,6 +729,13 @@ func (*SyscallFixture) Descriptor() ([]byte, []int) { return file_vm_proto_rawDescGZIP(), []int{5} } +func (x *SyscallFixture) GetMetadata() *FixtureMetadata { + if x != nil { + return x.Metadata + } + return nil +} + func (x *SyscallFixture) GetInput() *SyscallContext { if x != nil { return x.Input @@ -684,8 +870,9 @@ type ValidateVmFixture struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Input *FullVmContext `protobuf:"bytes,1,opt,name=input,proto3" json:"input,omitempty"` - Output *ValidateVmEffects `protobuf:"bytes,2,opt,name=output,proto3" json:"output,omitempty"` + Metadata *FixtureMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + Input *FullVmContext `protobuf:"bytes,2,opt,name=input,proto3" json:"input,omitempty"` + Output *ValidateVmEffects `protobuf:"bytes,3,opt,name=output,proto3" json:"output,omitempty"` } func (x *ValidateVmFixture) Reset() { @@ -720,6 +907,13 @@ func (*ValidateVmFixture) Descriptor() ([]byte, []int) { return file_vm_proto_rawDescGZIP(), []int{8} } +func (x *ValidateVmFixture) GetMetadata() *FixtureMetadata { + if x != nil { + return x.Metadata + } + return nil +} + func (x *ValidateVmFixture) GetInput() *FullVmContext { if x != nil { return x.Input @@ -734,116 +928,209 @@ func (x *ValidateVmFixture) GetOutput() *ValidateVmEffects { return nil } +type ReturnData struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ProgramId []byte `protobuf:"bytes,1,opt,name=program_id,json=programId,proto3" json:"program_id,omitempty"` + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` +} + +func (x *ReturnData) Reset() { + *x = ReturnData{} + if protoimpl.UnsafeEnabled { + mi := &file_vm_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ReturnData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReturnData) ProtoMessage() {} + +func (x *ReturnData) ProtoReflect() protoreflect.Message { + mi := &file_vm_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReturnData.ProtoReflect.Descriptor instead. +func (*ReturnData) Descriptor() ([]byte, []int) { + return file_vm_proto_rawDescGZIP(), []int{9} +} + +func (x *ReturnData) GetProgramId() []byte { + if x != nil { + return x.ProgramId + } + return nil +} + +func (x *ReturnData) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + var File_vm_proto protoreflect.FileDescriptor var file_vm_proto_rawDesc = []byte{ 0x0a, 0x08, 0x76, 0x6d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x16, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x1a, 0x0c, 0x69, 0x6e, 0x76, 0x6f, 0x6b, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x1a, 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, + 0x1a, 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, + 0x0e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x64, 0x0a, 0x0f, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x73, 0x5f, 0x77, 0x72, 0x69, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, 0x73, 0x57, 0x72, 0x69, - 0x74, 0x61, 0x62, 0x6c, 0x65, 0x22, 0xd3, 0x03, 0x0a, 0x09, 0x56, 0x6d, 0x43, 0x6f, 0x6e, 0x74, + 0x74, 0x61, 0x62, 0x6c, 0x65, 0x22, 0xbe, 0x03, 0x0a, 0x09, 0x56, 0x6d, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x68, 0x65, 0x61, 0x70, 0x5f, 0x6d, 0x61, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x68, 0x65, 0x61, 0x70, 0x4d, 0x61, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, - 0x72, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x12, 0x3b, 0x0a, 0x1a, 0x72, 0x6f, 0x64, 0x61, 0x74, 0x61, - 0x5f, 0x74, 0x65, 0x78, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6f, 0x66, - 0x66, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x17, 0x72, 0x6f, 0x64, 0x61, - 0x74, 0x61, 0x54, 0x65, 0x78, 0x74, 0x53, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x66, 0x66, - 0x73, 0x65, 0x74, 0x12, 0x3b, 0x0a, 0x1a, 0x72, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x74, 0x65, - 0x78, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, - 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x17, 0x72, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x54, - 0x65, 0x78, 0x74, 0x53, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, - 0x12, 0x55, 0x0a, 0x12, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x72, - 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6f, - 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, - 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x44, 0x61, 0x74, 0x61, 0x52, - 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x52, 0x10, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x44, 0x61, 0x74, 0x61, - 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x30, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x02, 0x72, 0x30, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x31, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x02, 0x72, 0x31, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x32, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x02, 0x72, 0x32, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x33, 0x18, 0x09, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x02, 0x72, 0x33, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x34, 0x18, 0x0a, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x02, 0x72, 0x34, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x35, 0x18, 0x0b, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x02, 0x72, 0x35, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x36, 0x18, 0x0c, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x02, 0x72, 0x36, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x37, 0x18, 0x0d, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x02, 0x72, 0x37, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x38, 0x18, 0x0e, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x02, 0x72, 0x38, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x39, 0x18, 0x0f, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x02, 0x72, 0x39, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x31, 0x30, 0x18, 0x10, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x72, 0x31, 0x30, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x31, 0x31, - 0x18, 0x11, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x72, 0x31, 0x31, 0x22, 0x59, 0x0a, 0x11, 0x53, - 0x79, 0x73, 0x63, 0x61, 0x6c, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x68, 0x65, 0x61, 0x70, 0x5f, 0x70, 0x72, - 0x65, 0x66, 0x69, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x68, 0x65, 0x61, 0x70, - 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x22, 0xe7, 0x01, 0x0a, 0x0e, 0x53, 0x79, 0x73, 0x63, 0x61, - 0x6c, 0x6c, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x38, 0x0a, 0x06, 0x76, 0x6d, 0x5f, - 0x63, 0x74, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6f, 0x72, 0x67, 0x2e, - 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, - 0x76, 0x31, 0x2e, 0x56, 0x6d, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x05, 0x76, 0x6d, - 0x43, 0x74, 0x78, 0x12, 0x41, 0x0a, 0x09, 0x69, 0x6e, 0x73, 0x74, 0x72, 0x5f, 0x63, 0x74, 0x78, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, - 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, - 0x49, 0x6e, 0x73, 0x74, 0x72, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x08, 0x69, 0x6e, - 0x73, 0x74, 0x72, 0x43, 0x74, 0x78, 0x12, 0x58, 0x0a, 0x12, 0x73, 0x79, 0x73, 0x63, 0x61, 0x6c, - 0x6c, 0x5f, 0x69, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, - 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x73, 0x63, - 0x61, 0x6c, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x11, 0x73, - 0x79, 0x73, 0x63, 0x61, 0x6c, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x22, 0xcc, 0x01, 0x0a, 0x0e, 0x53, 0x79, 0x73, 0x63, 0x61, 0x6c, 0x6c, 0x45, 0x66, 0x66, 0x65, - 0x63, 0x74, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x30, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x72, 0x30, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x75, 0x5f, - 0x61, 0x76, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x63, 0x75, 0x41, - 0x76, 0x61, 0x69, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x65, 0x61, 0x70, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x04, 0x68, 0x65, 0x61, 0x70, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x63, - 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x12, 0x1c, - 0x0a, 0x09, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x64, 0x61, 0x74, 0x61, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x09, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1f, 0x0a, 0x0b, - 0x66, 0x72, 0x61, 0x6d, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x0a, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x10, 0x0a, - 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x22, - 0x8e, 0x01, 0x0a, 0x0e, 0x53, 0x79, 0x73, 0x63, 0x61, 0x6c, 0x6c, 0x46, 0x69, 0x78, 0x74, 0x75, - 0x72, 0x65, 0x12, 0x3c, 0x0a, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x26, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, - 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x73, 0x63, 0x61, - 0x6c, 0x6c, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, - 0x12, 0x3e, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x26, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, + 0x72, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x30, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x02, 0x72, 0x30, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x31, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x02, 0x72, 0x31, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x32, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x02, 0x72, 0x32, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x33, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x02, 0x72, 0x33, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x34, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x02, 0x72, 0x34, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x35, 0x18, 0x0b, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x02, 0x72, 0x35, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x36, 0x18, 0x0c, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x02, 0x72, 0x36, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x37, 0x18, 0x0d, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x02, 0x72, 0x37, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x38, 0x18, 0x0e, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x02, 0x72, 0x38, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x39, 0x18, 0x0f, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x02, 0x72, 0x39, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x31, 0x30, 0x18, 0x10, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x03, 0x72, 0x31, 0x30, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x31, 0x31, 0x18, + 0x11, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x72, 0x31, 0x31, 0x12, 0x19, 0x0a, 0x08, 0x65, 0x6e, + 0x74, 0x72, 0x79, 0x5f, 0x70, 0x63, 0x18, 0x14, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x65, 0x6e, + 0x74, 0x72, 0x79, 0x50, 0x63, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x77, 0x68, + 0x69, 0x74, 0x65, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x15, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x63, + 0x61, 0x6c, 0x6c, 0x57, 0x68, 0x69, 0x74, 0x65, 0x6c, 0x69, 0x73, 0x74, 0x12, 0x43, 0x0a, 0x0b, + 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x17, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x22, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, + 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x74, 0x75, 0x72, + 0x6e, 0x44, 0x61, 0x74, 0x61, 0x52, 0x0a, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x44, 0x61, 0x74, + 0x61, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x62, 0x70, 0x66, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x18, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x73, 0x62, 0x70, 0x66, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, + 0x4a, 0x04, 0x08, 0x16, 0x10, 0x17, 0x22, 0x7c, 0x0a, 0x11, 0x53, 0x79, 0x73, 0x63, 0x61, 0x6c, + 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x66, + 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x0c, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x1f, 0x0a, 0x0b, 0x68, 0x65, 0x61, 0x70, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x68, 0x65, 0x61, 0x70, 0x50, 0x72, 0x65, 0x66, 0x69, + 0x78, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, + 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x50, 0x72, + 0x65, 0x66, 0x69, 0x78, 0x22, 0xe7, 0x01, 0x0a, 0x0e, 0x53, 0x79, 0x73, 0x63, 0x61, 0x6c, 0x6c, + 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x38, 0x0a, 0x06, 0x76, 0x6d, 0x5f, 0x63, 0x74, + 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, + 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, + 0x2e, 0x56, 0x6d, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x05, 0x76, 0x6d, 0x43, 0x74, + 0x78, 0x12, 0x41, 0x0a, 0x09, 0x69, 0x6e, 0x73, 0x74, 0x72, 0x5f, 0x63, 0x74, 0x78, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, + 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, + 0x73, 0x74, 0x72, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x08, 0x69, 0x6e, 0x73, 0x74, + 0x72, 0x43, 0x74, 0x78, 0x12, 0x58, 0x0a, 0x12, 0x73, 0x79, 0x73, 0x63, 0x61, 0x6c, 0x6c, 0x5f, + 0x69, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x29, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x73, 0x63, 0x61, 0x6c, - 0x6c, 0x45, 0x66, 0x66, 0x65, 0x63, 0x74, 0x73, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, - 0x22, 0x89, 0x01, 0x0a, 0x0d, 0x46, 0x75, 0x6c, 0x6c, 0x56, 0x6d, 0x43, 0x6f, 0x6e, 0x74, 0x65, - 0x78, 0x74, 0x12, 0x38, 0x0a, 0x06, 0x76, 0x6d, 0x5f, 0x63, 0x74, 0x78, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, - 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x6d, 0x43, 0x6f, - 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x05, 0x76, 0x6d, 0x43, 0x74, 0x78, 0x12, 0x3e, 0x0a, 0x08, - 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, - 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, - 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x53, - 0x65, 0x74, 0x52, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x22, 0x45, 0x0a, 0x11, - 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x56, 0x6d, 0x45, 0x66, 0x66, 0x65, 0x63, 0x74, - 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x22, 0x93, 0x01, 0x0a, 0x11, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, - 0x56, 0x6d, 0x46, 0x69, 0x78, 0x74, 0x75, 0x72, 0x65, 0x12, 0x3b, 0x0a, 0x05, 0x69, 0x6e, 0x70, - 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, + 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x11, 0x73, 0x79, 0x73, + 0x63, 0x61, 0x6c, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x95, + 0x04, 0x0a, 0x0e, 0x53, 0x79, 0x73, 0x63, 0x61, 0x6c, 0x6c, 0x45, 0x66, 0x66, 0x65, 0x63, 0x74, + 0x73, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x3e, 0x0a, 0x0a, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6f, 0x72, + 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, + 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x72, 0x72, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x09, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x30, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x02, 0x72, 0x30, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x75, 0x5f, 0x61, 0x76, + 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x63, 0x75, 0x41, 0x76, 0x61, + 0x69, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x65, 0x61, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x04, 0x68, 0x65, 0x61, 0x70, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x12, 0x55, 0x0a, 0x12, + 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x6f, + 0x6e, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, - 0x31, 0x2e, 0x46, 0x75, 0x6c, 0x6c, 0x56, 0x6d, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, - 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x41, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, - 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, - 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x56, 0x6d, 0x45, 0x66, 0x66, 0x65, 0x63, 0x74, - 0x73, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x42, 0x0f, 0x5a, 0x0d, 0x2e, 0x2f, 0x63, - 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x31, 0x2e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x67, 0x69, 0x6f, + 0x6e, 0x52, 0x10, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x67, 0x69, + 0x6f, 0x6e, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x5f, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x43, + 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x6f, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x72, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x12, 0x0e, + 0x0a, 0x02, 0x70, 0x63, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x70, 0x63, 0x12, 0x0e, + 0x0a, 0x02, 0x72, 0x31, 0x18, 0x6b, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x72, 0x31, 0x12, 0x0e, + 0x0a, 0x02, 0x72, 0x32, 0x18, 0x6c, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x72, 0x32, 0x12, 0x0e, + 0x0a, 0x02, 0x72, 0x33, 0x18, 0x6d, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x72, 0x33, 0x12, 0x0e, + 0x0a, 0x02, 0x72, 0x34, 0x18, 0x6e, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x72, 0x34, 0x12, 0x0e, + 0x0a, 0x02, 0x72, 0x35, 0x18, 0x6f, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x72, 0x35, 0x12, 0x0e, + 0x0a, 0x02, 0x72, 0x36, 0x18, 0x70, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x72, 0x36, 0x12, 0x0e, + 0x0a, 0x02, 0x72, 0x37, 0x18, 0x71, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x72, 0x37, 0x12, 0x0e, + 0x0a, 0x02, 0x72, 0x38, 0x18, 0x72, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x72, 0x38, 0x12, 0x0e, + 0x0a, 0x02, 0x72, 0x39, 0x18, 0x73, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x72, 0x39, 0x12, 0x10, + 0x0a, 0x03, 0x72, 0x31, 0x30, 0x18, 0x74, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x72, 0x31, 0x30, + 0x4a, 0x04, 0x08, 0x06, 0x10, 0x07, 0x22, 0xd3, 0x01, 0x0a, 0x0e, 0x53, 0x79, 0x73, 0x63, 0x61, + 0x6c, 0x6c, 0x46, 0x69, 0x78, 0x74, 0x75, 0x72, 0x65, 0x12, 0x43, 0x0a, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6f, 0x72, + 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, + 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x78, 0x74, 0x75, 0x72, 0x65, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x3c, + 0x0a, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, + 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, + 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x73, 0x63, 0x61, 0x6c, 0x6c, 0x43, 0x6f, + 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x3e, 0x0a, 0x06, + 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6f, + 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, + 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x73, 0x63, 0x61, 0x6c, 0x6c, 0x45, 0x66, 0x66, + 0x65, 0x63, 0x74, 0x73, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0x89, 0x01, 0x0a, + 0x0d, 0x46, 0x75, 0x6c, 0x6c, 0x56, 0x6d, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x38, + 0x0a, 0x06, 0x76, 0x6d, 0x5f, 0x63, 0x74, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, + 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, + 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x6d, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, + 0x74, 0x52, 0x05, 0x76, 0x6d, 0x43, 0x74, 0x78, 0x12, 0x3e, 0x0a, 0x08, 0x66, 0x65, 0x61, 0x74, + 0x75, 0x72, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6f, 0x72, 0x67, + 0x2e, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, + 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x53, 0x65, 0x74, 0x52, 0x08, + 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x22, 0x45, 0x0a, 0x11, 0x56, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x65, 0x56, 0x6d, 0x45, 0x66, 0x66, 0x65, 0x63, 0x74, 0x73, 0x12, 0x16, 0x0a, + 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x72, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x22, + 0xd8, 0x01, 0x0a, 0x11, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x56, 0x6d, 0x46, 0x69, + 0x78, 0x74, 0x75, 0x72, 0x65, 0x12, 0x43, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, + 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, + 0x2e, 0x46, 0x69, 0x78, 0x74, 0x75, 0x72, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x3b, 0x0a, 0x05, 0x69, 0x6e, + 0x70, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6f, 0x72, 0x67, 0x2e, + 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, + 0x76, 0x31, 0x2e, 0x46, 0x75, 0x6c, 0x6c, 0x56, 0x6d, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, + 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x41, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, + 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x6f, + 0x6c, 0x61, 0x6e, 0x61, 0x2e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x2e, 0x76, 0x31, + 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x56, 0x6d, 0x45, 0x66, 0x66, 0x65, 0x63, + 0x74, 0x73, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0x3f, 0x0a, 0x0a, 0x52, 0x65, + 0x74, 0x75, 0x72, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x67, + 0x72, 0x61, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x72, + 0x6f, 0x67, 0x72, 0x61, 0x6d, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x2a, 0x42, 0x0a, 0x07, 0x45, + 0x72, 0x72, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, + 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x45, 0x42, 0x50, 0x46, 0x10, + 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x59, 0x53, 0x43, 0x41, 0x4c, 0x4c, 0x10, 0x02, 0x12, 0x0f, + 0x0a, 0x0b, 0x49, 0x4e, 0x53, 0x54, 0x52, 0x55, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x03, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -858,36 +1145,44 @@ func file_vm_proto_rawDescGZIP() []byte { return file_vm_proto_rawDescData } -var file_vm_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_vm_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_vm_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_vm_proto_goTypes = []any{ - (*InputDataRegion)(nil), // 0: org.solana.sealevel.v1.InputDataRegion - (*VmContext)(nil), // 1: org.solana.sealevel.v1.VmContext - (*SyscallInvocation)(nil), // 2: org.solana.sealevel.v1.SyscallInvocation - (*SyscallContext)(nil), // 3: org.solana.sealevel.v1.SyscallContext - (*SyscallEffects)(nil), // 4: org.solana.sealevel.v1.SyscallEffects - (*SyscallFixture)(nil), // 5: org.solana.sealevel.v1.SyscallFixture - (*FullVmContext)(nil), // 6: org.solana.sealevel.v1.FullVmContext - (*ValidateVmEffects)(nil), // 7: org.solana.sealevel.v1.ValidateVmEffects - (*ValidateVmFixture)(nil), // 8: org.solana.sealevel.v1.ValidateVmFixture - (*InstrContext)(nil), // 9: org.solana.sealevel.v1.InstrContext - (*FeatureSet)(nil), // 10: org.solana.sealevel.v1.FeatureSet + (ErrKind)(0), // 0: org.solana.sealevel.v1.ErrKind + (*InputDataRegion)(nil), // 1: org.solana.sealevel.v1.InputDataRegion + (*VmContext)(nil), // 2: org.solana.sealevel.v1.VmContext + (*SyscallInvocation)(nil), // 3: org.solana.sealevel.v1.SyscallInvocation + (*SyscallContext)(nil), // 4: org.solana.sealevel.v1.SyscallContext + (*SyscallEffects)(nil), // 5: org.solana.sealevel.v1.SyscallEffects + (*SyscallFixture)(nil), // 6: org.solana.sealevel.v1.SyscallFixture + (*FullVmContext)(nil), // 7: org.solana.sealevel.v1.FullVmContext + (*ValidateVmEffects)(nil), // 8: org.solana.sealevel.v1.ValidateVmEffects + (*ValidateVmFixture)(nil), // 9: org.solana.sealevel.v1.ValidateVmFixture + (*ReturnData)(nil), // 10: org.solana.sealevel.v1.ReturnData + (*InstrContext)(nil), // 11: org.solana.sealevel.v1.InstrContext + (*FixtureMetadata)(nil), // 12: org.solana.sealevel.v1.FixtureMetadata + (*FeatureSet)(nil), // 13: org.solana.sealevel.v1.FeatureSet } var file_vm_proto_depIdxs = []int32{ - 0, // 0: org.solana.sealevel.v1.VmContext.input_data_regions:type_name -> org.solana.sealevel.v1.InputDataRegion - 1, // 1: org.solana.sealevel.v1.SyscallContext.vm_ctx:type_name -> org.solana.sealevel.v1.VmContext - 9, // 2: org.solana.sealevel.v1.SyscallContext.instr_ctx:type_name -> org.solana.sealevel.v1.InstrContext - 2, // 3: org.solana.sealevel.v1.SyscallContext.syscall_invocation:type_name -> org.solana.sealevel.v1.SyscallInvocation - 3, // 4: org.solana.sealevel.v1.SyscallFixture.input:type_name -> org.solana.sealevel.v1.SyscallContext - 4, // 5: org.solana.sealevel.v1.SyscallFixture.output:type_name -> org.solana.sealevel.v1.SyscallEffects - 1, // 6: org.solana.sealevel.v1.FullVmContext.vm_ctx:type_name -> org.solana.sealevel.v1.VmContext - 10, // 7: org.solana.sealevel.v1.FullVmContext.features:type_name -> org.solana.sealevel.v1.FeatureSet - 6, // 8: org.solana.sealevel.v1.ValidateVmFixture.input:type_name -> org.solana.sealevel.v1.FullVmContext - 7, // 9: org.solana.sealevel.v1.ValidateVmFixture.output:type_name -> org.solana.sealevel.v1.ValidateVmEffects - 10, // [10:10] is the sub-list for method output_type - 10, // [10:10] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name + 10, // 0: org.solana.sealevel.v1.VmContext.return_data:type_name -> org.solana.sealevel.v1.ReturnData + 2, // 1: org.solana.sealevel.v1.SyscallContext.vm_ctx:type_name -> org.solana.sealevel.v1.VmContext + 11, // 2: org.solana.sealevel.v1.SyscallContext.instr_ctx:type_name -> org.solana.sealevel.v1.InstrContext + 3, // 3: org.solana.sealevel.v1.SyscallContext.syscall_invocation:type_name -> org.solana.sealevel.v1.SyscallInvocation + 0, // 4: org.solana.sealevel.v1.SyscallEffects.error_kind:type_name -> org.solana.sealevel.v1.ErrKind + 1, // 5: org.solana.sealevel.v1.SyscallEffects.input_data_regions:type_name -> org.solana.sealevel.v1.InputDataRegion + 12, // 6: org.solana.sealevel.v1.SyscallFixture.metadata:type_name -> org.solana.sealevel.v1.FixtureMetadata + 4, // 7: org.solana.sealevel.v1.SyscallFixture.input:type_name -> org.solana.sealevel.v1.SyscallContext + 5, // 8: org.solana.sealevel.v1.SyscallFixture.output:type_name -> org.solana.sealevel.v1.SyscallEffects + 2, // 9: org.solana.sealevel.v1.FullVmContext.vm_ctx:type_name -> org.solana.sealevel.v1.VmContext + 13, // 10: org.solana.sealevel.v1.FullVmContext.features:type_name -> org.solana.sealevel.v1.FeatureSet + 12, // 11: org.solana.sealevel.v1.ValidateVmFixture.metadata:type_name -> org.solana.sealevel.v1.FixtureMetadata + 7, // 12: org.solana.sealevel.v1.ValidateVmFixture.input:type_name -> org.solana.sealevel.v1.FullVmContext + 8, // 13: org.solana.sealevel.v1.ValidateVmFixture.output:type_name -> org.solana.sealevel.v1.ValidateVmEffects + 14, // [14:14] is the sub-list for method output_type + 14, // [14:14] is the sub-list for method input_type + 14, // [14:14] is the sub-list for extension type_name + 14, // [14:14] is the sub-list for extension extendee + 0, // [0:14] is the sub-list for field type_name } func init() { file_vm_proto_init() } @@ -897,6 +1192,7 @@ func file_vm_proto_init() { } file_invoke_proto_init() file_context_proto_init() + file_metadata_proto_init() if !protoimpl.UnsafeEnabled { file_vm_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*InputDataRegion); i { @@ -1006,19 +1302,32 @@ func file_vm_proto_init() { return nil } } + file_vm_proto_msgTypes[9].Exporter = func(v any, i int) any { + switch v := v.(*ReturnData); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_vm_proto_rawDesc, - NumEnums: 0, - NumMessages: 9, + NumEnums: 1, + NumMessages: 10, NumExtensions: 0, NumServices: 0, }, GoTypes: file_vm_proto_goTypes, DependencyIndexes: file_vm_proto_depIdxs, + EnumInfos: file_vm_proto_enumTypes, MessageInfos: file_vm_proto_msgTypes, }.Build() File_vm_proto = out.File diff --git a/conformance/vm_programs_test.go b/conformance/vm_programs_test.go index bc7486a4..bf4c8e1c 100644 --- a/conformance/vm_programs_test.go +++ b/conformance/vm_programs_test.go @@ -219,7 +219,7 @@ func newVMProgramExecCtxAndInstrAccts(fixture *InstrFixture) (*sealevelPkg.Execu Log: &sealevelPkg.LogRecorder{}, } execCtx.Accounts = accounts.NewMemAccounts() - execCtx.Features = *parsePBFeatures(input.GetEpochContext().GetFeatures()) + execCtx.Features = *parsePBFeatures(input.GetFeatures()) withoutConformanceStdout(func() { configureSysvarsFromFixture(&execCtx, fixture) @@ -237,10 +237,10 @@ func newVMProgramExecCtxAndInstrAccts(fixture *InstrFixture) (*sealevelPkg.Execu } } - slot := input.GetSlotContext().GetSlot() - if slot == 0 { - slot = ^uint64(0) - } + // protosol v5.4.0 removed slot_context from InstrContext, so the corpus no + // longer pins a slot for the instruction harness. Keep the sentinel the + // previous code already used whenever the fixture supplied none. + slot := ^uint64(0) execCtx.SlotCtx = &sealevelPkg.SlotCtx{ Accounts: slotAccounts, ParentAccts: accounts.NewMemAccounts(), diff --git a/go.mod b/go.mod index 5d170136..95a8dee2 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ replace github.com/gagliardetto/solana-go => github.com/palmerlao/solana-go v0.0 replace github.com/gagliardetto/binary => github.com/palmerlao/binary v0.0.0-20250617062159-3054b4d33aed require ( + github.com/Overclock-Validator/narya-ed25519 v0.0.0-20260726222623-da0d045dae9d github.com/cespare/xxhash/v2 v2.3.0 github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 github.com/charmbracelet/bubbletea v1.3.10 @@ -145,7 +146,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect - github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a + github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect github.com/panjf2000/ants/v2 v2.10.0 github.com/pierrec/lz4/v4 v4.1.22 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect diff --git a/go.sum b/go.sum index 12e59a41..f1c4e7e5 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/Overclock-Validator/crypto v0.0.0-20250307094320-aaf52fac5261 h1:Y715 github.com/Overclock-Validator/crypto v0.0.0-20250307094320-aaf52fac5261/go.mod h1:ZhRHOaVg8I1gg0VK4wmqOQPnlgPgKFT9McZ+TCW/hBA= github.com/Overclock-Validator/gnark-crypto v0.0.0-20250309203346-2a67ed08a105 h1:mP6FWHZ8ddcmbE8UTrVVI2Mi2c24aqX/8p12Vn6zokQ= github.com/Overclock-Validator/gnark-crypto v0.0.0-20250309203346-2a67ed08a105/go.mod h1:Poczuq3dbt+CwyTKgOjGaEwJOMP7YxQobF7QhgNcguk= +github.com/Overclock-Validator/narya-ed25519 v0.0.0-20260726222623-da0d045dae9d h1:ipaL+9MHKeI8QIfWneId0VLa+STLRM1e6MnvZhQyhPU= +github.com/Overclock-Validator/narya-ed25519 v0.0.0-20260726222623-da0d045dae9d/go.mod h1:B7/xqV/5NtGJa8OlZAa9TRMHgeIE+VEJNiPzMP4FrIg= github.com/Overclock-Validator/solana-snapshot-finder-go v0.0.0-20260223201452-d8363b514fc0 h1:elgavEQb8l7Zn3gS3Y+2/98PlUylOWdlM3V1VumQ7mA= github.com/Overclock-Validator/solana-snapshot-finder-go v0.0.0-20260223201452-d8363b514fc0/go.mod h1:XbqbvMA2NKeosY0w3WdBOpCg2eYJesBjfE4cNt9HSE8= github.com/Overclock-Validator/wide v0.0.0-20250221123529-f80959d02044 h1:ph9gnWIY116AWT/iCfXoPe9/cn2aWx2uJBuLdf/LyEE= diff --git a/pkg/gossip/contact_info.go b/pkg/gossip/contact_info.go index a2485efc..baed0d39 100644 --- a/pkg/gossip/contact_info.go +++ b/pkg/gossip/contact_info.go @@ -8,6 +8,8 @@ import ( "net" "sort" "time" + + narya "github.com/Overclock-Validator/narya-ed25519/ed25519" ) const ( @@ -120,7 +122,7 @@ func NewContactInfo(pubkey Pubkey, shredVersion uint16, gossipAddr, tvuAddr *net } func (r contactRecord) Verify() bool { - return ed25519.Verify(ed25519.PublicKey(r.Pubkey[:]), r.data, r.signature[:]) + return narya.VerifyStrict(r.Pubkey[:], r.data, r.signature[:]) } func (r contactRecord) ContactInfo() *ContactInfo { @@ -641,7 +643,7 @@ func (v CrdsValue) Verify() bool { if v.ContactInfo == nil { return false } - return ed25519.Verify(ed25519.PublicKey(v.ContactInfo.Pubkey[:]), v.Data, v.Signature[:]) + return narya.VerifyStrict(v.ContactInfo.Pubkey[:], v.Data, v.Signature[:]) } func hashPingToken(token [32]byte) Hash { diff --git a/pkg/gossip/message.go b/pkg/gossip/message.go index 4b1d5689..0d5b4303 100644 --- a/pkg/gossip/message.go +++ b/pkg/gossip/message.go @@ -6,6 +6,8 @@ import ( "errors" "fmt" "net" + + narya "github.com/Overclock-Validator/narya-ed25519/ed25519" ) var errUnsupportedCRDSValue = errors.New("unsupported CRDS value") @@ -50,11 +52,11 @@ func newPong(ping Ping, identity ed25519.PrivateKey) (Pong, error) { } func (p Ping) Verify() bool { - return ed25519.Verify(ed25519.PublicKey(p.From[:]), p.Token[:], p.Signature[:]) + return narya.VerifyStrict(p.From[:], p.Token[:], p.Signature[:]) } func (p Pong) Verify() bool { - return ed25519.Verify(ed25519.PublicKey(p.From[:]), p.Hash[:], p.Signature[:]) + return narya.VerifyStrict(p.From[:], p.Hash[:], p.Signature[:]) } func encodePingMessage(ping Ping) []byte { diff --git a/pkg/gossip/strict_verify_test.go b/pkg/gossip/strict_verify_test.go new file mode 100644 index 00000000..30a7ef27 --- /dev/null +++ b/pkg/gossip/strict_verify_test.go @@ -0,0 +1,88 @@ +package gossip + +import ( + "crypto/ed25519" + "encoding/hex" + "testing" + + narya "github.com/Overclock-Validator/narya-ed25519/ed25519" +) + +// A canonical small-order point encoding. Mainnet (ed25519-dalek +// verify_strict) rejects a signature whose public key or R point is +// small-order; Go's crypto/ed25519 does not. These gossip Verify +// methods must reject it — a crafted contact-info or ping/pong with a +// small-order key must not be accepted where mainnet would reject it. +const smallOrderPubHex = "0100000000000000000000000000000000000000000000000000000000000000" + +func smallOrderPubkey(t *testing.T) Pubkey { + t.Helper() + raw, err := hex.DecodeString(smallOrderPubHex) + if err != nil { + t.Fatal(err) + } + var p Pubkey + copy(p[:], raw) + return p +} + +// TestPingVerifyStrict covers the happy path (an honest ping still +// verifies), small-order rejection (the fix), and exact agreement with +// narya.VerifyStrict across a spread of inputs (the wiring). +func TestPingVerifyStrict(t *testing.T) { + _, priv, err := ed25519.GenerateKey(nil) + if err != nil { + t.Fatal(err) + } + honest, err := newPing(priv) + if err != nil { + t.Fatal(err) + } + if !honest.Verify() { + t.Fatal("honest ping failed to verify") + } + + // Small-order From: rejected regardless of signature. + bad := honest + bad.From = smallOrderPubkey(t) + if bad.Verify() { + t.Fatal("ping with small-order public key was accepted") + } + + // The method must equal narya.VerifyStrict on every input. + for _, p := range []Ping{honest, bad} { + if p.Verify() != narya.VerifyStrict(p.From[:], p.Token[:], p.Signature[:]) { + t.Fatal("Ping.Verify diverged from narya.VerifyStrict") + } + } +} + +// TestPongVerifyStrict mirrors TestPingVerifyStrict for pong. +func TestPongVerifyStrict(t *testing.T) { + _, priv, err := ed25519.GenerateKey(nil) + if err != nil { + t.Fatal(err) + } + ping, err := newPing(priv) + if err != nil { + t.Fatal(err) + } + honest, err := newPong(ping, priv) + if err != nil { + t.Fatal(err) + } + if !honest.Verify() { + t.Fatal("honest pong failed to verify") + } + + bad := honest + bad.From = smallOrderPubkey(t) + if bad.Verify() { + t.Fatal("pong with small-order public key was accepted") + } + for _, p := range []Pong{honest, bad} { + if p.Verify() != narya.VerifyStrict(p.From[:], p.Hash[:], p.Signature[:]) { + t.Fatal("Pong.Verify diverged from narya.VerifyStrict") + } + } +} diff --git a/pkg/repair/protocol.go b/pkg/repair/protocol.go index 9a7040ff..77281f8a 100644 --- a/pkg/repair/protocol.go +++ b/pkg/repair/protocol.go @@ -9,6 +9,7 @@ import ( "time" "github.com/Overclock-Validator/mithril/pkg/gossip" + narya "github.com/Overclock-Validator/narya-ed25519/ed25519" ) const ( @@ -73,7 +74,7 @@ func DecodePing(packet []byte) (Ping, bool) { copy(ping.From[:], packet[4:36]) copy(ping.Token[:], packet[36:68]) copy(ping.Signature[:], packet[68:132]) - if !ed25519.Verify(ed25519.PublicKey(ping.From[:]), ping.Token[:], ping.Signature[:]) { + if !narya.VerifyStrict(ping.From[:], ping.Token[:], ping.Signature[:]) { return Ping{}, false } return ping, true @@ -136,6 +137,21 @@ func signRepairPacket(identity ed25519.PrivateKey, packet []byte) { copy(packet[repairSignatureOffset:repairSignatureOffset+repairSignatureSize], signature) } +// VerifySignedRequest authenticates an INBOUND repair request — a peer asking +// us to serve it a shred. Mithril is requester-side only today, so nothing +// outside tests calls this yet. +// +// When repair serving is wired up, this becomes a packet-rate consumer fed by +// a UDP socket loop, which is the shape that wants batching: signatures are +// verified eight per AVX-512 group, so verifying one packet at a time pays for +// a whole group and uses one lane of it. Drain the socket's work queue and +// verify a group per pass, exactly as pkg/tpu/pipeline does — sigverify.Drain +// and sigverify.FairShare exist for this, and take a share rather than the +// whole queue so batching does not eat the parallelism across workers. +// +// The predicate here is already the strict one, which matters more than the +// throughput: a repair request authenticated under plain stdlib rules would +// accept small-order sender keys that Agave rejects. func VerifySignedRequest(packet []byte, sender gossip.Pubkey) bool { if len(packet) < repairSignatureOffset+repairSignatureSize { return false @@ -143,7 +159,7 @@ func VerifySignedRequest(packet []byte, sender gossip.Pubkey) bool { signable := make([]byte, 0, len(packet)-repairSignatureSize) signable = append(signable, packet[:repairSignatureOffset]...) signable = append(signable, packet[repairSignatureOffset+repairSignatureSize:]...) - return ed25519.Verify(ed25519.PublicKey(sender[:]), signable, packet[repairSignatureOffset:repairSignatureOffset+repairSignatureSize]) + return narya.VerifyStrict(sender[:], signable, packet[repairSignatureOffset:repairSignatureOffset+repairSignatureSize]) } func hashPingToken(token [32]byte) gossip.Hash { diff --git a/pkg/repair/strict_verify_test.go b/pkg/repair/strict_verify_test.go new file mode 100644 index 00000000..e9b8bd5a --- /dev/null +++ b/pkg/repair/strict_verify_test.go @@ -0,0 +1,98 @@ +package repair + +import ( + "crypto/ed25519" + "encoding/hex" + "testing" + + "github.com/Overclock-Validator/mithril/pkg/gossip" +) + +// Real ed25519vectors (via Firedancer's CCTV corpus) that Go's +// crypto/ed25519.Verify ACCEPTS but mainnet's verify_strict REJECTS, +// because the public key A or the signature point R is small-order. +// A repair request carrying such a signature must be rejected — this +// is the exact adversarial class the strict fix closes. +var strictDivergentVectors = []struct { + name, pub, msg, sig string +}{ + { + name: "small-order A (all-zero pubkey)", + pub: "0000000000000000000000000000000000000000000000000000000000000000", + msg: "65643235353139766563746f72732033", + sig: "36684ea91032ba5b1dbab2d02f4debc74c3327f2b3802e2e4d371aa42b12b56b05ba9a796274d80437afa36f1236563f2f3b0aa84cecddc3d20914615ba4fe02", + }, + { + name: "small-order R (all-zero R point)", + pub: "10eb7c3acfb2bed3e0d6ab89bf5a3d6afddd1176ce4812e38d9fd485058fdb1f", + msg: "65643235353139766563746f72732033", + sig: "00000000000000000000000000000000000000000000000000000000000000009472a69cd9a701a50d130ed52189e2455b23767db52cacb8716fb896ffeeac09", + }, +} + +func mustHex(t *testing.T, s string) []byte { + t.Helper() + b, err := hex.DecodeString(s) + if err != nil { + t.Fatal(err) + } + return b +} + +// TestVerifySignedRequestRejectsSmallOrder is the rigorous divergence +// proof: for each vector, the standard library accepts (precondition), +// and VerifySignedRequest — now routed through strict verification — +// rejects. A repair request's signable message is the packet with the +// signature field spliced out, so we build a packet whose reconstructed +// signable equals the vector's message. +func TestVerifySignedRequestRejectsSmallOrder(t *testing.T) { + for _, v := range strictDivergentVectors { + pub := mustHex(t, v.pub) + msg := mustHex(t, v.msg) + sig := mustHex(t, v.sig) + if len(msg) < repairSignatureOffset { + t.Fatalf("%s: message too short to embed", v.name) + } + + // Precondition: the standard library accepts this signature, so + // the test genuinely exercises the strict/stdlib divergence. + if !ed25519.Verify(pub, msg, sig) { + t.Fatalf("%s: precondition failed, stdlib should accept", v.name) + } + + // packet = msg[:4] ‖ sig ‖ msg[4:], so + // signable = packet[:4] ‖ packet[68:] = msg. + packet := make([]byte, 0, len(msg)+repairSignatureSize) + packet = append(packet, msg[:repairSignatureOffset]...) + packet = append(packet, sig...) + packet = append(packet, msg[repairSignatureOffset:]...) + + var sender gossip.Pubkey + copy(sender[:], pub) + + if VerifySignedRequest(packet, sender) { + t.Fatalf("%s: strict verification accepted a signature mainnet rejects", v.name) + } + } +} + +// TestVerifySignedRequestHappyPath guards against the strict change +// breaking honest repair requests: a properly signed request verifies. +func TestVerifySignedRequestHappyPath(t *testing.T) { + _, priv, err := ed25519.GenerateKey(nil) + if err != nil { + t.Fatal(err) + } + var recipient gossip.Pubkey + packet, err := BuildWindowIndexRequest(priv, recipient, 42, 7, 99) + if err != nil { + t.Fatal(err) + } + sender, err := senderPubkey(priv) + if err != nil { + t.Fatal(err) + } + if !VerifySignedRequest(packet, sender) { + t.Fatal("honest repair request failed strict verification") + } +} diff --git a/pkg/replay/alpenglow_nanosecond_clock_test.go b/pkg/replay/alpenglow_nanosecond_clock_test.go index a70badff..4661fdb0 100644 --- a/pkg/replay/alpenglow_nanosecond_clock_test.go +++ b/pkg/replay/alpenglow_nanosecond_clock_test.go @@ -5,8 +5,8 @@ import ( "sync" "testing" - b "github.com/Overclock-Validator/mithril/pkg/block" "github.com/Overclock-Validator/mithril/pkg/accounts" + b "github.com/Overclock-Validator/mithril/pkg/block" "github.com/Overclock-Validator/mithril/pkg/sealevel" "github.com/gagliardetto/solana-go" "github.com/stretchr/testify/require" diff --git a/pkg/replay/sigverify_batch_test.go b/pkg/replay/sigverify_batch_test.go new file mode 100644 index 00000000..c8637533 --- /dev/null +++ b/pkg/replay/sigverify_batch_test.go @@ -0,0 +1,169 @@ +package replay + +import ( + "fmt" + "strings" + "sync" + "testing" + "time" + + "github.com/Overclock-Validator/mithril/pkg/sigverify" +) + +// Batching must not blur failure attribution. An invalid signature is a +// deliberate process halt, and the panic message is the only forensic artifact +// the operator gets, so it has to name the signer that actually failed — not +// the first in the group, and not merely "somewhere in this batch". +// +// The bad job is placed at the front, the back, and the interior of a full +// group so a verdict written to the wrong lane cannot pass by symmetry. +func TestVerifySignatureBatchNamesTheFailingSigner(t *testing.T) { + for _, badIndex := range []int{0, 1, 7, 8, 31, sigverify.MaxDrain - 1} { + t.Run(fmt.Sprintf("badIndex=%d", badIndex), func(t *testing.T) { + var wg sync.WaitGroup + group := make([]sigverifyJob, sigverify.MaxDrain) + for i := range group { + wg.Add(1) + group[i] = sigverifyJob{snapshot: signedTestSnapshot(t, i == badIndex), wg: &wg} + } + wantSigner := group[badIndex].snapshot.signers[0].String() + + defer func() { + r := recover() + if r == nil { + t.Fatalf("badIndex=%d: expected a panic on the invalid signature", badIndex) + } + msg, ok := r.(string) + if !ok || !strings.Contains(msg, "invalid signature") { + t.Fatalf("badIndex=%d: panic = %v, want invalid-signature message", badIndex, r) + } + if !strings.Contains(msg, wantSigner) { + t.Fatalf("badIndex=%d: panic named the wrong signer\n got: %s\nwant signer: %s", + badIndex, msg, wantSigner) + } + // The halt must not also leak the block's WaitGroup: every job + // in the group is released on the way out. + wg.Wait() + }() + + var batch sigverify.Batch + verifySignatureBatch(group, &batch) + }) + } +} + +// The join contract ProcessBlock depends on: every job in a group is released, +// and grouping does not change that. Jobs in one group may belong to different +// blocks, so each is released against its own WaitGroup. +func TestVerifySignatureBatchReleasesEveryWaitGroupInTheGroup(t *testing.T) { + var first, second sync.WaitGroup + group := make([]sigverifyJob, 0, 16) + for i := 0; i < 16; i++ { + wg := &first + if i%2 == 1 { + wg = &second + } + wg.Add(1) + group = append(group, sigverifyJob{snapshot: signedTestSnapshot(t, false), wg: wg}) + } + + var batch sigverify.Batch + verifySignatureBatch(group, &batch) + + first.Wait() // hangs (test timeout) if a job was released against the wrong group + second.Wait() // ...or not at all +} + +// A worker reuses one Batch across groups. A stale verdict or a retained +// public key from the previous group would be a correctness bug, not just a +// leak, so run several groups through one Batch and mix in a failure. +func TestVerifySignatureBatchReusesScratchAcrossGroups(t *testing.T) { + var batch sigverify.Batch + for round := 0; round < 4; round++ { + var wg sync.WaitGroup + group := make([]sigverifyJob, 0, 9) + for i := 0; i < 9; i++ { + wg.Add(1) + group = append(group, sigverifyJob{snapshot: signedTestSnapshot(t, false), wg: &wg}) + } + verifySignatureBatch(group, &batch) + wg.Wait() + } +} + +// The arity check predates batching and must survive it: a snapshot whose +// signer and signature counts disagree halts before any verification, because +// the pairing it would verify is meaningless. +func TestVerifySignatureBatchHaltsOnArityMismatch(t *testing.T) { + var wg sync.WaitGroup + wg.Add(1) + snapshot := signedTestSnapshot(t, false) + snapshot.signatures = append(snapshot.signatures, snapshot.signatures[0]) + + defer func() { + r := recover() + if r == nil { + t.Fatal("expected a panic on mismatched signers/signatures") + } + if msg, ok := r.(string); !ok || !strings.Contains(msg, "mismatched signers/signatures") { + t.Fatalf("panic = %v, want mismatched-arity message", r) + } + wg.Wait() + }() + + var batch sigverify.Batch + verifySignatureBatch([]sigverifyJob{{snapshot: snapshot, wg: &wg}}, &batch) +} + +// The property that matters most about grouping: nothing is ever left sitting +// in a partly-filled batch. Workers never wait to reach a target width, so a +// count that divides badly into groups must still drain completely. +// +// The assertion is on the JOIN completing, not merely on no error being +// returned: a leftover held back in a worker's scratch would leave wg.Wait() +// below waiting forever, so the bug surfaces as a timeout instead of slipping +// through as a pass. +func TestSigverifyPoolDrainsAwkwardCountsCompletely(t *testing.T) { + for _, count := range []int{1, 2, 3, 7, 8, 9, 63, 64, 65, 129} { + t.Run(fmt.Sprintf("count=%d", count), func(t *testing.T) { + var wg sync.WaitGroup + for i := 0; i < count; i++ { + wg.Add(1) + enqueueSigverify(signedTestSnapshot(t, false), &wg) + } + + done := make(chan struct{}) + go func() { + wg.Wait() + close(done) + }() + select { + case <-done: + case <-time.After(30 * time.Second): + t.Fatalf("count=%d: signatures stranded in an unfinished batch", count) + } + }) + } +} + +// A trickle must not be held back waiting for company. Each snapshot is +// enqueued only after the previous one has been verified, so every batch is a +// batch of one; the pool has to keep making progress anyway. +func TestSigverifyPoolMakesProgressOnATrickle(t *testing.T) { + for i := 0; i < 12; i++ { + var wg sync.WaitGroup + wg.Add(1) + enqueueSigverify(signedTestSnapshot(t, false), &wg) + + done := make(chan struct{}) + go func() { + wg.Wait() + close(done) + }() + select { + case <-done: + case <-time.After(10 * time.Second): + t.Fatalf("item %d was held waiting for a fuller batch", i) + } + } +} diff --git a/pkg/replay/sigverify_pool.go b/pkg/replay/sigverify_pool.go index 642a5ac7..69fce016 100644 --- a/pkg/replay/sigverify_pool.go +++ b/pkg/replay/sigverify_pool.go @@ -3,6 +3,8 @@ package replay import ( "runtime" "sync" + + "github.com/Overclock-Validator/mithril/pkg/sigverify" ) // Transaction signature verification runs CONCURRENT with execution: the @@ -20,6 +22,15 @@ import ( // verification backlog into gentle backpressure on the enqueuers instead of // an unbounded goroutine pileup; the block-end WaitGroup drain is where any // residual lag surfaces (inside the exec time, same as before). +// +// Workers verify a DRAINED GROUP of jobs rather than one job at a time. The +// vectorized backend verifies eight signatures per AVX-512 group, so a lone +// signature pays for a whole group and uses one lane of it — roughly 3.7x the +// per-signature cost of one inside a full group. Draining costs nothing when +// the queue is empty (the worker still takes exactly the one job it blocked +// for) and is free width when a backlog exists. That suits this queue +// specifically: a backlog here means catch-up, and catch-up is precisely when +// throughput matters and per-block latency does not. const sigverifyQueueDepth = 8192 type sigverifyJob struct { @@ -28,22 +39,32 @@ type sigverifyJob struct { } var ( - sigverifyOnce sync.Once - sigverifyQueue chan sigverifyJob + sigverifyOnce sync.Once + sigverifyQueue chan sigverifyJob + sigverifyWorkers int ) // enqueueSigverify hands a snapshot to the verification pool. The caller -// must have added to wg; a worker calls verifySignatures, which Done()s it. +// must have added to wg; a worker calls verifySignatureBatch, which Done()s it. // An invalid signature panics in the worker — a deliberate halt, identical // to the per-goroutine behavior it replaces. func enqueueSigverify(snapshot *sigverifySnapshot, wg *sync.WaitGroup) { sigverifyOnce.Do(func() { sigverifyQueue = make(chan sigverifyJob, sigverifyQueueDepth) workers := max(2, runtime.GOMAXPROCS(0)/2) + sigverifyWorkers = workers for i := 0; i < workers; i++ { go func() { + // Both are worker-local scratch reused across groups, so a + // steady-state worker allocates nothing per batch. + var ( + group []sigverifyJob + batch sigverify.Batch + ) for job := range sigverifyQueue { - verifySignatures(job.snapshot, job.wg) + group = sigverify.Drain(group, job, sigverifyQueue, + sigverify.FairShare(len(sigverifyQueue), sigverifyWorkers, sigverify.MaxDrain)) + verifySignatureBatch(group, &batch) } }() } diff --git a/pkg/replay/transaction.go b/pkg/replay/transaction.go index 0d15b2c6..d67a53bf 100644 --- a/pkg/replay/transaction.go +++ b/pkg/replay/transaction.go @@ -19,6 +19,8 @@ import ( "github.com/Overclock-Validator/mithril/pkg/metrics" "github.com/Overclock-Validator/mithril/pkg/mlog" "github.com/Overclock-Validator/mithril/pkg/sealevel" + "github.com/Overclock-Validator/mithril/pkg/sigverify" + "github.com/Overclock-Validator/mithril/pkg/statsd" "github.com/Overclock-Validator/mithril/pkg/txverify" "github.com/Overclock-Validator/mithril/pkg/util" bin "github.com/gagliardetto/binary" @@ -491,25 +493,79 @@ func (s *sigverifySnapshot) diagContext() string { s.staticKeys, s.totalKeys, s.lookups, firstSigners, firstKeys) } +// verifySignatures verifies one snapshot. It is the single-job spelling of +// verifySignatureBatch, so the arity check, the failure diagnostics, and the +// halt semantics have exactly one implementation. func verifySignatures(snapshot *sigverifySnapshot, sigverifyWg *sync.WaitGroup) { - defer sigverifyWg.Done() - start := time.Now() + var batch sigverify.Batch + verifySignatureBatch([]sigverifyJob{{snapshot: snapshot, wg: sigverifyWg}}, &batch) +} - if len(snapshot.signers) != len(snapshot.signatures) { - mlog.Log.Errorf("sigverify context: %s", snapshot.diagContext()) - panic(fmt.Sprintf("error - tx %s (version = %d) had mismatched signers/signatures: got %d signers, but %d signatures", - snapshot.txSigString(), snapshot.version, len(snapshot.signers), len(snapshot.signatures))) - } +// verifySignatureBatch verifies every signature across a drained group of jobs +// in one call, then attributes the result back to the transaction it came from. +// +// Grouping is what makes the vectorized backend worth having, and it is safe +// here because the backend reports a verdict PER SIGNATURE rather than a single +// batch-wide answer. The failing signer is therefore identified exactly as +// before, with no re-verification and no loss of diagnostic precision — which +// matters, because an invalid signature is a deliberate process halt and the +// panic message is the only forensic artifact. +// +// batch is caller-owned scratch so a pool worker reuses it across groups. +func verifySignatureBatch(group []sigverifyJob, batch *sigverify.Batch) { + // Release every job's WaitGroup even if verification panics, matching the + // deferred Done() this replaced. A panic halts the process so nothing + // observes the difference today; the defer keeps the contract honest + // against future edits that might recover. + defer func() { + for _, job := range group { + job.wg.Done() + } + }() - for i, sig := range snapshot.signatures { - if snapshot.signers[i].Verify(snapshot.message, sig) { - continue + start := time.Now() + batch.Reset() + for _, job := range group { + snapshot := job.snapshot + if len(snapshot.signers) != len(snapshot.signatures) { + mlog.Log.Errorf("sigverify context: %s", snapshot.diagContext()) + panic(fmt.Sprintf("error - tx %s (version = %d) had mismatched signers/signatures: got %d signers, but %d signatures", + snapshot.txSigString(), snapshot.version, len(snapshot.signers), len(snapshot.signatures))) + } + for i := range snapshot.signatures { + batch.Add((*[32]byte)(&snapshot.signers[i]), snapshot.message, snapshot.signatures[i][:]) + } + } + + if !batch.Verify() { + lane := 0 + for _, job := range group { + snapshot := job.snapshot + for i := range snapshot.signatures { + if !batch.OK(lane) { + mlog.Log.Errorf("sigverify context: %s", snapshot.diagContext()) + panic(fmt.Sprintf("error - tx %s (version = %d) had an invalid signature: invalid signature by %s", + snapshot.txSigString(), snapshot.version, snapshot.signers[i])) + } + lane++ + } } - mlog.Log.Errorf("sigverify context: %s", snapshot.diagContext()) - panic(fmt.Sprintf("error - tx %s (version = %d) had an invalid signature: invalid signature by %s", - snapshot.txSigString(), snapshot.version, snapshot.signers[i])) } - metrics.GlobalBlockReplay.Sigverify.AddTimingSince(start) + + // One observation per group. SumNanoseconds keeps its documented meaning — + // total asynchronous worker time spent verifying — while Count now counts + // groups rather than transactions, so a mean derived from these two is a + // mean per group. The group-width metric below carries the missing factor, + // so the pair stays interpretable. + elapsed := time.Since(start) + metrics.GlobalBlockReplay.Sigverify.AddTiming(elapsed) + + // Replay sigverify had no Prometheus series at all before this. Width is + // the one worth watching: it is the difference between paying for a vector + // group and using it, and no backend setting can compensate for work that + // arrives too thinly to fill one. + _ = statsd.Duration(statsd.ReplaySigverifyGroup, elapsed, nil) + statsd.Count(statsd.ReplaySigverifyGroupSignatures, int64(batch.Len()), nil) } func cloneTransaction(tx *solana.Transaction) (*solana.Transaction, error) { diff --git a/pkg/sealevel/ed25519_program.go b/pkg/sealevel/ed25519_program.go index 3ee79890..179c6e22 100644 --- a/pkg/sealevel/ed25519_program.go +++ b/pkg/sealevel/ed25519_program.go @@ -1,13 +1,15 @@ package sealevel import ( + stded25519 "crypto/ed25519" + "bytes" "encoding/binary" "io" "math" "github.com/Overclock-Validator/mithril/pkg/features" - "github.com/oasisprotocol/curve25519-voi/primitives/ed25519" + "github.com/Overclock-Validator/mithril/pkg/sigverify" ) const DataStart = (SignatureOffsetsSerializedSize + SignatureOffsetStarts) @@ -127,17 +129,29 @@ func Ed25519ProgramExecute(execCtx *ExecutionCtx) error { return PrecompileErrDataOffset } - pk := ed25519.PublicKey(pubkey) + if len(pubkey) != PubkeySerializedSize { + return PrecompileErrDataOffset + } + // Signatures are verified one at a time on purpose. The reference + // walks entries in order and returns the FIRST error, so batching + // these would let a later entry's offset error preempt an earlier + // entry's signature error. That error code reaches the ledger, so the + // ordering is consensus-visible and is not worth trading for the + // throughput of a batch that is usually one or two signatures deep. if execCtx.Features.IsActive(features.Ed25519PrecompileVerifyStrict) { - verifyOptions := ed25519.VerifyOptions{AllowSmallOrderA: false, AllowSmallOrderR: false, CofactorlessVerify: true} - opts := ed25519.Options{Verify: &verifyOptions} - - if !ed25519.VerifyWithOptions(pk, msg[:offsets.MessageDataSize], signature[:64], &opts) { + // DalekStrict: reject small-order A and R, accept a non-canonical + // A and hash its original bytes. + if !sigverify.VerifyOne((*[32]byte)(pubkey), msg[:offsets.MessageDataSize], signature[:64]) { return PrecompileErrSignature } } else { - if !ed25519.Verify(pk, msg[:offsets.MessageDataSize], signature[:64]) { + // Before the feature gate the reference used plain (non-strict) + // verification, which is exactly crypto/ed25519.Verify: cofactorless, + // no small-order rejection, non-canonical A accepted, R compared as + // bytes. narya exposes no StdlibCompat entry point, and the stdlib + // is definitionally correct here, so this path uses it directly. + if !stded25519.Verify(stded25519.PublicKey(pubkey), msg[:offsets.MessageDataSize], signature[:64]) { return PrecompileErrSignature } } diff --git a/pkg/sealevel/ed25519_program_test.go b/pkg/sealevel/ed25519_program_test.go new file mode 100644 index 00000000..d5bc6dae --- /dev/null +++ b/pkg/sealevel/ed25519_program_test.go @@ -0,0 +1,210 @@ +package sealevel + +import ( + stded25519 "crypto/ed25519" + + "crypto/rand" + "encoding/binary" + "testing" + + "filippo.io/edwards25519" + "github.com/Overclock-Validator/mithril/pkg/accounts" + a "github.com/Overclock-Validator/mithril/pkg/addresses" + "github.com/Overclock-Validator/mithril/pkg/cu" + "github.com/Overclock-Validator/mithril/pkg/features" + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" +) + +// These drive the whole precompile -- offsets parsing and predicate together -- +// through ProcessInstruction, rather than calling the verifier underneath it. +// The 3479 Firedancer fixtures in conformance/ cover the same path far more +// broadly, but they need a ~7 GB external corpus, so they skip in an ordinary +// "go test ./...". These cost microseconds and always run. +// +// On the case that is deliberately absent: verify_strict accepts a +// non-canonical A and hashes its original bytes, and it would be natural to +// pin that here. It cannot be done. A non-canonical encoding requires y < 19, +// and every curve point with such a y whose discrete log is computable is +// already small-order, so it is rejected by the small-order gate before +// canonicality is ever consulted. Every one of the 152 non-canonical-A fixtures +// in the Firedancer corpus expects an error for exactly that reason. A test +// written for this bullet would pass identically whether or not the +// implementation handles non-canonical A correctly, so it would look like +// coverage while proving nothing. + +// buildEd25519Instruction lays out precompile instruction data for one +// signature. Field order matches Ed25519SignatureOffsets: signature_offset, +// signature_instruction_index, public_key_offset, public_key_instruction_index, +// message_data_offset, message_data_size, message_instruction_index. Index +// 0xffff means "this instruction's own data". +func buildEd25519Instruction(pubkey, signature, message []byte) []byte { + const currentInstruction = 0xFFFF + base := SignatureOffsetStarts + SignatureOffsetsSerializedSize + + data := make([]byte, 0, base+len(pubkey)+len(signature)+len(message)) + data = append(data, 1, 0) // one signature, then one padding byte + + put := func(v int) { data = binary.LittleEndian.AppendUint16(data, uint16(v)) } + put(base + len(pubkey)) // signature_offset + put(currentInstruction) // + put(base) // public_key_offset + put(currentInstruction) // + put(base + len(pubkey) + len(signature)) // message_data_offset + put(len(message)) // message_data_size + put(currentInstruction) // + + data = append(data, pubkey...) + data = append(data, signature...) + return append(data, message...) +} + +// runEd25519Precompile dispatches instruction data the same way the runtime +// does, so program-id routing and the instruction stack are exercised too. +func runEd25519Precompile(t *testing.T, data []byte) error { + t.Helper() + return runEd25519PrecompileWithStrict(t, data, true) +} + +// runEd25519PrecompileWithStrict lets a caller drive the pre-activation branch. +// Ed25519PrecompileVerifyStrict gates the predicate, and blocks from before it +// activated were validated without it, so historical replay has to be able to +// reach the non-strict path. +func runEd25519PrecompileWithStrict(t *testing.T, data []byte, strict bool) error { + t.Helper() + + programAcct := accounts.Account{ + Key: solana.PublicKeyFromBytes(a.Ed25519PrecompileAddr[:]), + Lamports: 1, + Data: []byte{}, + Owner: a.NativeLoaderAddr, + Executable: true, + } + txAccts := NewTransactionAccounts([]accounts.Account{programAcct}) + txCtx := NewTransactionCtx(*txAccts, 5, 64) + txCtx.AllInstructions = append(txCtx.AllInstructions, Instruction{Data: data}) + + execCtx := ExecutionCtx{ + TransactionContext: txCtx, + ComputeMeter: cu.NewComputeMeter(200000), + Log: &LogRecorder{}, + } + execCtx.Accounts = accounts.NewMemAccounts() + execCtx.Features = *features.NewFeaturesDefault() + if strict { + execCtx.Features.EnableFeature(features.Ed25519PrecompileVerifyStrict, 0) + } + + return execCtx.ProcessInstruction(data, []InstructionAccount{}, []uint64{0}) +} + +func TestEd25519PrecompileAcceptsAValidSignature(t *testing.T) { + pub, priv, err := stded25519.GenerateKey(rand.Reader) + require.NoError(t, err) + + message := []byte("ed25519 precompile end to end") + sig := stded25519.Sign(priv, message) + + require.NoError(t, runEd25519Precompile(t, buildEd25519Instruction(pub, sig, message)), + "a valid signature must be accepted") +} + +// Both encodings below are order-4 points, taken from the fourteen strings a +// permissive decoder maps into the 8-torsion subgroup. verify_strict rejects +// them before evaluating the equation, which is the entire difference between +// the strict predicate and a plain stdlib verify. +func TestEd25519PrecompileRejectsSmallOrderPoints(t *testing.T) { + pub, priv, err := stded25519.GenerateKey(rand.Reader) + require.NoError(t, err) + + message := []byte("ed25519 precompile end to end") + sig := stded25519.Sign(priv, message) + + smallOrder := make([]byte, PubkeySerializedSize) // y = 0, canonical spelling + + t.Run("small-order A", func(t *testing.T) { + err := runEd25519Precompile(t, buildEd25519Instruction(smallOrder, sig, message)) + require.ErrorIs(t, err, PrecompileErrSignature, + "a small-order public key must be rejected") + }) + + t.Run("small-order R", func(t *testing.T) { + spliced := make([]byte, SignatureSerializedSize) + copy(spliced, smallOrder) + copy(spliced[32:], sig[32:]) + + err := runEd25519Precompile(t, buildEd25519Instruction(pub, spliced, message)) + require.ErrorIs(t, err, PrecompileErrSignature, + "a small-order R must be rejected") + }) +} + +// A tampered signature is well-formed and decodes cleanly, so it fails on the +// equation rather than on any byte-level gate. This separates "the predicate +// rejects malformed input" from "the arithmetic actually runs". +func TestEd25519PrecompileRejectsATamperedSignature(t *testing.T) { + pub, priv, err := stded25519.GenerateKey(rand.Reader) + require.NoError(t, err) + + message := []byte("ed25519 precompile end to end") + sig := stded25519.Sign(priv, message) + sig[40] ^= 0x01 + + err = runEd25519Precompile(t, buildEd25519Instruction(pub, sig, message)) + require.ErrorIs(t, err, PrecompileErrSignature, + "a tampered signature must fail the equation") +} + +// smallOrderForgery builds a signature the standard library accepts and the +// strict predicate rejects: A is the identity, so [s]B - [k]A collapses to +// [s]B, which is exactly the R the signature carries. Nothing about it is +// forged in the usual sense -- it satisfies the equation. It is rejected only +// because strict verification refuses a small-order public key. +func smallOrderForgery(t *testing.T) (pub [32]byte, sig []byte) { + t.Helper() + + pub[0] = 1 // canonical identity: y = 1, sign bit clear + + uniform := make([]byte, 64) + _, err := rand.Read(uniform) + require.NoError(t, err) + s, err := edwards25519.NewScalar().SetUniformBytes(uniform) + require.NoError(t, err) + + r := (&edwards25519.Point{}).ScalarBaseMult(s) + + sig = make([]byte, SignatureSerializedSize) + copy(sig[:32], r.Bytes()) + copy(sig[32:], s.Bytes()) + return pub, sig +} + +// The feature gate is the whole difference between the two branches, so pin it +// with one input and both feature states. Historical replay depends on the +// inactive branch staying non-strict: blocks from before activation were +// validated that way, and re-verifying them strictly would reject transactions +// the network accepted, producing a different bank hash. +// +// This also guards the failure mode that actually bit us. When the conformance +// fixtures parsed with an empty feature set, the gate read inactive, the +// non-strict branch ran, and 163 signatures were accepted that should have been +// rejected. A feature-plumbing bug became an acceptance change. Pinning both +// directions means that shows up as a test failure rather than a quiet fork. +func TestEd25519PrecompileFeatureGateSelectsThePredicate(t *testing.T) { + message := []byte("historical replay") + pub, sig := smallOrderForgery(t) + data := buildEd25519Instruction(pub[:], sig, message) + + require.True(t, stded25519.Verify(pub[:], message, sig), + "premise: this signature satisfies the stdlib equation") + + t.Run("feature active rejects it", func(t *testing.T) { + require.ErrorIs(t, runEd25519PrecompileWithStrict(t, data, true), PrecompileErrSignature, + "strict verification must refuse a small-order public key") + }) + + t.Run("feature inactive accepts it", func(t *testing.T) { + require.NoError(t, runEd25519PrecompileWithStrict(t, data, false), + "pre-activation replay must reproduce the non-strict predicate") + }) +} diff --git a/pkg/sealevel/native_programs_common.go b/pkg/sealevel/native_programs_common.go index f7bd393e..b48ccd71 100644 --- a/pkg/sealevel/native_programs_common.go +++ b/pkg/sealevel/native_programs_common.go @@ -18,6 +18,17 @@ func resolveNativeProgramById(programId [32]byte) (func(ctx *ExecutionCtx) error return SystemProgramExecute, a.SystemProgramAddrStr, nil case a.VoteProgramAddr: return VoteProgramExecute, a.VoteProgramAddrStr, nil + case a.AddressLookupTableAddr: + return AddressLookupTableExecute, a.AddressLookupTableProgramAddrStr, nil + // Config and Stake have been migrated to BPF on mainnet, so in practice + // their accounts are loader-owned and route through the loader instead. + // The caller only reaches this resolver when the program account's owner is + // NativeLoader, which is the pre-migration shape, so these cases exist for + // historical replay and cannot shadow the migrated versions. + case a.ConfigProgramAddr: + return ConfigProgramExecute, a.ConfigProgramAddrStr, nil + case a.StakeProgramAddr: + return StakeProgramExecute, a.StakeProgramAddrStr, nil case a.ComputeBudgetProgramAddr: return ComputeBudgetExecute, a.ComputeBudgetProgramAddrStr, nil case a.BpfLoader2Addr: diff --git a/pkg/sealevel/native_programs_common_test.go b/pkg/sealevel/native_programs_common_test.go new file mode 100644 index 00000000..b22a4be1 --- /dev/null +++ b/pkg/sealevel/native_programs_common_test.go @@ -0,0 +1,56 @@ +package sealevel + +import ( + "testing" + + a "github.com/Overclock-Validator/mithril/pkg/addresses" + "github.com/stretchr/testify/require" +) + +// A native program with an implementation but no resolver case is invisible: +// resolveNativeProgramById returns InstrErrUnsupportedProgramId and the program +// simply never runs. That is how AddressLookupTableExecute sat unreachable while +// its own 31 unit tests failed, and it fails quietly rather than at build time +// because the implementation still compiles and is still exported. +// +// This pins the mapping so adding an implementation without wiring it, or +// dropping a case during a refactor, shows up here. +func TestEveryNativeProgramResolves(t *testing.T) { + for _, tc := range []struct { + name string + address [32]byte + want string + }{ + {"system", a.SystemProgramAddr, a.SystemProgramAddrStr}, + {"vote", a.VoteProgramAddr, a.VoteProgramAddrStr}, + {"stake", a.StakeProgramAddr, a.StakeProgramAddrStr}, + {"config", a.ConfigProgramAddr, a.ConfigProgramAddrStr}, + {"address lookup table", a.AddressLookupTableAddr, a.AddressLookupTableProgramAddrStr}, + {"compute budget", a.ComputeBudgetProgramAddr, a.ComputeBudgetProgramAddrStr}, + {"bpf loader v2", a.BpfLoader2Addr, a.BpfLoader2AddrStr}, + {"bpf loader deprecated", a.BpfLoaderDeprecatedAddr, a.BpfLoaderDeprecatedAddrStr}, + {"bpf loader upgradeable", a.BpfLoaderUpgradeableAddr, a.BpfLoaderUpgradeableAddrStr}, + {"loader v4", a.LoaderV4Addr, a.LoaderV4AddrStr}, + {"zk elgamal proof", a.ZkElgamalProofProgramAddr, a.ZkElgamalProofProgramAddrStr}, + {"ed25519 precompile", a.Ed25519PrecompileAddr, a.Ed25519PrecompileAddrStr}, + {"secp256k1 precompile", a.Secp256kPrecompileAddr, a.Secp256kPrecompileAddrStr}, + {"secp256r1 precompile", a.Secp256r1PrecompileAddr, a.Secp256r1PrecompileAddrStr}, + } { + t.Run(tc.name, func(t *testing.T) { + execute, name, err := resolveNativeProgramById(tc.address) + require.NoError(t, err, "%s has an implementation but no resolver case", tc.name) + require.NotNil(t, execute) + require.Equal(t, tc.want, name) + }) + } +} + +// The resolver must not claim an address it has no implementation for, or a +// BPF-owned program would be shadowed by a nonexistent builtin. +func TestUnknownProgramIdDoesNotResolve(t *testing.T) { + var unknown [32]byte + unknown[0] = 0xAB + + _, _, err := resolveNativeProgramById(unknown) + require.ErrorIs(t, err, InstrErrUnsupportedProgramId) +} diff --git a/pkg/sigverify/configure_test.go b/pkg/sigverify/configure_test.go new file mode 100644 index 00000000..75e11dba --- /dev/null +++ b/pkg/sigverify/configure_test.go @@ -0,0 +1,175 @@ +package sigverify + +import ( + "fmt" + "os" + "os/exec" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Backend selection is one-shot per process, by design: the library pins its +// backend on first use so the key cache can never hold tables in two formats. +// Every case here therefore needs its own process. Running them in-process +// would let the first Configure win and turn the rest into vacuous passes, +// which is worse than not testing them. +const configureChildEnv = "MITHRIL_SIGVERIFY_CONFIGURE_CHILD" + +// runConfigureChild re-executes this test binary and runs only the named +// subtest, with the child marker set. +func runConfigureChild(t *testing.T, name string) (string, error) { + t.Helper() + + cmd := exec.Command(os.Args[0], "-test.run", "^"+name+"$", "-test.v") + cmd.Env = append(os.Environ(), configureChildEnv+"=1") + out, err := cmd.CombinedOutput() + return string(out), err +} + +func inChild() bool { return os.Getenv(configureChildEnv) == "1" } + +// TestConfigureBackends covers every accepted name plus a rejected one. +// +// r51 has no fallback by contract: on a CPU without AVX512-IFMA it must fail +// startup rather than quietly running the portable backend, because an operator +// who asked for the accelerated path needs to know they did not get it. The +// assertion is written to hold on both kinds of machine -- it accepts either +// "r51 resolved" or "a clear error", and rejects the one outcome that would be +// a bug, namely success while some other backend is active. +func TestConfigureBackends(t *testing.T) { + cases := []struct { + name string + backend string + }{ + {"Auto", BackendAuto}, + {"R51", BackendR51}, + {"Generic", BackendGeneric}, + {"Stdlib", BackendStdlib}, + {"Empty", ""}, + {"Unknown", "definitely-not-a-backend"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + full := "TestConfigureBackends/" + tc.name + if !inChild() { + out, err := runConfigureChild(t, full) + require.NoError(t, err, "child output:\n%s", out) + require.Contains(t, out, "PASS") + return + } + + resolved, err := Configure(Config{Backend: tc.backend}) + + switch tc.backend { + case "definitely-not-a-backend": + require.Error(t, err, "an unknown backend must be rejected") + assert.Contains(t, err.Error(), "must be one of") + assert.Empty(t, resolved) + // A rejected name must not become visible anywhere. + assert.Equal(t, BackendAuto, Cfg.Backend, + "a rejected backend must not be published to Cfg") + + case BackendR51: + if err != nil { + // The only acceptable failure is "this CPU cannot do it". + assert.Contains(t, err.Error(), BackendR51) + return + } + assert.Equal(t, BackendR51, resolved, + "r51 must not silently resolve to a different backend") + + case "", BackendAuto: + require.NoError(t, err) + assert.NotEmpty(t, resolved) + assert.Equal(t, BackendAuto, Cfg.Backend, + "an empty backend must default to auto") + + default: + require.NoError(t, err) + assert.Equal(t, tc.backend, resolved) + assert.Equal(t, tc.backend, Cfg.Backend) + } + }) + } +} + +// Configure is a startup-only operation. A second call must be refused rather +// than partially applied: the library has already pinned its backend, so a late +// switch would leave the process in a state neither caller asked for. +func TestConfigureRefusesASecondCall(t *testing.T) { + if !inChild() { + out, err := runConfigureChild(t, t.Name()) + require.NoError(t, err, "child output:\n%s", out) + require.Contains(t, out, "PASS") + return + } + + first, err := Configure(Config{Backend: BackendGeneric}) + require.NoError(t, err) + require.Equal(t, BackendGeneric, first) + + second, err := Configure(Config{Backend: BackendStdlib}) + require.Error(t, err, "a second Configure must be refused") + assert.Empty(t, second) + assert.Contains(t, err.Error(), "already configured") + + // The refusal must leave the first selection intact rather than half-applied. + assert.Equal(t, BackendGeneric, Cfg.Backend) + assert.Equal(t, BackendGeneric, Backend()) +} + +// Re-stating the same backend is still a second call, and is still refused. +// Treating it as a harmless no-op would make "Configure ran twice" invisible, +// and the second caller has no way to know its configuration was ignored. +func TestConfigureRefusesARepeatOfTheSameBackend(t *testing.T) { + if !inChild() { + out, err := runConfigureChild(t, t.Name()) + require.NoError(t, err, "child output:\n%s", out) + require.Contains(t, out, "PASS") + return + } + + _, err := Configure(Config{Backend: BackendGeneric}) + require.NoError(t, err) + + _, err = Configure(Config{Backend: BackendGeneric}) + require.Error(t, err, "repeating the same backend is still a second call") +} + +// A verification must work without Configure ever being called: not every entry +// point into the codebase runs node startup, and defaulting to no backend at all +// would fail closed in a way that looks like a signature problem. +func TestVerificationWorksWithoutConfigure(t *testing.T) { + if !inChild() { + out, err := runConfigureChild(t, t.Name()) + require.NoError(t, err, "child output:\n%s", out) + require.Contains(t, out, "PASS") + return + } + + signed := makeSigned(t, 0, true) + assert.True(t, VerifyOne(&signed.pub, signed.msg, signed.sig), + "a valid signature must verify before Configure is called") + assert.NotEmpty(t, Backend(), "some backend must be active by default") +} + +// Guards the subprocess plumbing itself. If the child marker or the -test.run +// pattern ever stops matching, every test above would spawn a child that runs +// nothing, exit zero, and report a pass without asserting anything. +func TestConfigureChildPlumbingActuallyRuns(t *testing.T) { + if inChild() { + fmt.Println("child-marker-observed") + return + } + + out, err := runConfigureChild(t, t.Name()) + require.NoError(t, err, "child output:\n%s", out) + require.Contains(t, out, "child-marker-observed", + "the child did not execute the subtest; the harness is not testing anything") + require.Equal(t, 1, strings.Count(out, "child-marker-observed"), + "the -test.run pattern matched more than the intended subtest") +} diff --git a/pkg/sigverify/drain.go b/pkg/sigverify/drain.go new file mode 100644 index 00000000..f243c1ba --- /dev/null +++ b/pkg/sigverify/drain.go @@ -0,0 +1,87 @@ +package sigverify + +// BatchTarget is the group width worth aiming for: the vectorized backend +// verifies eight signatures per AVX-512 group, so eight is the point at which +// every lane is busy and the per-signature cost stops falling steeply. +// +// It is a TARGET, never a threshold. Nothing in this package waits to reach it. +// A producer that meters work out to its workers should hand over roughly this +// many per worker so a group is reachable at all; a consumer should take +// whatever is actually queued. +const BatchTarget = 8 + +// MaxDrain is how many work items a verification worker will coalesce into one +// batch. +// +// The accelerated backend verifies eight signatures per AVX-512 group, so any +// multiple of eight keeps every lane busy. 64 is eight full groups: large +// enough that per-batch overhead is negligible, small enough that one worker +// cannot monopolise a shared queue while its peers idle, and small enough that +// the scratch a worker holds stays in cache. +const MaxDrain = 64 + +// FairShare reports how many items one worker may take in a single pass, +// given how many are queued behind it and how many workers share the queue. +// +// Draining unconditionally is a trap. Eight workers facing eight queued items +// would let the first worker take all eight and verify them as one group while +// the other seven idle — one group of eight costs more wall-clock than eight +// workers each verifying one, so the "optimization" would be a latency +// regression exactly when the queue is shallow. Batching buys throughput per +// core; it must never buy it with parallelism that was already available. +// +// So a worker takes its share and no more: one item plus an equal cut of what +// remains. A shallow queue spreads across workers, and a deep queue — the +// catch-up case, where every worker is saturated regardless — gives everyone +// full groups. +// +// max caps the result. The floor is 1: the item already in hand is never +// given back, which is what keeps this incapable of stranding work. +func FairShare(queued, workers, max int) int { + if workers < 1 { + workers = 1 + } + share := queued/workers + 1 + if share > max { + share = max + } + if share < 1 { + share = 1 + } + return share +} + +// Drain coalesces work from ch into dst, starting with an item the caller has +// already received. +// +// The shape matters and is the whole point of this helper. The caller blocks +// receiving the first item — a worker with nothing to do should sleep, not +// spin — and everything after that is a non-blocking peek. So Drain NEVER waits +// for a batch to fill. There is no timer and no minimum size. +// +// That is what makes batching safe to bolt onto latency-sensitive paths: when +// work is scarce, a batch is whatever happened to be queued (often one item) +// and latency is unchanged; when work is abundant, batches fill naturally and +// the accelerated path pays off. A batching timer would trade the first case +// away to improve the second, and the first case is the tip of the chain. +// +// dst is reused across calls; pass the same worker-local slice every time and +// Drain will not allocate after it has grown once. +func Drain[T any](dst []T, first T, ch <-chan T, max int) []T { + if max < 1 { + max = 1 + } + dst = append(dst[:0], first) + for len(dst) < max { + select { + case item, open := <-ch: + if !open { + return dst + } + dst = append(dst, item) + default: + return dst + } + } + return dst +} diff --git a/pkg/sigverify/sigverify.go b/pkg/sigverify/sigverify.go new file mode 100644 index 00000000..fae00447 --- /dev/null +++ b/pkg/sigverify/sigverify.go @@ -0,0 +1,220 @@ +// Package sigverify is Mithril's single entry point for ed25519 transaction +// signature verification. +// +// It exists for two reasons. +// +// Predicate. Solana mainnet verifies transaction signatures with +// ed25519-dalek's verify_strict, which is Go's crypto/ed25519.Verify plus +// rejection of small-order A and small-order R. Verifying with plain stdlib +// therefore ACCEPTS a class of signatures mainnet REJECTS — a divergence from +// the invariant that Mithril reproduce mainnet state exactly. Every path here +// applies the strict predicate. +// +// Throughput. The underlying library verifies eight signatures per AVX-512 +// group, so cost per signature is a strong function of how many signatures are +// handed over at once: on Zen 5 a lone signature costs ~22.9us while a group of +// eight costs ~6.1us each. Callers must therefore batch. Batch is the shape +// that makes that easy and allocation-free; Drain is the policy for filling it +// from a work channel. +package sigverify + +import ( + "fmt" + "sync" + + narya "github.com/Overclock-Validator/narya-ed25519/ed25519" +) + +// Backend names accepted by Config.Backend. +const ( + // BackendAuto prefers the AVX-512 backend and silently falls back to the + // portable one when the CPU lacks AVX512-IFMA. + BackendAuto = "auto" + // BackendR51 forces the AVX-512 backend. Startup fails on a CPU without + // AVX512-IFMA rather than silently degrading. + BackendR51 = "r51" + // BackendGeneric forces the portable pure-Go backend. + BackendGeneric = "generic" + // BackendStdlib uses Go's crypto/ed25519-backed arithmetic, but with the + // strictness checks applied first. + BackendStdlib = "stdlib" +) + +// Config selects the verification backend. It is deliberately tiny: the +// library's own defaults are good, and every knob here is a consensus-visible +// or performance-visible choice that an operator should have to state. +type Config struct { + Backend string +} + +// Defaults returns the configuration used when the operator sets nothing. +func Defaults() Config { return Config{Backend: BackendAuto} } + +// Cfg is the live configuration, set once by Configure during startup and +// read-only afterwards. It follows the same shape as replay.TrailingVerifierCfg. +var Cfg = Defaults() + +// Configure resolves cfg and installs the backend. It returns the name of the +// backend actually selected, which the caller should log — with BackendAuto the +// resolved name is the only way an operator learns whether they got the +// accelerated path. +// +// It must be called exactly once, before any verification. Calling it twice +// returns an error rather than silently ignoring the second call, because the +// underlying library pins its backend on first use and a late switch would +// leave the process in a state neither caller asked for. +func Configure(cfg Config) (string, error) { + if cfg.Backend == "" { + cfg.Backend = Defaults().Backend + } + + configureMu.Lock() + defer configureMu.Unlock() + + if configuredBackend != "" { + return "", fmt.Errorf( + "sigverify: already configured with backend %q; Configure is a startup-only operation", + configuredBackend) + } + + // Validate before publishing anything. Assigning Cfg first would leave a + // rejected backend name visible to Backend() and to the startup log. + switch cfg.Backend { + case BackendAuto, BackendR51, BackendGeneric, BackendStdlib: + default: + return "", fmt.Errorf( + "sigverify.backend must be one of %q, %q, %q, %q; got %q", + BackendAuto, BackendR51, BackendGeneric, BackendStdlib, cfg.Backend) + } + + resolved, err := installBackend(cfg.Backend) + if err != nil { + return "", err + } + + Cfg = cfg + configuredBackend = resolved + return resolved, nil +} + +// configureMu guards the one-shot handoff. Configure runs during startup while +// verification runs on pool goroutines, so the published state needs a barrier +// even though the write happens once. +var ( + configureMu sync.Mutex + configuredBackend string +) + +func installBackend(backend string) (string, error) { + // The strict predicate is not optional and not configurable: it is what + // mainnet does. Set it before selecting a backend so no window exists in + // which a verification could run under the compat predicate. + narya.SetDefaultProfile(narya.DalekStrict) + + switch backend { + case BackendAuto: + // Try the accelerated backend, accept the portable one. An error here + // means "this CPU lacks AVX512-IFMA", which is the expected answer on + // most hardware and not a startup failure. + if err := narya.SetBackend(BackendR51); err == nil { + return narya.ActiveBackend(), nil + } + if err := narya.SetBackend(BackendGeneric); err != nil { + return "", fmt.Errorf("sigverify: select portable backend: %w", err) + } + return narya.ActiveBackend(), nil + + case BackendR51, BackendGeneric, BackendStdlib: + // Deliberately no fallback. BackendR51 on a CPU without AVX512-IFMA is + // an operator asking for something the hardware cannot provide, and + // silently degrading would hide that. + if err := narya.SetBackend(backend); err != nil { + return "", fmt.Errorf("sigverify: select backend %q: %w", backend, err) + } + return narya.ActiveBackend(), nil + } + + // Unreachable: Configure validates the name before calling in. + return "", fmt.Errorf("sigverify: unhandled backend %q", backend) +} + +// Backend reports the backend in use, for metrics and diagnostics. +func Backend() string { + return narya.ActiveBackend() +} + +// InternalFaultFallbacks reports how many times the accelerated backend hit an +// internal fault and recomputed the work on the portable backend. It should be +// zero forever; a nonzero value is a bug in the accelerated backend, not an +// input-dependent condition, and is worth alerting on. +func InternalFaultFallbacks() uint64 { + return narya.ActiveBackendStats().InternalFaultFallbacks +} + +// VerifyOne verifies a single signature under the strict predicate. +// +// Prefer Batch. This costs roughly 3.7x per signature what the same work costs +// inside a group of eight, so it is for paths that genuinely have one signature +// and no way to accumulate more. +func VerifyOne(pub *[32]byte, msg, sig []byte) bool { + if pub == nil { + return false + } + return narya.VerifyStrict(pub[:], msg, sig) +} + +// Batch accumulates signatures and verifies them in one call. It is reusable +// worker-local scratch: Reset keeps the backing arrays, so a worker that loops +// on Reset/Add/Verify allocates nothing after the first batch. +// +// A Batch is not safe for concurrent use. Give each worker its own. +type Batch struct { + pubs []*[32]byte + msgs [][]byte + sigs [][]byte + ok []bool +} + +// Reset empties the batch while retaining capacity. +func (b *Batch) Reset() { + // Clear the pointer-bearing slots so a finished batch does not pin public + // keys, messages, and signatures alive until the worker's next batch + // happens to overwrite that index. + clear(b.pubs) + clear(b.msgs) + clear(b.sigs) + b.pubs = b.pubs[:0] + b.msgs = b.msgs[:0] + b.sigs = b.sigs[:0] + b.ok = b.ok[:0] +} + +// Add appends one signature. pub must remain valid until Verify returns. +func (b *Batch) Add(pub *[32]byte, msg, sig []byte) { + b.pubs = append(b.pubs, pub) + b.msgs = append(b.msgs, msg) + b.sigs = append(b.sigs, sig) + b.ok = append(b.ok, false) +} + +// Len reports how many signatures are queued. +func (b *Batch) Len() int { return len(b.pubs) } + +// Verify checks every queued signature and reports whether all of them passed. +// Per-signature verdicts are available from OK afterwards either way, so a +// caller that needs to identify WHICH signature failed does not have to +// re-verify anything. +func (b *Batch) Verify() bool { + // Recorded here rather than at each drain site so a new caller cannot forget + // to instrument itself. See stats.go for why width, not count, is the metric. + observeBatchWidth(len(b.pubs)) + + if len(b.pubs) == 0 { + return true + } + return narya.VerifyBatchStrict(b.pubs, b.msgs, b.sigs, b.ok) +} + +// OK reports the verdict for the i'th queued signature. It is only meaningful +// after Verify. +func (b *Batch) OK(i int) bool { return b.ok[i] } diff --git a/pkg/sigverify/sigverify_test.go b/pkg/sigverify/sigverify_test.go new file mode 100644 index 00000000..bf52a340 --- /dev/null +++ b/pkg/sigverify/sigverify_test.go @@ -0,0 +1,321 @@ +package sigverify + +import ( + stded25519 "crypto/ed25519" + "crypto/rand" + "fmt" + "os" + "os/exec" + "testing" + + "filippo.io/edwards25519" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// smallOrderForgery builds a signature that Go's stdlib accepts and that +// mainnet rejects, which is the entire reason this package exists. +// +// The construction: let the public key A be the identity point. The +// verification equation is [s]B == R + [k]A, and [k]A is the identity for every +// k when A is the identity — so the challenge drops out and ANY (R, s) with +// R == [s]B satisfies it, for ANY message, with no private key involved. +// +// stdlib implements exactly that equation and accepts. ed25519-dalek's +// verify_strict — what Agave and therefore mainnet use — additionally rejects a +// small-order A, so it refuses. A validator that accepts these is accepting +// transactions mainnet does not. +func smallOrderForgery(tb testing.TB, message []byte) (pub [32]byte, sig []byte) { + tb.Helper() + + // Canonical encoding of the identity: y = 1, sign bit clear. + pub[0] = 1 + + uniform := make([]byte, 64) + _, err := rand.Read(uniform) + require.NoError(tb, err) + s, err := edwards25519.NewScalar().SetUniformBytes(uniform) + require.NoError(tb, err) + + r := (&edwards25519.Point{}).ScalarBaseMult(s) + + sig = make([]byte, 64) + copy(sig[:32], r.Bytes()) + copy(sig[32:], s.Bytes()) + return pub, sig +} + +func TestSmallOrderForgeryIsAcceptedByStdlibAndRejectedHere(t *testing.T) { + message := []byte("transfer everything") + pub, sig := smallOrderForgery(t, message) + + require.True(t, stded25519.Verify(pub[:], message, sig), + "precondition: the forgery must be accepted by stdlib, or the test proves nothing") + + assert.False(t, VerifyOne(&pub, message, sig), + "strict verification must reject a small-order public key") + + var batch Batch + batch.Add(&pub, message, sig) + assert.False(t, batch.Verify(), "batch path must reject it too") + assert.False(t, batch.OK(0)) +} + +// backend=stdlib swaps the arithmetic, never the acceptance rule. An earlier +// revision routed this name straight at crypto/ed25519, so selecting it +// silently dropped the small-order rejection and let an operator flag decide +// which signatures the node accepts. +// +// The library allows one backend selection per process, deliberately, so the +// key cache can never hold tables in two formats. Any other test that calls +// Configure first would therefore make this one skip, and a test that skips in +// the ordinary "go test ./..." run guards nothing. It re-executes itself in a +// child process instead, so the assertion always actually runs. +const stdlibChildEnv = "MITHRIL_SIGVERIFY_STDLIB_BACKEND_CHILD" + +func TestStdlibBackendStillRejectsTheSmallOrderForgery(t *testing.T) { + message := []byte("transfer everything") + pub, sig := smallOrderForgery(t, message) + + // The forgery is only interesting because the standard library accepts it. + // If that stops holding, this test proves nothing. + require.True(t, stded25519.Verify(pub[:], message, sig), + "premise: crypto/ed25519 accepts this forgery") + + if os.Getenv(stdlibChildEnv) != "1" { + cmd := exec.Command(os.Args[0], + "-test.run", "^"+t.Name()+"$", "-test.v") + cmd.Env = append(os.Environ(), stdlibChildEnv+"=1") + out, err := cmd.CombinedOutput() + require.NoError(t, err, "child process output:\n%s", out) + require.Contains(t, string(out), "PASS") + return + } + + resolved, err := Configure(Config{Backend: BackendStdlib}) + require.NoError(t, err) + require.Equal(t, BackendStdlib, resolved) + + assert.False(t, VerifyOne(&pub, message, sig), + "backend=stdlib accepted a small-order forgery; it must swap arithmetic, not the predicate") + + var batch Batch + batch.Add(&pub, message, sig) + assert.False(t, batch.Verify(), "batch path accepted it under backend=stdlib") + assert.False(t, batch.OK(0)) +} + +type signedMessage struct { + pub [32]byte + msg []byte + sig []byte + valid bool +} + +func makeSigned(tb testing.TB, index int, valid bool) signedMessage { + tb.Helper() + pubKey, privKey, err := stded25519.GenerateKey(rand.Reader) + require.NoError(tb, err) + + msg := []byte(fmt.Sprintf("message number %d", index)) + sig := stded25519.Sign(privKey, msg) + if !valid { + // Flip a bit in s rather than truncating, so the signature stays + // well-formed and is rejected on the equation rather than on a length + // or range precheck. + sig[40] ^= 0x01 + } + var pub [32]byte + copy(pub[:], pubKey) + return signedMessage{pub: pub, msg: msg, sig: sig, valid: valid} +} + +func TestValidSignaturesAgreeWithStdlib(t *testing.T) { + for i := 0; i < 16; i++ { + item := makeSigned(t, i, true) + require.True(t, stded25519.Verify(item.pub[:], item.msg, item.sig)) + assert.True(t, VerifyOne(&item.pub, item.msg, item.sig), + "honest signature %d must verify", i) + } +} + +// Batching must not change any verdict. This is the property that lets the +// replay pool keep its panic-with-exact-signer contract: a batch reports +// per-signature results, and they have to be the results the caller would have +// got one at a time. +func TestBatchVerdictsMatchPerItemVerdicts(t *testing.T) { + // Widths straddling the x4 and x8 group boundaries, plus the tails either + // side of them, are where a lane-mapping bug would hide. + for _, width := range []int{1, 2, 3, 4, 5, 7, 8, 9, 12, 16, 17, 31, 64} { + t.Run(fmt.Sprintf("width=%d", width), func(t *testing.T) { + // Put the single invalid signature at every position in turn, so a + // verdict written to the wrong lane cannot pass by symmetry. + for badIndex := 0; badIndex < width; badIndex++ { + items := make([]signedMessage, width) + for i := range items { + items[i] = makeSigned(t, i, i != badIndex) + } + + var batch Batch + for i := range items { + batch.Add(&items[i].pub, items[i].msg, items[i].sig) + } + all := batch.Verify() + + assert.False(t, all, "batch containing an invalid signature must not report all-valid") + for i, item := range items { + assert.Equal(t, item.valid, batch.OK(i), + "width=%d badIndex=%d: verdict for item %d", width, badIndex, i) + assert.Equal(t, VerifyOne(&item.pub, item.msg, item.sig), batch.OK(i), + "width=%d badIndex=%d: batch and single disagree at %d", width, badIndex, i) + } + } + }) + } +} + +func TestAllValidBatchReportsAllValid(t *testing.T) { + for _, width := range []int{1, 4, 8, 9, 64} { + var batch Batch + items := make([]signedMessage, width) + for i := range items { + items[i] = makeSigned(t, i, true) + batch.Add(&items[i].pub, items[i].msg, items[i].sig) + } + assert.True(t, batch.Verify(), "width=%d: every signature is honest", width) + } +} + +func TestBatchResetRetainsCapacityAndClearsVerdicts(t *testing.T) { + var batch Batch + item := makeSigned(t, 0, true) + for i := 0; i < 8; i++ { + batch.Add(&item.pub, item.msg, item.sig) + } + require.True(t, batch.Verify()) + capacityBefore := cap(batch.pubs) + + batch.Reset() + assert.Zero(t, batch.Len()) + assert.Equal(t, capacityBefore, cap(batch.pubs), "Reset must retain capacity") + assert.Nil(t, batch.pubs[:1][0], "Reset must not pin the previous batch's public keys") + + // A reused batch must not inherit stale verdicts. + bad := makeSigned(t, 1, false) + batch.Add(&bad.pub, bad.msg, bad.sig) + assert.False(t, batch.Verify()) + assert.False(t, batch.OK(0)) +} + +// Batch is worker-local scratch, so filling it must not allocate once its +// backing arrays have grown. This measures only the accumulation this package +// owns; what a backend allocates internally is that backend's contract and is +// covered by its own suite. +func TestBatchAccumulationAllocatesNothingAfterWarmup(t *testing.T) { + var batch Batch + items := make([]signedMessage, MaxDrain) + for i := range items { + items[i] = makeSigned(t, i, true) + } + fill := func() { + batch.Reset() + for i := range items { + batch.Add(&items[i].pub, items[i].msg, items[i].sig) + } + } + fill() // grow the backing arrays + + allocs := testing.AllocsPerRun(5, fill) + assert.Zero(t, allocs, "a warmed-up worker batch must not allocate while filling") +} + +func TestEmptyBatchIsVacuouslyValid(t *testing.T) { + var batch Batch + assert.True(t, batch.Verify()) + assert.Zero(t, batch.Len()) +} + +func TestVerifyOneRejectsNilKey(t *testing.T) { + item := makeSigned(t, 0, true) + assert.False(t, VerifyOne(nil, item.msg, item.sig)) +} + +func TestDrainTakesWhatIsReadyAndNeverBlocks(t *testing.T) { + ch := make(chan int, 16) + for i := 2; i <= 5; i++ { + ch <- i + } + + got := Drain(nil, 1, ch, MaxDrain) + assert.Equal(t, []int{1, 2, 3, 4, 5}, got, + "Drain must take the queued items and return rather than wait for more") +} + +func TestDrainStopsAtMax(t *testing.T) { + ch := make(chan int, 32) + for i := 0; i < 32; i++ { + ch <- i + } + got := Drain(nil, -1, ch, 8) + assert.Len(t, got, 8) + assert.Equal(t, 25, len(ch), "the untaken items must stay queued for other workers") +} + +func TestDrainOnEmptyChannelReturnsJustTheFirstItem(t *testing.T) { + ch := make(chan int) + got := Drain(nil, 42, ch, MaxDrain) + assert.Equal(t, []int{42}, got) +} + +func TestDrainHandlesClosedChannel(t *testing.T) { + ch := make(chan int, 2) + ch <- 2 + close(ch) + got := Drain(nil, 1, ch, MaxDrain) + assert.Equal(t, []int{1, 2}, got, "a closed channel must terminate the drain, not spin") +} + +func TestDrainReusesTheDestinationSlice(t *testing.T) { + ch := make(chan int, 8) + dst := make([]int, 0, MaxDrain) + for round := 0; round < 3; round++ { + ch <- round*10 + 1 + dst = Drain(dst, round*10, ch, MaxDrain) + require.Len(t, dst, 2) + assert.Equal(t, round*10, dst[0]) + assert.Equal(t, MaxDrain, cap(dst), "Drain must not reallocate a sufficient buffer") + } +} + +// FairShare must never return less than one: the item already in the worker's +// hand is never given back, which is what makes it impossible for this package +// to strand work. +func TestFairShareNeverStrandsTheItemInHand(t *testing.T) { + for _, queued := range []int{0, 1, 7, 100, 100000} { + for _, workers := range []int{-1, 0, 1, 8, 1000} { + for _, max := range []int{1, BatchTarget, MaxDrain} { + got := FairShare(queued, workers, max) + assert.GreaterOrEqual(t, got, 1, + "queued=%d workers=%d max=%d", queued, workers, max) + assert.LessOrEqual(t, got, max, + "queued=%d workers=%d max=%d", queued, workers, max) + } + } + } +} + +// A shallow queue must spread across workers rather than collapsing onto one. +// Eight workers facing eight items should take one each: one group of eight on +// a single core is slower in wall-clock than eight singles on eight cores. +func TestFairShareSpreadsShallowQueues(t *testing.T) { + assert.Equal(t, 1, FairShare(7, 8, MaxDrain), "8 workers, 8 items total -> one each") + assert.Equal(t, 1, FairShare(0, 8, MaxDrain), "nothing behind us -> just the item in hand") + assert.Equal(t, 2, FairShare(8, 8, MaxDrain), "one spare each") +} + +// A deep queue means every worker is saturated regardless, so each may take a +// full group. +func TestFairShareGivesFullGroupsOnDeepQueues(t *testing.T) { + assert.Equal(t, BatchTarget, FairShare(8192, 8, BatchTarget)) + assert.Equal(t, MaxDrain, FairShare(8192, 8, MaxDrain)) +} diff --git a/pkg/sigverify/stats.go b/pkg/sigverify/stats.go new file mode 100644 index 00000000..97c24677 --- /dev/null +++ b/pkg/sigverify/stats.go @@ -0,0 +1,159 @@ +package sigverify + +import ( + "fmt" + "strings" + "sync/atomic" +) + +// Batch width is the single number that decides what verification costs. The +// library verifies eight signatures per AVX-512 group, so a stream of width-1 +// batches pays roughly 3.7x per signature what the same work costs at width 8. +// A total signature count cannot show that: a million signatures arriving one +// at a time and the same million arriving in groups of eight produce identical +// totals and wildly different cost. Only the distribution distinguishes them, +// which is why this is a histogram rather than a counter. +// +// Buckets are per-width up to 8 because that is where the group boundary sits +// and where the interesting behaviour is, then coarse above it: past 8 the +// question is only "are we filling groups", and the answer is yes. +var batchWidthBuckets = [...]int{1, 2, 3, 4, 5, 6, 7, 8, 16, 32, 64, 128, 256, 512, 1024} + +// One extra slot for everything above the last bucket. +var batchWidthCounts [len(batchWidthBuckets) + 1]atomic.Uint64 + +var ( + batchesObserved atomic.Uint64 + signaturesTotal atomic.Uint64 + emptyBatchesTotal atomic.Uint64 +) + +// observeBatchWidth records one verified batch. It is called from Verify rather +// than from each drain site so that a new caller cannot forget to instrument +// itself, and it is a plain atomic add into a fixed array: no allocation, no +// lock, and nothing that can fail on the verification path. +func observeBatchWidth(width int) { + batchesObserved.Add(1) + if width <= 0 { + emptyBatchesTotal.Add(1) + return + } + signaturesTotal.Add(uint64(width)) + + index := len(batchWidthBuckets) + for i, upper := range batchWidthBuckets { + if width <= upper { + index = i + break + } + } + batchWidthCounts[index].Add(1) +} + +// WidthBucket is one row of the batch-width distribution. Upper is inclusive; +// the final row reports Upper of 0 to mean "wider than every named bucket". +type WidthBucket struct { + Upper int + Batches uint64 +} + +// Snapshot is a point-in-time view of verification behaviour. It is a value, so +// a reporter can take one and format it without holding anything. +type Snapshot struct { + Backend string + InternalFaultFallbacks uint64 + Batches uint64 + Signatures uint64 + EmptyBatches uint64 + Widths []WidthBucket +} + +// MeanWidth is the average number of signatures per non-empty batch. It is the +// one-number summary; the distribution is what actually matters, because a mean +// of 4 is produced both by every batch being width 4 and by half being width 1 +// and half width 7, and those cost very different amounts. +func (s Snapshot) MeanWidth() float64 { + nonEmpty := s.Batches - s.EmptyBatches + if nonEmpty == 0 { + return 0 + } + return float64(s.Signatures) / float64(nonEmpty) +} + +// FullGroupShare is the fraction of signatures that arrived in a batch of at +// least eight, which is the fraction getting the accelerated path's full +// benefit. This is the number to watch when deciding whether a drain policy is +// working. +func (s Snapshot) FullGroupShare() float64 { + if s.Signatures == 0 { + return 0 + } + var wide uint64 + for _, b := range s.Widths { + if b.Upper == 0 || b.Upper >= 8 { + // Approximate: a bucket's signatures are not recorded per bucket, + // only its batch count, so weight by the bucket's upper bound. For + // the per-width buckets at and below 8 this is exact. + wide += b.Batches * uint64(max(b.Upper, 8)) + } + } + if wide > s.Signatures { + return 1 + } + return float64(wide) / float64(s.Signatures) +} + +// Stats returns the current snapshot. Counters are monotonic for the life of +// the process; a reporter that wants deltas should difference two snapshots. +func Stats() Snapshot { + widths := make([]WidthBucket, 0, len(batchWidthCounts)) + for i := range batchWidthCounts { + count := batchWidthCounts[i].Load() + if count == 0 { + continue + } + upper := 0 + if i < len(batchWidthBuckets) { + upper = batchWidthBuckets[i] + } + widths = append(widths, WidthBucket{Upper: upper, Batches: count}) + } + + return Snapshot{ + Backend: Backend(), + InternalFaultFallbacks: InternalFaultFallbacks(), + Batches: batchesObserved.Load(), + Signatures: signaturesTotal.Load(), + EmptyBatches: emptyBatchesTotal.Load(), + Widths: widths, + } +} + +// String renders the snapshot as one line per report. Callers write this to the +// per-run log directory; it is deliberately not printed to the terminal, where +// it would compete with replay progress for an operator's attention. +func (s Snapshot) String() string { + var b strings.Builder + fmt.Fprintf(&b, "backend=%s fallbacks=%d batches=%d signatures=%d mean_width=%.2f full_group_share=%.1f%%", + s.Backend, s.InternalFaultFallbacks, s.Batches, s.Signatures, + s.MeanWidth(), s.FullGroupShare()*100) + if s.EmptyBatches > 0 { + fmt.Fprintf(&b, " empty=%d", s.EmptyBatches) + } + if len(s.Widths) == 0 { + return b.String() + } + b.WriteString(" widths=[") + for i, w := range s.Widths { + if i > 0 { + b.WriteString(" ") + } + if w.Upper == 0 { + fmt.Fprintf(&b, ">%d:%d", batchWidthBuckets[len(batchWidthBuckets)-1], w.Batches) + continue + } + fmt.Fprintf(&b, "%d:%d", w.Upper, w.Batches) + } + b.WriteString("]") + return b.String() +} diff --git a/pkg/sigverify/stats_test.go b/pkg/sigverify/stats_test.go new file mode 100644 index 00000000..cf9082b4 --- /dev/null +++ b/pkg/sigverify/stats_test.go @@ -0,0 +1,108 @@ +package sigverify + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// The counters are process-global and monotonic, so assertions are written as +// deltas around the work rather than against absolute values. Another test in +// this package verifying signatures concurrently must not be able to break +// these. +func TestBatchWidthIsRecordedByVerify(t *testing.T) { + before := Stats() + + widths := []int{1, 1, 3, 8, 8, 8, 40} + for _, width := range widths { + var batch Batch + for i := 0; i < width; i++ { + signed := makeSigned(t, i, true) + batch.Add(&signed.pub, signed.msg, signed.sig) + } + require.True(t, batch.Verify()) + } + + after := Stats() + + var expectedSignatures int + for _, w := range widths { + expectedSignatures += w + } + + assert.Equal(t, uint64(len(widths)), after.Batches-before.Batches, + "every Verify must be counted, whatever its width") + assert.Equal(t, uint64(expectedSignatures), after.Signatures-before.Signatures) +} + +// An empty batch is a real event -- a drain that found nothing -- and must be +// counted without being charged signatures, or the mean width is wrong. +func TestEmptyBatchIsCountedSeparately(t *testing.T) { + before := Stats() + + var batch Batch + require.True(t, batch.Verify(), "an empty batch trivially passes") + + after := Stats() + assert.Equal(t, uint64(1), after.Batches-before.Batches) + assert.Equal(t, uint64(1), after.EmptyBatches-before.EmptyBatches) + assert.Equal(t, uint64(0), after.Signatures-before.Signatures, + "an empty batch must not contribute signatures") +} + +// MeanWidth exists to be read at a glance, so its denominator has to exclude +// empty batches. Counting them would drag the mean toward zero and make a +// healthy drain look starved. +func TestMeanWidthExcludesEmptyBatches(t *testing.T) { + snapshot := Snapshot{Batches: 10, EmptyBatches: 6, Signatures: 32} + assert.InDelta(t, 8.0, snapshot.MeanWidth(), 0.001, + "four non-empty batches carrying 32 signatures is a mean of 8") + + empty := Snapshot{Batches: 3, EmptyBatches: 3} + assert.Zero(t, empty.MeanWidth(), "no non-empty batches must not divide by zero") +} + +// The rendered line is what lands in the per-run log, so it is worth pinning +// that the fields an operator would grep for are actually present. +func TestSnapshotRendersTheFieldsWorthGrepping(t *testing.T) { + line := Snapshot{ + Backend: "r51", + Batches: 9, + Signatures: 40, + Widths: []WidthBucket{{Upper: 8, Batches: 5}, {Upper: 0, Batches: 1}}, + }.String() + + for _, want := range []string{"backend=r51", "batches=9", "signatures=40", "mean_width=", "full_group_share=", "widths=["} { + assert.Contains(t, line, want) + } + assert.Contains(t, line, ">1024:1", "the overflow bucket must be legible") + assert.NotContains(t, line, "empty=", "a zero empty count should not add noise") +} + +func TestSnapshotReportsEmptyBatchesWhenPresent(t *testing.T) { + line := Snapshot{Backend: "generic", Batches: 4, EmptyBatches: 2, Signatures: 8}.String() + assert.Contains(t, line, "empty=2") +} + +// Stats must be safe to call from a reporter goroutine while verification runs. +func TestStatsIsSafeUnderConcurrentVerification(t *testing.T) { + done := make(chan struct{}) + go func() { + defer close(done) + for i := 0; i < 200; i++ { + var batch Batch + signed := makeSigned(t, i, true) + batch.Add(&signed.pub, signed.msg, signed.sig) + batch.Verify() + } + }() + + for i := 0; i < 200; i++ { + if s := Stats(); strings.TrimSpace(s.String()) == "" { + t.Fatal("snapshot rendered empty") + } + } + <-done +} diff --git a/pkg/statsd/statsd.go b/pkg/statsd/statsd.go index ca2a26a3..bbf3eea9 100644 --- a/pkg/statsd/statsd.go +++ b/pkg/statsd/statsd.go @@ -124,7 +124,14 @@ var ( TurbineBlockDecode = Metric{"turbine_block_decode_duration_seconds"} TurbineTransactionParse = Metric{"turbine_transaction_parse_duration_seconds"} TurbineTransactionSigverify = Metric{"turbine_transaction_sigverify_duration_seconds"} - TurbineReplayAdmission = Metric{"turbine_replay_admission_duration_seconds"} + // ReplaySigverifyGroup times one drained group of transaction signatures + // and ReplaySigverifyGroupSignatures counts how many signatures were in it. + // The pair is what tells an operator whether batching is actually happening: + // a group width stuck near one means work is arriving too thinly to fill a + // vector group, which is a throughput ceiling no backend choice can lift. + ReplaySigverifyGroup = Metric{"replay_sigverify_group_duration_seconds"} + ReplaySigverifyGroupSignatures = Metric{"replay_sigverify_group_signatures_total"} + TurbineReplayAdmission = Metric{"turbine_replay_admission_duration_seconds"} SnapshotWorkerPoolUtilization = Metric{"snapshot_worker_pool_utilization"} TasksSetIfSlotHigherQueueSize = Metric{"tasks_set_if_slot_higher_queue_size"} @@ -238,6 +245,8 @@ var MetricToType = map[Metric]metricType{ TurbineBlockDecode: TimingT, TurbineTransactionParse: TimingT, TurbineTransactionSigverify: TimingT, + ReplaySigverifyGroup: TimingT, + ReplaySigverifyGroupSignatures: CountT, TurbineReplayAdmission: TimingT, TestCount: CountT, @@ -344,6 +353,8 @@ var MetricToLabels = map[Metric][]string{ TurbineBlockDecode: {}, TurbineTransactionParse: {}, TurbineTransactionSigverify: {}, + ReplaySigverifyGroup: {}, + ReplaySigverifyGroupSignatures: {}, TurbineReplayAdmission: {}, SnapshotWorkerPoolUtilization: {"task"}, @@ -379,6 +390,7 @@ var MetricToBuckets = map[Metric][]float64{ TurbineBlockDecode: turbinePipelineDurationBuckets, TurbineTransactionParse: turbinePipelineDurationBuckets, TurbineTransactionSigverify: turbinePipelineDurationBuckets, + ReplaySigverifyGroup: turbinePipelineDurationBuckets, TurbineReplayAdmission: turbinePipelineDurationBuckets, AlpenglowVoteRewards: turbinePipelineDurationBuckets, VoteRewardValidatorPreparation: turbinePipelineDurationBuckets, diff --git a/pkg/tpu/pipeline/pipeline.go b/pkg/tpu/pipeline/pipeline.go index 0b142dcf..2038c2d7 100644 --- a/pkg/tpu/pipeline/pipeline.go +++ b/pkg/tpu/pipeline/pipeline.go @@ -6,6 +6,7 @@ import ( "sync" "sync/atomic" + "github.com/Overclock-Validator/mithril/pkg/sigverify" "github.com/Overclock-Validator/mithril/pkg/tpu/dedup" "github.com/Overclock-Validator/mithril/pkg/tpu/packet" "github.com/Overclock-Validator/mithril/pkg/tpu/sink" @@ -172,7 +173,7 @@ func startSigverifyPool( go func() { defer verifyWG.Done() defer wg.Done() - runSigverifyWorker(in, out, stats) + runSigverifyWorker(in, out, workers, stats) }() } wg.Add(1) @@ -183,24 +184,59 @@ func startSigverifyPool( }() } +// runSigverifyWorker verifies a DRAINED GROUP of packets per pass rather than +// one packet at a time. +// +// A transaction carries one or two signatures, and the vectorized backend +// verifies eight per group, so packet-at-a-time verification pays for a whole +// group and uses one lane of it. Draining costs nothing when ingress is quiet — +// the worker still takes exactly the packet it blocked for, and forwards it +// with unchanged latency — and turns a burst into free width. func runSigverifyWorker( in <-chan packet.Packet, out chan<- packet.Packet, + workers int, stats *SigverifyStats, ) { + // Worker-local scratch, reused across groups. + var ( + group []packet.Packet + payloads [][]byte + verdicts []bool + verifier batchVerifier + ) for pkt := range in { - data := pkt.Data() - atomic.AddUint64(&stats.InPackets, 1) - atomic.AddUint64(&stats.InBytes, uint64(len(data))) - - if !verifyPacket(data) { - atomic.AddUint64(&stats.DroppedSigverify, 1) - pkt.Release() - continue + group = sigverify.Drain(group, pkt, in, + sigverify.FairShare(len(in), workers, sigverify.MaxDrain)) + + payloads = payloads[:0] + for _, p := range group { + data := p.Data() + atomic.AddUint64(&stats.InPackets, 1) + atomic.AddUint64(&stats.InBytes, uint64(len(data))) + payloads = append(payloads, data) + } + + if cap(verdicts) < len(payloads) { + verdicts = make([]bool, len(payloads)) + } + verdicts = verdicts[:len(payloads)] + verifier.Verify(payloads, verdicts) + + for i, p := range group { + if !verdicts[i] { + atomic.AddUint64(&stats.DroppedSigverify, 1) + p.Release() + continue + } + atomic.AddUint64(&stats.VerifiedPackets, 1) + atomic.AddUint64(&stats.VerifiedBytes, uint64(len(payloads[i]))) + out <- p } - atomic.AddUint64(&stats.VerifiedPackets, 1) - atomic.AddUint64(&stats.VerifiedBytes, uint64(len(data))) - out <- pkt + // Released packets must not stay reachable through the scratch slices + // until the next group happens to overwrite that index. + clear(group) + clear(payloads) } } diff --git a/pkg/tpu/pipeline/pipeline_test.go b/pkg/tpu/pipeline/pipeline_test.go index 23c35d8d..eb694a7a 100644 --- a/pkg/tpu/pipeline/pipeline_test.go +++ b/pkg/tpu/pipeline/pipeline_test.go @@ -2,12 +2,14 @@ package pipeline import ( "context" + "fmt" "runtime" "testing" "time" "github.com/Overclock-Validator/mithril/pkg/tpu/packet" "github.com/Overclock-Validator/mithril/pkg/tpu/sink" + "github.com/Overclock-Validator/mithril/pkg/tpu/txfixture" "github.com/Overclock-Validator/mithril/pkg/tpu/wire" ) @@ -47,3 +49,32 @@ func requireEnqueue(t *testing.T, ingress chan<- packet.Packet, pkt packet.Packe runtime.Gosched() } } + +// Every admitted packet must reach the sink, whatever the count. Sigverify +// workers group packets, and a count that divides badly into groups must not +// leave a remainder sitting in a worker's scratch waiting for company that +// never arrives. +func TestPipelineDeliversAwkwardPacketCounts(t *testing.T) { + for _, count := range []int{1, 3, 7, 8, 9, 65} { + t.Run(fmt.Sprintf("count=%d", count), func(t *testing.T) { + noop := &sink.Noop{} + p, ingress := Start(context.Background(), Config{SigverifyWorkers: 4, Sink: noop}) + defer p.Stop() + + for i := 0; i < count; i++ { + // Distinct payloads: identical ones are dropped by the dedup + // stage before sigverify ever sees them. + requireEnqueue(t, ingress, packet.Owned(txfixture.MustSignedTransferWire(uint64(i)))) + } + + deadline := time.Now().Add(10 * time.Second) + for noop.Snapshot().InPackets < uint64(count) { + if time.Now().After(deadline) { + t.Fatalf("count=%d: only %d packets reached the sink, stats=%+v", + count, noop.Snapshot().InPackets, p.Stats()) + } + time.Sleep(5 * time.Millisecond) + } + }) + } +} diff --git a/pkg/tpu/pipeline/verify.go b/pkg/tpu/pipeline/verify.go index 7f1abdb7..2072c672 100644 --- a/pkg/tpu/pipeline/verify.go +++ b/pkg/tpu/pipeline/verify.go @@ -1,7 +1,12 @@ package pipeline -import "github.com/Overclock-Validator/mithril/pkg/tpu/sigverify" +import tpusigverify "github.com/Overclock-Validator/mithril/pkg/tpu/sigverify" + +// batchVerifier keeps the pipeline's dependency on the TPU sigverify package +// confined to this file, so pipeline.go can use the unqualified name for +// Mithril's shared verification package. +type batchVerifier = tpusigverify.BatchVerifier func verifyPacket(data []byte) bool { - return sigverify.VerifyPacket(data) + return tpusigverify.VerifyPacket(data) } diff --git a/pkg/tpu/sigverify/sigverify.go b/pkg/tpu/sigverify/sigverify.go index aa7c9a1f..91c335e1 100644 --- a/pkg/tpu/sigverify/sigverify.go +++ b/pkg/tpu/sigverify/sigverify.go @@ -1,9 +1,9 @@ package sigverify import ( - "crypto/ed25519" "errors" + "github.com/Overclock-Validator/mithril/pkg/txverify" "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" ) @@ -25,6 +25,9 @@ func ParseTx(p []byte) (tx *solana.Transaction, err error) { // VerifyPacket parses a wire transaction and verifies its signatures against // static account keys. Unparseable packets and invalid signatures are discarded. // Address lookup tables are not resolved. +// +// Prefer BatchVerifier: a transaction carries one or two signatures, so +// verifying packets one at a time leaves most of a vector group idle. func VerifyPacket(data []byte) bool { tx, err := ParseTx(data) if err != nil { @@ -34,21 +37,63 @@ func VerifyPacket(data []byte) bool { } func VerifyTransaction(tx *solana.Transaction) bool { - required := int(tx.Message.Header.NumRequiredSignatures) - if required == 0 || len(tx.Signatures) != required || required > len(tx.Message.AccountKeys) { + if !admissible(tx) { return false } + // Signature checking goes through txverify rather than being reimplemented + // here. This path used to marshal the message itself and so omitted the + // version-byte fixup, which meant a correctly signed versioned transaction + // was dropped at ingest. + return txverify.VerifyTransaction(tx) == nil +} - msg, err := tx.Message.MarshalBinary() - if err != nil { +// admissible rejects shapes TPU must not admit regardless of cryptography: a +// transaction that requires no signatures at all, a signature list that +// disagrees with the header, or a header claiming more signers than the account +// table can supply. +func admissible(tx *solana.Transaction) bool { + if tx == nil { return false } + required := int(tx.Message.Header.NumRequiredSignatures) + return required > 0 && + len(tx.Signatures) == required && + required <= len(tx.Message.AccountKeys) +} - keys := tx.Message.AccountKeys - for i := 0; i < required; i++ { - if !ed25519.Verify(keys[i][:], msg, tx.Signatures[i][:]) { - return false +// BatchVerifier parses and verifies many packets per call. It is reusable +// caller-owned scratch and is not safe for concurrent use; give each worker +// its own. +type BatchVerifier struct { + inner txverify.BatchVerifier + txs []*solana.Transaction + errs []error +} + +// Verify writes a verdict for each packet into ok, which must be at least as +// long as packets. A packet that fails to parse, or whose shape is +// inadmissible, is reported false without consuming a signature lane. +func (v *BatchVerifier) Verify(packets [][]byte, ok []bool) { + clear(v.txs) + v.txs = v.txs[:0] + for _, data := range packets { + tx, err := ParseTx(data) + if err != nil || !admissible(tx) { + // A nil entry keeps the packet's position so verdicts line up, and + // the batch verifier reports it failed without adding lanes. + v.txs = append(v.txs, nil) + continue } + v.txs = append(v.txs, tx) + } + + if cap(v.errs) < len(v.txs) { + v.errs = make([]error, len(v.txs)) + } + v.errs = v.errs[:len(v.txs)] + v.inner.Verify(v.txs, v.errs) + + for i := range v.txs { + ok[i] = v.txs[i] != nil && v.errs[i] == nil } - return true } diff --git a/pkg/tpu/sigverify/sigverify_test.go b/pkg/tpu/sigverify/sigverify_test.go new file mode 100644 index 00000000..9c8d146d --- /dev/null +++ b/pkg/tpu/sigverify/sigverify_test.go @@ -0,0 +1,148 @@ +package sigverify + +import ( + stded25519 "crypto/ed25519" + "crypto/rand" + "testing" + + "github.com/Overclock-Validator/mithril/pkg/tpu/txfixture" + "github.com/Overclock-Validator/mithril/pkg/txverify" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/programs/system" + "github.com/stretchr/testify/require" +) + +// signedV0Transaction builds a versioned (v0) transaction and signs it over the +// bytes a real Solana client signs — that is, with the 0x80 version prefix that +// txverify.MessageBytes produces, not solana-go's MarshalBinary output. +// +// This distinction is the whole point of the test. solana-go's MarshalV0 emits +// versionNum+127 = 0x7f, which is not the wire encoding. A verifier that +// marshals for itself and skips the fixup checks the signature against bytes no +// honest client ever signed, so it rejects every valid versioned transaction. +func signedV0Transaction(t *testing.T) *solana.Transaction { + t.Helper() + + payer, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + + tx, err := solana.NewTransaction( + []solana.Instruction{ + system.NewTransferInstruction(1, payer.PublicKey(), payer.PublicKey()).Build(), + }, + solana.Hash{}, + solana.TransactionPayer(payer.PublicKey()), + ) + require.NoError(t, err) + tx.Message.SetVersion(solana.MessageVersionV0) + + msg, err := txverify.MessageBytes(tx) + require.NoError(t, err) + require.Equal(t, byte(0x80), msg[0], "a v0 message is signed over a 0x80 prefix") + + tx.Signatures = []solana.Signature{ + solana.SignatureFromBytes(stded25519.Sign(stded25519.PrivateKey(payer), msg)), + } + return tx +} + +// Regression: versioned transactions used to be dropped at TPU ingest no matter +// how well signed they were, because this package marshalled the message itself +// and never applied the version-byte fixup. +func TestVersionedTransactionIsAccepted(t *testing.T) { + tx := signedV0Transaction(t) + require.True(t, VerifyTransaction(tx), + "a correctly signed v0 transaction must be admitted") +} + +func TestLegacyTransactionIsAccepted(t *testing.T) { + require.True(t, VerifyPacket(txfixture.MustSignedTransferWire(1))) +} + +func TestGarbageAndCorruptionAreRejected(t *testing.T) { + require.False(t, VerifyPacket([]byte("not a transaction")), "unparseable packet") + require.False(t, VerifyPacket(nil), "empty packet") + + wire := txfixture.MustSignedTransferWire(2) + corrupt := make([]byte, len(wire)) + copy(corrupt, wire) + corrupt[3] ^= 0xFF // inside the first signature + require.False(t, VerifyPacket(corrupt), "corrupted signature") +} + +// Batching must not change any verdict, and an unparseable packet in the middle +// of a group must not shift the verdicts of its neighbours: it contributes no +// signature lanes, so a naive index mapping would slide every later result. +func TestBatchVerdictsMatchSingleVerdictsWithGaps(t *testing.T) { + const width = 24 + packets := make([][]byte, 0, width) + want := make([]bool, 0, width) + + for i := 0; i < width; i++ { + switch i % 4 { + case 0: // unparseable — contributes no lane + packets = append(packets, []byte{0x01, 0x02, 0x03}) + want = append(want, false) + case 1: // corrupted signature — contributes a lane that fails + wire := txfixture.MustSignedTransferWire(uint64(i)) + corrupt := make([]byte, len(wire)) + copy(corrupt, wire) + corrupt[5] ^= 0xFF + packets = append(packets, corrupt) + want = append(want, false) + default: // honest + packets = append(packets, txfixture.MustSignedTransferWire(uint64(i))) + want = append(want, true) + } + } + + var verifier BatchVerifier + got := make([]bool, width) + verifier.Verify(packets, got) + + for i := range packets { + require.Equal(t, want[i], got[i], "batch verdict for packet %d", i) + require.Equal(t, VerifyPacket(packets[i]), got[i], + "batch and single-packet verdicts disagree at %d", i) + } +} + +func TestBatchVerifierIsReusable(t *testing.T) { + var verifier BatchVerifier + for round := 0; round < 3; round++ { + packets := [][]byte{ + txfixture.MustSignedTransferWire(uint64(round*2 + 1)), + []byte("garbage"), + } + got := make([]bool, 2) + verifier.Verify(packets, got) + require.True(t, got[0], "round %d: honest packet", round) + require.False(t, got[1], "round %d: garbage packet", round) + } +} + +func TestInadmissibleShapesAreRejected(t *testing.T) { + tx := signedV0Transaction(t) + require.True(t, VerifyTransaction(tx)) + + noSignatures := *tx + noSignatures.Signatures = nil + require.False(t, VerifyTransaction(&noSignatures), + "a transaction with no signatures must not be admitted") + + extraSignature := *tx + extraSignature.Signatures = append(append([]solana.Signature{}, tx.Signatures...), solana.Signature{}) + require.False(t, VerifyTransaction(&extraSignature), + "signature count must match the header") + + require.False(t, VerifyTransaction(nil), "nil transaction") +} + +func TestRandomKeyDoesNotVerify(t *testing.T) { + tx := signedV0Transaction(t) + var scratch [64]byte + _, err := rand.Read(scratch[:]) + require.NoError(t, err) + tx.Signatures[0] = solana.SignatureFromBytes(scratch[:]) + require.False(t, VerifyTransaction(tx)) +} diff --git a/pkg/tpu/tpu.go b/pkg/tpu/tpu.go index 48484da4..2f71a9cb 100644 --- a/pkg/tpu/tpu.go +++ b/pkg/tpu/tpu.go @@ -1,9 +1,9 @@ package tpu import ( - "crypto/ed25519" "errors" + "github.com/Overclock-Validator/mithril/pkg/tpu/sigverify" "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" ) @@ -28,25 +28,14 @@ func VerifyPacket(data []byte) bool { return err == nil && VerifyTxSig(tx) } +// VerifyTxSig reports whether every required signature on tx is valid. +// +// It delegates rather than reimplementing. The hand-rolled version this +// replaces marshalled the message itself and so omitted the version-byte +// fixup that txverify.MessageBytes applies, which meant a correctly signed +// versioned transaction never verified here. func VerifyTxSig(tx *solana.Transaction) (ok bool) { - msg, err := tx.Message.MarshalBinary() - if err != nil { - return false - } - - signers := ExtractSigners(tx) - - if len(signers) != len(tx.Signatures) { - return false - } - - for i, sig := range tx.Signatures { - if !ed25519.Verify(signers[i][:], msg, sig[:]) { - return false - } - } - - return true + return sigverify.VerifyTransaction(tx) } func ExtractSigners(tx *solana.Transaction) []solana.PublicKey { diff --git a/pkg/turbine/shred.go b/pkg/turbine/shred.go index 4524214b..dae24ddd 100644 --- a/pkg/turbine/shred.go +++ b/pkg/turbine/shred.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" + narya "github.com/Overclock-Validator/narya-ed25519/ed25519" "github.com/gagliardetto/solana-go" ) @@ -395,7 +396,7 @@ func (s *Shred) VerifySignature(leader solana.PublicKey) error { if err != nil { return err } - if !leader.Verify(root[:], s.Signature) { + if !narya.VerifyStrict(leader[:], root[:], s.Signature[:]) { return fmt.Errorf("%w: slot %d shred %d", ErrInvalidSignature, s.Slot, s.Index) } return nil diff --git a/pkg/turbine/sigcache.go b/pkg/turbine/sigcache.go index c85a672c..7f3b1313 100644 --- a/pkg/turbine/sigcache.go +++ b/pkg/turbine/sigcache.go @@ -5,6 +5,7 @@ import ( "sync" "sync/atomic" + narya "github.com/Overclock-Validator/narya-ed25519/ed25519" "github.com/gagliardetto/solana-go" ) @@ -15,8 +16,8 @@ import ( // bytes to the root) resolves to an already-verified (leader, root, // signature) triple is authenticated by the chain alone. Signature verifies // drop from one per shred to one per FEC set while staying bit-for-bit -// equivalent to verifying each: ed25519.Verify is deterministic, so a hit -// reproduces exactly the result of re-running it on the same inputs. +// equivalent to verifying each: strict verification is deterministic, so a +// hit reproduces exactly the result of re-running it on the same inputs. // Tampered content can never hit — different bytes yield a different root, // hence a different key. Failures are never cached. type shredSigCache struct { @@ -64,7 +65,7 @@ func (c *shredSigCache) verifyShred(s *Shred, leader solana.PublicKey) error { c.mu.Unlock() c.verifies.Add(1) - if !leader.Verify(root[:], s.Signature) { + if !narya.VerifyStrict(leader[:], root[:], s.Signature[:]) { return fmt.Errorf("%w: slot %d shred %d", ErrInvalidSignature, s.Slot, s.Index) } c.mu.Lock() diff --git a/pkg/turbine/strict_verify_test.go b/pkg/turbine/strict_verify_test.go new file mode 100644 index 00000000..33df5742 --- /dev/null +++ b/pkg/turbine/strict_verify_test.go @@ -0,0 +1,65 @@ +package turbine + +import ( + "encoding/hex" + "testing" + + narya "github.com/Overclock-Validator/narya-ed25519/ed25519" + "github.com/gagliardetto/solana-go" +) + +// TestShredVerifyStrict confirms shred signature verification is routed +// through strict semantics: an honestly signed shred still verifies, +// and a small-order leader key is rejected (mainnet's verify_strict +// rejects small-order A). A crafted shred attributed to a small-order +// leader must not be admitted where mainnet would reject it. +func TestShredVerifyStrict(t *testing.T) { + shred, leader := buildSignedTestShred(t, 10, 0x42) + if err := shred.VerifySignature(leader); err != nil { + t.Fatalf("honest shred failed strict verification: %v", err) + } + + raw, err := hex.DecodeString("0100000000000000000000000000000000000000000000000000000000000000") + if err != nil { + t.Fatal(err) + } + var smallOrderLeader solana.PublicKey + copy(smallOrderLeader[:], raw) + if err := shred.VerifySignature(smallOrderLeader); err == nil { + t.Fatal("shred with small-order leader key was accepted") + } + + // The reference and cached paths must agree with narya.VerifyStrict. + root, err := shred.MerkleRoot() + if err != nil { + t.Fatal(err) + } + for _, leaderKey := range []solana.PublicKey{leader, smallOrderLeader} { + want := narya.VerifyStrict(leaderKey[:], root[:], shred.Signature[:]) + got := shred.VerifySignature(leaderKey) == nil + if got != want { + t.Fatalf("VerifySignature diverged from narya.VerifyStrict for leader %x", leaderKey) + } + cache := &shredSigCache{} + if (cache.verifyShred(shred, leaderKey) == nil) != want { + t.Fatalf("verifyShred diverged from narya.VerifyStrict for leader %x", leaderKey) + } + } +} + +// TestShredSigCacheDoesNotCacheStrictRejection ensures a strict +// rejection is never memoized as valid: re-verifying a small-order +// leader must keep failing. +func TestShredSigCacheDoesNotCacheStrictRejection(t *testing.T) { + shred, _ := buildSignedTestShred(t, 11, 0x7) + raw, _ := hex.DecodeString("0100000000000000000000000000000000000000000000000000000000000000") + var smallOrderLeader solana.PublicKey + copy(smallOrderLeader[:], raw) + + cache := &shredSigCache{} + for i := 0; i < 3; i++ { + if cache.verifyShred(shred, smallOrderLeader) == nil { + t.Fatalf("iteration %d: cache accepted a small-order leader", i) + } + } +} diff --git a/pkg/turbine/transaction_verifier.go b/pkg/turbine/transaction_verifier.go index 31684a84..e64e3669 100644 --- a/pkg/turbine/transaction_verifier.go +++ b/pkg/turbine/transaction_verifier.go @@ -7,6 +7,7 @@ import ( "sync" "github.com/Overclock-Validator/mithril/pkg/block" + "github.com/Overclock-Validator/mithril/pkg/sigverify" "github.com/Overclock-Validator/mithril/pkg/txverify" "github.com/gagliardetto/solana-go" ) @@ -23,40 +24,111 @@ type transactionVerifier struct { jobs chan transactionVerifyJob verify func(*solana.Transaction) error workers int - close sync.Once - worker sync.WaitGroup + // wave is how many transactions verifyBlockContext admits at once. It is + // workers * sigverify.BatchTarget so each worker can actually accumulate a + // full vector group rather than being handed one transaction at a time. + wave int + close sync.Once + + worker sync.WaitGroup } func newTransactionVerifier(workers, queueDepth int, verify func(*solana.Transaction) error) *transactionVerifier { if workers < 1 { workers = 1 } + wave := workers * sigverify.BatchTarget if queueDepth < 1 { queueDepth = 1 } - if verify == nil { - verify = txverify.VerifyTransaction - } v := &transactionVerifier{ jobs: make(chan transactionVerifyJob, queueDepth), verify: verify, workers: workers, + wave: wave, } v.worker.Add(workers) for i := 0; i < workers; i++ { go func() { defer v.worker.Done() + // Worker-local scratch reused across groups. + var ( + group []transactionVerifyJob + scr verifyScratch + ) for job := range v.jobs { - func() { - defer job.done.Done() - *job.err = verifyTransactionSafely(v.verify, job.tx) - }() + group = sigverify.Drain(group, job, v.jobs, + sigverify.FairShare(len(v.jobs), v.workers, sigverify.BatchTarget)) + v.verifyGroup(group, &scr) + // Do not keep finished jobs reachable through the scratch. + clear(group) } }() } return v } +// verifyScratch is one worker's reusable buffers. +type verifyScratch struct { + txs []*solana.Transaction + errs []error + batch txverify.BatchVerifier +} + +// verifyGroup verifies a drained group and releases every job in it. +// +// Releasing happens in a defer covering the whole group, so no caller can be +// left waiting on a job that was drained into a batch which then failed — +// a stranded job would hang verifyBlockContext's done.Wait() forever. +func (v *transactionVerifier) verifyGroup(group []transactionVerifyJob, scr *verifyScratch) { + defer func() { + for _, job := range group { + job.done.Done() + } + }() + + // An injected verifier is a per-transaction function and stays that way; + // only the default path can batch. This seam is used by tests. + if v.verify != nil { + for _, job := range group { + *job.err = verifyTransactionSafely(v.verify, job.tx) + } + return + } + + scr.txs = scr.txs[:0] + for _, job := range group { + scr.txs = append(scr.txs, job.tx) + } + if cap(scr.errs) < len(scr.txs) { + scr.errs = make([]error, len(scr.txs)) + } + scr.errs = scr.errs[:len(scr.txs)] + + verifyBatchSafely(&scr.batch, scr.txs, scr.errs) + + for i, job := range group { + *job.err = scr.errs[i] + } + clear(scr.txs) +} + +// verifyBatchSafely mirrors verifyTransactionSafely: a panic in the verifier +// becomes an error for every transaction in the group rather than taking down +// the process. Attributing it to all of them is deliberate — a panic gives no +// evidence about which transaction caused it, and silently passing the others +// would admit unverified transactions. +func verifyBatchSafely(batch *txverify.BatchVerifier, txs []*solana.Transaction, errs []error) { + defer func() { + if recovered := recover(); recovered != nil { + for i := range errs { + errs[i] = fmt.Errorf("signature verifier panic: %v", recovered) + } + } + }() + batch.Verify(txs, errs) +} + func (v *transactionVerifier) closeAndWait() { if v == nil { return @@ -93,11 +165,11 @@ func (v *transactionVerifier) verifyBlockContext(ctx context.Context, blk *block // Admit one worker-wave at a time. A monster block still occupies every // verifier lane, but cannot park tens of thousands of jobs ahead of a newly // completed small block in the shared bounded queue. - for chunkStart := 0; chunkStart < len(blk.Transactions); chunkStart += v.workers { + for chunkStart := 0; chunkStart < len(blk.Transactions); chunkStart += v.wave { if err := ctx.Err(); err != nil { return err } - chunkEnd := min(chunkStart+v.workers, len(blk.Transactions)) + chunkEnd := min(chunkStart+v.wave, len(blk.Transactions)) errs := make([]error, chunkEnd-chunkStart) var done sync.WaitGroup for txIdx := chunkStart; txIdx < chunkEnd; txIdx++ { @@ -157,7 +229,7 @@ var ( func validateBlockTransactionsContext(ctx context.Context, blk *block.Block) error { defaultTransactionVerifierOnce.Do(func() { workers := max(1, (runtime.GOMAXPROCS(0)+1)/2) - defaultTransactionVerifier = newTransactionVerifier(workers, 2*workers, nil) + defaultTransactionVerifier = newTransactionVerifier(workers, 2*workers*sigverify.BatchTarget, nil) }) return defaultTransactionVerifier.verifyBlockContext(ctx, blk) } diff --git a/pkg/turbine/transaction_verifier_test.go b/pkg/turbine/transaction_verifier_test.go index 4a8ea9f1..8eb19c63 100644 --- a/pkg/turbine/transaction_verifier_test.go +++ b/pkg/turbine/transaction_verifier_test.go @@ -10,6 +10,7 @@ import ( "github.com/Overclock-Validator/mithril/pkg/block" "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" ) func verifierTestBlock(count int) *block.Block { @@ -128,3 +129,35 @@ func TestTransactionVerifierRejectsNilAtDeterministicIndex(t *testing.T) { t.Fatalf("nil transaction error = %q, want %q", got, want) } } + +// Every transaction in a block must be verified and joined, whatever the count. +// Workers group transactions, so a count that divides badly into groups must +// not leave a remainder waiting for company: verifyBlockContext joins each +// wave with done.Wait(), and a stranded job would hang it forever. +// +// A counting verifier is injected so the assertion is on what was actually +// verified, not merely on returning without error. +func TestTransactionVerifierVerifiesEveryTransactionForAwkwardCounts(t *testing.T) { + for _, count := range []int{1, 3, 7, 8, 9, 33, 65} { + t.Run(fmt.Sprintf("count=%d", count), func(t *testing.T) { + var seen atomic.Int32 + verifier := newTransactionVerifier(4, 8, func(*solana.Transaction) error { + seen.Add(1) + return nil + }) + defer verifier.closeAndWait() + + done := make(chan error, 1) + go func() { done <- verifier.verifyBlock(verifierTestBlock(count)) }() + + select { + case err := <-done: + require.NoError(t, err) + case <-time.After(30 * time.Second): + t.Fatalf("count=%d: transactions stranded in an unfinished group", count) + } + require.Equal(t, int32(count), seen.Load(), + "count=%d: every transaction must be verified exactly once", count) + }) + } +} diff --git a/pkg/txverify/txverify.go b/pkg/txverify/txverify.go index 789cc7c1..2eef851d 100644 --- a/pkg/txverify/txverify.go +++ b/pkg/txverify/txverify.go @@ -3,9 +3,19 @@ package txverify import ( "fmt" + "github.com/Overclock-Validator/mithril/pkg/sigverify" "github.com/gagliardetto/solana-go" ) +// MessageBytes returns the exact bytes a transaction's signatures are computed +// over. +// +// The version-byte fixup is load-bearing and easy to omit. solana-go's +// MarshalV0 writes versionNum+127, i.e. 0x7f for a v0 message, which is not the +// Solana wire encoding; the signed prefix is 0x80. Marshalling without this +// correction produces bytes that no honest signature will ever verify against, +// so every verifier must come through here rather than calling MarshalBinary +// directly. func MessageBytes(tx *solana.Transaction) ([]byte, error) { if tx == nil { return nil, fmt.Errorf("nil transaction") @@ -28,6 +38,11 @@ func MessageBytes(tx *solana.Transaction) ([]byte, error) { return msg, nil } +// VerifyTransaction verifies one transaction's signatures. +// +// Prefer BatchVerifier where more than a few transactions are available: a +// transaction carries one or two signatures, and verifying one or two at a time +// leaves most of a vector group idle. func VerifyTransaction(tx *solana.Transaction) error { msg, err := MessageBytes(tx) if err != nil { @@ -39,10 +54,84 @@ func VerifyTransaction(tx *solana.Transaction) error { return fmt.Errorf("got %d signers, but %d signatures", len(signers), len(tx.Signatures)) } - for i, sig := range tx.Signatures { - if !sig.Verify(signers[i], msg) { + for i := range tx.Signatures { + if !sigverify.VerifyOne((*[32]byte)(&signers[i]), msg, tx.Signatures[i][:]) { return fmt.Errorf("invalid signature by %s", signers[i]) } } return nil } + +// BatchVerifier verifies many transactions per call. It is reusable +// caller-owned scratch and is not safe for concurrent use; give each worker +// its own. +type BatchVerifier struct { + batch sigverify.Batch + // counts[i] is how many signature lanes transaction i contributed, which + // is zero when it failed a precheck. It is what maps a lane verdict back + // to the transaction that produced it. + counts []int + // signers[i] is retained so a failure can name the signer without + // recomputing Signers(), which allocates. + signers [][]solana.PublicKey +} + +// Verify checks every transaction in txs and writes a per-transaction result +// into errs, which must be the same length as txs. A nil entry means that +// transaction verified. +// +// Every transaction gets an independent verdict: one bad transaction does not +// mask the others, so a caller can report precisely which one failed. +func (v *BatchVerifier) Verify(txs []*solana.Transaction, errs []error) { + if len(errs) != len(txs) { + panic("txverify: errs and txs length mismatch") + } + v.batch.Reset() + v.counts = v.counts[:0] + clear(v.signers) + v.signers = v.signers[:0] + + for i, tx := range txs { + errs[i] = nil + signers, msg, err := prepare(tx) + if err != nil { + errs[i] = err + v.counts = append(v.counts, 0) + v.signers = append(v.signers, nil) + continue + } + for j := range tx.Signatures { + v.batch.Add((*[32]byte)(&signers[j]), msg, tx.Signatures[j][:]) + } + v.counts = append(v.counts, len(tx.Signatures)) + v.signers = append(v.signers, signers) + } + + if v.batch.Verify() { + return + } + + lane := 0 + for i, count := range v.counts { + for j := 0; j < count; j++ { + if !v.batch.OK(lane+j) && errs[i] == nil { + errs[i] = fmt.Errorf("invalid signature by %s", v.signers[i][j]) + } + } + lane += count + } +} + +// prepare runs the checks that must precede verification and that determine a +// transaction's result on their own. +func prepare(tx *solana.Transaction) ([]solana.PublicKey, []byte, error) { + msg, err := MessageBytes(tx) + if err != nil { + return nil, nil, err + } + signers := tx.Message.Signers() + if len(signers) != len(tx.Signatures) { + return nil, nil, fmt.Errorf("got %d signers, but %d signatures", len(signers), len(tx.Signatures)) + } + return signers, msg, nil +}