From 7eca239bc691bf9a4719034bf98f1f4f8ad83fac Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:42:08 -0500 Subject: [PATCH 01/23] fix(consensus): reject small-order signatures in gossip, shreds, repair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agave v4.2.0-beta.1 verifies every P2P/consensus signature — transactions, gossip (CRDS, ping/pong, prune), shreds, and repair — through solana-signature -> ed25519-dalek 2.2.0 verify_strict (confirmed uniform by reading each path). verify_strict rejects small-order public keys A and small-order signature points R; Go's crypto/ed25519.Verify does not (it never decodes R). Mithril used the standard library at every one of these sites, so it accepted an adversarial-only class of signatures mainnet rejects — a crafted block, gossip packet, or repair message could be admitted by Mithril and discarded by mainnet, splitting the node from consensus. Route all eight sites through narya.VerifyStrict, which enforces DalekStrict regardless of any global profile: gossip contact-info and CRDS value (contact_info.go), ping/pong (message.go), turbine shred reference and cache paths (shred.go, sigcache.go), and repair ping and signed request (protocol.go). The shred cache only inserts after a successful verify and never caches failures, so the single miss-path check is sufficient — a strict rejection can never be memoized as valid. Tests: repair carries the rigorous proof — real ed25519vectors that crypto/ed25519 accepts and verify_strict rejects (small-order A and R) are now rejected by VerifySignedRequest; gossip and turbine cover the honest happy path, small-order-key rejection, agreement with narya.VerifyStrict on both paths, and that the shred cache never caches a strict rejection. The transaction replay path is fixed separately in the narya sigverify integration. The ed25519 precompile (pkg/sealevel) is a separate, feature-gated path analyzed under its own task. narya is wired via a local replace directive (../narya) for now; it must become a published version pin before this branch is shared. Co-Authored-By: Claude Fable 5 --- go.mod | 3 + pkg/gossip/contact_info.go | 6 +- pkg/gossip/message.go | 6 +- pkg/gossip/strict_verify_test.go | 88 +++++++++++++++++++++++++++ pkg/repair/protocol.go | 5 +- pkg/repair/strict_verify_test.go | 98 +++++++++++++++++++++++++++++++ pkg/turbine/shred.go | 3 +- pkg/turbine/sigcache.go | 3 +- pkg/turbine/strict_verify_test.go | 65 ++++++++++++++++++++ 9 files changed, 269 insertions(+), 8 deletions(-) create mode 100644 pkg/gossip/strict_verify_test.go create mode 100644 pkg/repair/strict_verify_test.go create mode 100644 pkg/turbine/strict_verify_test.go diff --git a/go.mod b/go.mod index 5d170136..1c0cf80b 100644 --- a/go.mod +++ b/go.mod @@ -110,6 +110,7 @@ require ( require ( filippo.io/edwards25519 v1.0.0 github.com/Overclock-Validator/bgls v0.0.0-20250309141600-b7db1bfbf3fa + github.com/Overclock-Validator/narya v0.0.0-00010101000000-000000000000 github.com/Overclock-Validator/solana-snapshot-finder-go v0.0.0-20260223201452-d8363b514fc0 github.com/Overclock-Validator/wide v0.0.0-20250221123529-f80959d02044 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect @@ -168,3 +169,5 @@ require ( golang.org/x/time v0.9.0 google.golang.org/protobuf v1.36.10 ) + +replace github.com/Overclock-Validator/narya => ../narya diff --git a/pkg/gossip/contact_info.go b/pkg/gossip/contact_info.go index a2485efc..ec8d39b0 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" ) 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..89a1d8cc 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" ) 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..bfc64ca1 --- /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" +) + +// 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..1230e4dc 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" ) 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 @@ -143,7 +144,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/turbine/shred.go b/pkg/turbine/shred.go index 4524214b..cc74096e 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" "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..bb8e360c 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" "github.com/gagliardetto/solana-go" ) @@ -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..e47c25a1 --- /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" + "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) + } + } +} From 51d8421c3772ead6a529f0c748384db41568671a Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:59:54 -0500 Subject: [PATCH 02/23] Refresh stale sigcache comment after strict-verify switch Co-Authored-By: Claude Fable 5 --- pkg/turbine/sigcache.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/turbine/sigcache.go b/pkg/turbine/sigcache.go index bb8e360c..6876037b 100644 --- a/pkg/turbine/sigcache.go +++ b/pkg/turbine/sigcache.go @@ -16,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 { From 5fe985b06442d50bcbc26047a1a20b6847e7c19e Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Sun, 26 Jul 2026 00:50:45 -0500 Subject: [PATCH 03/23] sigverify: add batching wrapper over narya strict verification New package owning backend selection and the batch entry point. Mithril verifies with stdlib today, which is non-strict, so it accepts small-order A and R that ed25519-dalek verify_strict -- and therefore mainnet -- rejects. Every path here applies the strict predicate. Batch is reusable worker-local scratch and Drain is the fill policy: block for the first item, non-blocking peek for the rest, no timer. Batch width is what sets cost per signature (a lone signature is roughly 3.7x the per-signature cost of one inside a group of eight), but a batching timer would trade tip latency away to buy that, so batches are whatever happened to be queued. The consensus claim is tested rather than asserted: the suite constructs a signature with a small-order public key that stdlib accepts and this package rejects, and pins that backend=stdlib faithfully reintroduces it. Co-Authored-By: Claude Opus 5 --- pkg/sigverify/drain.go | 46 ++++++ pkg/sigverify/sigverify.go | 210 ++++++++++++++++++++++++++ pkg/sigverify/sigverify_test.go | 257 ++++++++++++++++++++++++++++++++ 3 files changed, 513 insertions(+) create mode 100644 pkg/sigverify/drain.go create mode 100644 pkg/sigverify/sigverify.go create mode 100644 pkg/sigverify/sigverify_test.go diff --git a/pkg/sigverify/drain.go b/pkg/sigverify/drain.go new file mode 100644 index 00000000..704a0bb2 --- /dev/null +++ b/pkg/sigverify/drain.go @@ -0,0 +1,46 @@ +package sigverify + +// 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 + +// 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..f1addf86 --- /dev/null +++ b/pkg/sigverify/sigverify.go @@ -0,0 +1,210 @@ +// 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 ( + stded25519 "crypto/ed25519" + "fmt" + "sync/atomic" + + 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 bypasses the library entirely and calls crypto/ed25519 + // directly. This is the rollback switch: it restores the exact behaviour + // Mithril had before this package existed, INCLUDING the non-strict + // predicate, so it reintroduces the small-order divergence described above. + // It exists so an operator can eliminate this package as a suspect without + // rebuilding, not as a supported steady state. + 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() + +// bypass is read on every verification, so it is an atomic rather than a plain +// bool: Configure runs during startup but the verifiers run on pool goroutines, +// and the race detector is correctly unhappy about an unsynchronised handoff. +var bypass atomic.Bool + +// 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 + } + Cfg = cfg + + // 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 cfg.Backend { + case BackendStdlib: + bypass.Store(true) + return BackendStdlib, nil + + 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: + if err := narya.SetBackend(cfg.Backend); err != nil { + return "", fmt.Errorf("sigverify: select backend %q: %w", cfg.Backend, err) + } + return narya.ActiveBackend(), nil + + default: + return "", fmt.Errorf( + "sigverify.backend must be one of %q, %q, %q, %q; got %q", + BackendAuto, BackendR51, BackendGeneric, BackendStdlib, cfg.Backend) + } +} + +// Backend reports the backend in use, for metrics and diagnostics. +func Backend() string { + if bypass.Load() { + return BackendStdlib + } + 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 { + if bypass.Load() { + return 0 + } + 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 + } + if bypass.Load() { + return stded25519.Verify(pub[:], msg, sig) + } + 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 { + if len(b.pubs) == 0 { + return true + } + if bypass.Load() { + all := true + for i, pub := range b.pubs { + verdict := pub != nil && stded25519.Verify(pub[:], b.msgs[i], b.sigs[i]) + b.ok[i] = verdict + all = all && verdict + } + return all + } + 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..0eb61296 --- /dev/null +++ b/pkg/sigverify/sigverify_test.go @@ -0,0 +1,257 @@ +package sigverify + +import ( + stded25519 "crypto/ed25519" + "crypto/rand" + "fmt" + "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)) +} + +// The bypass switch is a rollback to the pre-existing behaviour, divergence +// included. Pinning that here keeps it an informed choice rather than a +// surprise, and fails loudly if someone later "fixes" the bypass into +// something that is no longer a faithful rollback. +func TestStdlibBypassReintroducesTheDivergence(t *testing.T) { + bypass.Store(true) + t.Cleanup(func() { bypass.Store(false) }) + + message := []byte("transfer everything") + pub, sig := smallOrderForgery(t, message) + + assert.True(t, VerifyOne(&pub, message, sig), + "backend=stdlib is a rollback: it accepts what stdlib accepts, small-order included") +} + +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") + } +} From 4bb407e79485259c8251ecbd36c67104108c5349 Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Sun, 26 Jul 2026 00:54:37 -0500 Subject: [PATCH 04/23] replay: verify drained groups of signatures instead of one at a time The pool worker blocked for one job and verified it alone. One transaction carries one or two signatures, which is the worst shape for a backend that verifies eight per AVX-512 group: a lone signature pays for a whole group and uses one lane of it. Workers now drain what is already queued and verify the group in one call. Draining costs nothing on an empty queue -- the worker still takes exactly the job it blocked for -- and a backlog here means catch-up, which is when throughput matters and per-block latency does not. Attribution is preserved exactly. The backend reports a verdict per signature, so the failing signer is identified without re-verifying anything, and the panic still names it. Tests place the bad signature at every boundary of a full group, and cover release of jobs belonging to different blocks. Count on the Sigverify timing now counts groups rather than transactions; SumNanoseconds keeps its documented meaning as total async worker time. Co-Authored-By: Claude Opus 5 --- go.mod | 4 +- pkg/gossip/contact_info.go | 2 +- pkg/gossip/message.go | 2 +- pkg/gossip/strict_verify_test.go | 2 +- pkg/repair/protocol.go | 2 +- pkg/replay/alpenglow_nanosecond_clock_test.go | 2 +- pkg/replay/sigverify_batch_test.go | 115 ++++++++++++++++++ pkg/replay/sigverify_pool.go | 22 +++- pkg/replay/transaction.go | 73 +++++++++-- pkg/turbine/shred.go | 2 +- pkg/turbine/sigcache.go | 2 +- pkg/turbine/strict_verify_test.go | 2 +- 12 files changed, 205 insertions(+), 25 deletions(-) create mode 100644 pkg/replay/sigverify_batch_test.go diff --git a/go.mod b/go.mod index 1c0cf80b..ed57d72a 100644 --- a/go.mod +++ b/go.mod @@ -110,7 +110,7 @@ require ( require ( filippo.io/edwards25519 v1.0.0 github.com/Overclock-Validator/bgls v0.0.0-20250309141600-b7db1bfbf3fa - github.com/Overclock-Validator/narya v0.0.0-00010101000000-000000000000 + github.com/Overclock-Validator/narya-ed25519 v0.0.0-00010101000000-000000000000 github.com/Overclock-Validator/solana-snapshot-finder-go v0.0.0-20260223201452-d8363b514fc0 github.com/Overclock-Validator/wide v0.0.0-20250221123529-f80959d02044 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect @@ -170,4 +170,4 @@ require ( google.golang.org/protobuf v1.36.10 ) -replace github.com/Overclock-Validator/narya => ../narya +replace github.com/Overclock-Validator/narya-ed25519 => ../narya diff --git a/pkg/gossip/contact_info.go b/pkg/gossip/contact_info.go index ec8d39b0..baed0d39 100644 --- a/pkg/gossip/contact_info.go +++ b/pkg/gossip/contact_info.go @@ -9,7 +9,7 @@ import ( "sort" "time" - narya "github.com/Overclock-Validator/narya/ed25519" + narya "github.com/Overclock-Validator/narya-ed25519/ed25519" ) const ( diff --git a/pkg/gossip/message.go b/pkg/gossip/message.go index 89a1d8cc..0d5b4303 100644 --- a/pkg/gossip/message.go +++ b/pkg/gossip/message.go @@ -7,7 +7,7 @@ import ( "fmt" "net" - narya "github.com/Overclock-Validator/narya/ed25519" + narya "github.com/Overclock-Validator/narya-ed25519/ed25519" ) var errUnsupportedCRDSValue = errors.New("unsupported CRDS value") diff --git a/pkg/gossip/strict_verify_test.go b/pkg/gossip/strict_verify_test.go index bfc64ca1..30a7ef27 100644 --- a/pkg/gossip/strict_verify_test.go +++ b/pkg/gossip/strict_verify_test.go @@ -5,7 +5,7 @@ import ( "encoding/hex" "testing" - narya "github.com/Overclock-Validator/narya/ed25519" + narya "github.com/Overclock-Validator/narya-ed25519/ed25519" ) // A canonical small-order point encoding. Mainnet (ed25519-dalek diff --git a/pkg/repair/protocol.go b/pkg/repair/protocol.go index 1230e4dc..cd457c36 100644 --- a/pkg/repair/protocol.go +++ b/pkg/repair/protocol.go @@ -9,7 +9,7 @@ import ( "time" "github.com/Overclock-Validator/mithril/pkg/gossip" - narya "github.com/Overclock-Validator/narya/ed25519" + narya "github.com/Overclock-Validator/narya-ed25519/ed25519" ) const ( 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..437d813b --- /dev/null +++ b/pkg/replay/sigverify_batch_test.go @@ -0,0 +1,115 @@ +package replay + +import ( + "fmt" + "strings" + "sync" + "testing" + + "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) +} diff --git a/pkg/replay/sigverify_pool.go b/pkg/replay/sigverify_pool.go index 642a5ac7..41bf0df2 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 { @@ -33,7 +44,7 @@ var ( ) // 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) { @@ -42,8 +53,15 @@ func enqueueSigverify(snapshot *sigverifySnapshot, wg *sync.WaitGroup) { workers := max(2, runtime.GOMAXPROCS(0)/2) 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.MaxDrain) + verifySignatureBatch(group, &batch) } }() } diff --git a/pkg/replay/transaction.go b/pkg/replay/transaction.go index 0d15b2c6..aa04b53b 100644 --- a/pkg/replay/transaction.go +++ b/pkg/replay/transaction.go @@ -19,6 +19,7 @@ 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/txverify" "github.com/Overclock-Validator/mithril/pkg/util" bin "github.com/gagliardetto/binary" @@ -491,24 +492,70 @@ 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])) } + + // 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 sigverify batch-size metric carries the group width + // so the pair stays interpretable. metrics.GlobalBlockReplay.Sigverify.AddTimingSince(start) } diff --git a/pkg/turbine/shred.go b/pkg/turbine/shred.go index cc74096e..dae24ddd 100644 --- a/pkg/turbine/shred.go +++ b/pkg/turbine/shred.go @@ -6,7 +6,7 @@ import ( "errors" "fmt" - narya "github.com/Overclock-Validator/narya/ed25519" + narya "github.com/Overclock-Validator/narya-ed25519/ed25519" "github.com/gagliardetto/solana-go" ) diff --git a/pkg/turbine/sigcache.go b/pkg/turbine/sigcache.go index 6876037b..7f3b1313 100644 --- a/pkg/turbine/sigcache.go +++ b/pkg/turbine/sigcache.go @@ -5,7 +5,7 @@ import ( "sync" "sync/atomic" - narya "github.com/Overclock-Validator/narya/ed25519" + narya "github.com/Overclock-Validator/narya-ed25519/ed25519" "github.com/gagliardetto/solana-go" ) diff --git a/pkg/turbine/strict_verify_test.go b/pkg/turbine/strict_verify_test.go index e47c25a1..33df5742 100644 --- a/pkg/turbine/strict_verify_test.go +++ b/pkg/turbine/strict_verify_test.go @@ -4,7 +4,7 @@ import ( "encoding/hex" "testing" - narya "github.com/Overclock-Validator/narya/ed25519" + narya "github.com/Overclock-Validator/narya-ed25519/ed25519" "github.com/gagliardetto/solana-go" ) From df8af6efd7c93f2babf049a3c6c3fd71016647c4 Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Sun, 26 Jul 2026 01:00:40 -0500 Subject: [PATCH 05/23] tpu: batch packet sigverify and fix versioned-transaction rejection Two TPU verifiers each marshalled the signed message themselves and so skipped the version-byte fixup. solana-go's MarshalV0 writes 0x7f, which is not the Solana wire encoding -- the signed prefix is 0x80 -- so a correctly signed versioned transaction was checked against bytes no client ever signs and was dropped at ingest. Both now go through txverify, which owns that fixup, and pick up the strict predicate at the same time. The pipeline worker verified one packet at a time. A transaction carries one or two signatures against a backend that verifies eight per group, so it paid for a group and used one lane. Workers now drain what is already queued; an empty queue still yields exactly the packet the worker blocked for, so quiet-ingress latency is unchanged. An unparseable packet contributes no signature lane, so the batch keeps a nil placeholder to stop verdicts sliding onto their neighbours -- covered by a test that interleaves garbage, corrupted, and honest packets. Co-Authored-By: Claude Opus 5 --- pkg/tpu/pipeline/pipeline.go | 56 ++++++++--- pkg/tpu/pipeline/verify.go | 9 +- pkg/tpu/quicserver/config.go | 4 +- pkg/tpu/sigverify/sigverify.go | 65 ++++++++++-- pkg/tpu/sigverify/sigverify_test.go | 148 ++++++++++++++++++++++++++++ pkg/tpu/tpu.go | 27 ++--- pkg/tpu/wire/wire.go | 14 +-- pkg/txverify/txverify.go | 93 ++++++++++++++++- 8 files changed, 363 insertions(+), 53 deletions(-) create mode 100644 pkg/tpu/sigverify/sigverify_test.go diff --git a/pkg/tpu/pipeline/pipeline.go b/pkg/tpu/pipeline/pipeline.go index 0b142dcf..048d1125 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" @@ -183,24 +184,57 @@ 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, 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.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) } - atomic.AddUint64(&stats.VerifiedPackets, 1) - atomic.AddUint64(&stats.VerifiedBytes, uint64(len(data))) - out <- pkt + 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 + } + + // 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/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/quicserver/config.go b/pkg/tpu/quicserver/config.go index 71dc0846..7c29f0f3 100644 --- a/pkg/tpu/quicserver/config.go +++ b/pkg/tpu/quicserver/config.go @@ -36,8 +36,8 @@ type ServerConfig struct { // Ingress receives completed transaction packets for the TPU pipeline. // When nil, packets are released after read. - Ingress chan<- packet.Packet - IngressStats *pipeline.IngressStats + Ingress chan<- packet.Packet + IngressStats *pipeline.IngressStats } func DefaultServerConfig() ServerConfig { 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/tpu/wire/wire.go b/pkg/tpu/wire/wire.go index 16354b8b..16461ee4 100644 --- a/pkg/tpu/wire/wire.go +++ b/pkg/tpu/wire/wire.go @@ -10,13 +10,13 @@ import ( const PacketDataSize = 1232 var ( - ErrEmpty = errors.New("empty transaction") - ErrTooLarge = errors.New("transaction exceeds packet size") - ErrInvalidEncoding = errors.New("invalid compact-u16 encoding") - ErrInvalidSigCount = errors.New("invalid signature count") - ErrInvalidMessage = errors.New("invalid message encoding") - ErrSigCountMismatch = errors.New("signature count mismatch") - ErrInsufficientData = errors.New("insufficient transaction data") + ErrEmpty = errors.New("empty transaction") + ErrTooLarge = errors.New("transaction exceeds packet size") + ErrInvalidEncoding = errors.New("invalid compact-u16 encoding") + ErrInvalidSigCount = errors.New("invalid signature count") + ErrInvalidMessage = errors.New("invalid message encoding") + ErrSigCountMismatch = errors.New("signature count mismatch") + ErrInsufficientData = errors.New("insufficient transaction data") ) // View is a zero-copy parsed legacy transaction wire layout. 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 +} From 4c01491fb98cfedd2bfe663cd4957f78282c9e69 Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Sun, 26 Jul 2026 01:17:53 -0500 Subject: [PATCH 06/23] sigverify: take a fair share when draining, not the whole queue Draining unconditionally was a latency regression waiting to happen. Eight workers facing eight queued items would let the first worker take all eight and verify them as one group while the other seven idled -- and one group of eight costs more wall-clock than eight workers each verifying one. Batching buys throughput per core; it must not buy it with parallelism that was already there. A turbine cancellation regression test caught exactly this: one worker swallowed both jobs meant to occupy two workers. FairShare gives a worker one item plus an equal cut of what remains, so a shallow queue spreads and a deep queue -- catch-up, where every worker is saturated anyway -- gives everyone full groups. The floor is 1: the item already in hand is never given back. Turbine additionally admits workers*BatchTarget transactions per wave rather than one per worker, since a producer that meters work out one-per-worker makes groups unreachable no matter what the consumer does. The fairness bound it was protecting is unchanged in spirit: tens of jobs parked, not tens of thousands. Nothing waits for a target width anywhere, so a partly-filled group cannot strand work. That property is now pinned in all four pipelines with counts that divide badly into groups (1, 3, 7, 9, 33, 65, 129); a stranded item hangs the join and fails the test rather than passing quietly. Co-Authored-By: Claude Opus 5 --- pkg/costmodel/limits.go | 6 +- pkg/costmodel/transaction_cost.go | 6 +- pkg/leaderschedule/leader_schedule_test.go | 16 ++-- pkg/replay/sigverify_batch_test.go | 52 ++++++++++++ pkg/replay/sigverify_pool.go | 9 +- pkg/rpcserver/simulate_transaction.go | 1 + pkg/rpcserver/token_balances.go | 13 ++- pkg/sigverify/drain.go | 41 +++++++++ pkg/sigverify/sigverify_test.go | 33 ++++++++ pkg/tpu/pipeline/pipeline.go | 6 +- pkg/tpu/pipeline/pipeline_test.go | 31 +++++++ pkg/tui/theme.go | 2 +- pkg/turbine/broadcast.go | 4 +- pkg/turbine/generate_test.go | 8 +- pkg/turbine/transaction_verifier.go | 96 +++++++++++++++++++--- pkg/turbine/transaction_verifier_test.go | 33 ++++++++ pkg/turbine/vector.go | 30 +++---- pkg/turbine/weighted_shuffle.go | 2 +- pkg/version/version.go | 7 +- 19 files changed, 333 insertions(+), 63 deletions(-) diff --git a/pkg/costmodel/limits.go b/pkg/costmodel/limits.go index fd73c275..9a675b9a 100644 --- a/pkg/costmodel/limits.go +++ b/pkg/costmodel/limits.go @@ -4,12 +4,12 @@ package costmodel const ( ComputeUnitToUSRatio = 30 - SignatureCost = ComputeUnitToUSRatio * 24 // 720 + SignatureCost = ComputeUnitToUSRatio * 24 // 720 Secp256k1VerifyCost = ComputeUnitToUSRatio * 223 Ed25519VerifyStrictCost = ComputeUnitToUSRatio * 80 Secp256r1VerifyCost = ComputeUnitToUSRatio * 160 - WriteLockUnits = ComputeUnitToUSRatio * 10 // 300 - InstructionDataBytesCost = 140 / ComputeUnitToUSRatio // ~4 CU per byte + WriteLockUnits = ComputeUnitToUSRatio * 10 // 300 + InstructionDataBytesCost = 140 / ComputeUnitToUSRatio // ~4 CU per byte MaxBlockUnitsSIMD0256 = 60_000_000 MaxBlockUnitsSIMD0286 = 100_000_000 diff --git a/pkg/costmodel/transaction_cost.go b/pkg/costmodel/transaction_cost.go index c4399bf5..748f10e8 100644 --- a/pkg/costmodel/transaction_cost.go +++ b/pkg/costmodel/transaction_cost.go @@ -41,9 +41,9 @@ func EstimateTransactionCost(tx *solana.Transaction, feats *features.Features) ( if err != nil { // Agave treats compute-budget parse failure as zero execution cost (tx won't execute). return TransactionCost{ - SignatureCost: signatureCost(tx), - WriteLockCost: writeLockCost(countWriteLocks(tx)), - DataBytesCost: instructionDataCost(tx), + SignatureCost: signatureCost(tx), + WriteLockCost: writeLockCost(countWriteLocks(tx)), + DataBytesCost: instructionDataCost(tx), WritableAccounts: writableAccounts(tx), }, nil } diff --git a/pkg/leaderschedule/leader_schedule_test.go b/pkg/leaderschedule/leader_schedule_test.go index 4cfcb22d..b20ea54b 100644 --- a/pkg/leaderschedule/leader_schedule_test.go +++ b/pkg/leaderschedule/leader_schedule_test.go @@ -165,11 +165,12 @@ func TestEpoch905TieBreakPubkeys(t *testing.T) { } // pubkeyFromU16 creates a deterministic pubkey matching Agave's test helper -// fn pubkey_from_u16(n: u16) -> Pubkey { -// let mut bytes = [0; 32]; -// bytes[0..2].copy_from_slice(&n.to_le_bytes()); -// Pubkey::new_from_array(bytes) -// } +// +// fn pubkey_from_u16(n: u16) -> Pubkey { +// let mut bytes = [0; 32]; +// bytes[0..2].copy_from_slice(&n.to_le_bytes()); +// Pubkey::new_from_array(bytes) +// } func pubkeyFromU16(n uint16) solana.PublicKey { var bytes [32]byte binary.LittleEndian.PutUint16(bytes[:], n) @@ -439,8 +440,9 @@ func TestUint64nAgaveCompatibility(t *testing.T) { // Test vectors from firedancer/src/ballet/chacha20/test_chacha_rng_roll.c using MODE_MOD. // // Firedancer uses MODE_MOD for leader schedule (same as Agave's UniformU64Sampler): -// zone = ULONG_MAX - ((ULONG_MAX - n + 1) % n) -// accept if lo <= zone +// +// zone = ULONG_MAX - ((ULONG_MAX - n + 1) % n) +// accept if lo <= zone func TestUint64nFiredancerCompatibility(t *testing.T) { // Firedancer test seed: [0x41; 32] (all bytes set to 'A') var seedBytes [32]byte diff --git a/pkg/replay/sigverify_batch_test.go b/pkg/replay/sigverify_batch_test.go index 437d813b..002ba761 100644 --- a/pkg/replay/sigverify_batch_test.go +++ b/pkg/replay/sigverify_batch_test.go @@ -5,6 +5,7 @@ import ( "strings" "sync" "testing" + "time" "github.com/Overclock-Validator/mithril/pkg/sigverify" ) @@ -113,3 +114,54 @@ func TestVerifySignatureBatchHaltsOnArityMismatch(t *testing.T) { 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. +// +// If a leftover could strand, wg.Wait() below never returns and this test hangs +// rather than failing quietly. +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 41bf0df2..69fce016 100644 --- a/pkg/replay/sigverify_pool.go +++ b/pkg/replay/sigverify_pool.go @@ -39,8 +39,9 @@ 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 @@ -51,6 +52,7 @@ 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 @@ -60,7 +62,8 @@ func enqueueSigverify(snapshot *sigverifySnapshot, wg *sync.WaitGroup) { batch sigverify.Batch ) for job := range sigverifyQueue { - group = sigverify.Drain(group, job, sigverifyQueue, sigverify.MaxDrain) + group = sigverify.Drain(group, job, sigverifyQueue, + sigverify.FairShare(len(sigverifyQueue), sigverifyWorkers, sigverify.MaxDrain)) verifySignatureBatch(group, &batch) } }() diff --git a/pkg/rpcserver/simulate_transaction.go b/pkg/rpcserver/simulate_transaction.go index 98e88dc2..e8294545 100644 --- a/pkg/rpcserver/simulate_transaction.go +++ b/pkg/rpcserver/simulate_transaction.go @@ -603,6 +603,7 @@ func ptrSliceTokenBalance(s []TokenBalancePayload) *[]TokenBalancePayload { // in caller-specified order. Lookup precedence: // 1. post-execution transaction context (most up-to-date for tx accounts) // 2. accountsdb fallback (for addresses NOT touched by the tx) +// // Each entry is nil (JSON null) when the address can't be resolved. // Always returns a slice of length len(conf.accounts.addresses), matching // Agave so clients can index by request order. diff --git a/pkg/rpcserver/token_balances.go b/pkg/rpcserver/token_balances.go index b44e87f6..d321a513 100644 --- a/pkg/rpcserver/token_balances.go +++ b/pkg/rpcserver/token_balances.go @@ -12,7 +12,7 @@ import ( // SPL Token program IDs. var ( - splTokenProgramID = solana.MustPublicKeyFromBase58("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") + splTokenProgramID = solana.MustPublicKeyFromBase58("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") splToken2022ProgramID = solana.MustPublicKeyFromBase58("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb") ) @@ -20,14 +20,14 @@ var ( // the legacy program and Token-2022 (Token-2022 stores extension data // past byte 165, but the leading account state has the same layout). const ( - tokenAccountSize = 165 - tokenAccountMintOffset = 0 - tokenAccountOwnerOffset = 32 + tokenAccountSize = 165 + tokenAccountMintOffset = 0 + tokenAccountOwnerOffset = 32 tokenAccountAmountOffset = 64 // Mint account layout: 82 bytes for legacy SPL Token. decimals lives at // offset 44 (mintAuthorityOption 4 + mintAuthority 32 + supply 8 = 44). - mintAccountSize = 82 - mintDecimalsOffset = 44 + mintAccountSize = 82 + mintDecimalsOffset = 44 ) // isTokenProgramOwner reports whether an account is owned by either the @@ -195,4 +195,3 @@ func tokenBalancesFromAccounts(txAccts []*accounts.Account, db *accountsdb.Accou mintDecimals := fetchMintDecimals(txAccts, db, slot) return extractTokenBalances(txAccts, mintDecimals) } - diff --git a/pkg/sigverify/drain.go b/pkg/sigverify/drain.go index 704a0bb2..f243c1ba 100644 --- a/pkg/sigverify/drain.go +++ b/pkg/sigverify/drain.go @@ -1,5 +1,15 @@ 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. // @@ -10,6 +20,37 @@ package sigverify // 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. // diff --git a/pkg/sigverify/sigverify_test.go b/pkg/sigverify/sigverify_test.go index 0eb61296..867c2180 100644 --- a/pkg/sigverify/sigverify_test.go +++ b/pkg/sigverify/sigverify_test.go @@ -255,3 +255,36 @@ func TestDrainReusesTheDestinationSlice(t *testing.T) { 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/tpu/pipeline/pipeline.go b/pkg/tpu/pipeline/pipeline.go index 048d1125..2038c2d7 100644 --- a/pkg/tpu/pipeline/pipeline.go +++ b/pkg/tpu/pipeline/pipeline.go @@ -173,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) @@ -195,6 +195,7 @@ func startSigverifyPool( func runSigverifyWorker( in <-chan packet.Packet, out chan<- packet.Packet, + workers int, stats *SigverifyStats, ) { // Worker-local scratch, reused across groups. @@ -205,7 +206,8 @@ func runSigverifyWorker( verifier batchVerifier ) for pkt := range in { - group = sigverify.Drain(group, pkt, in, sigverify.MaxDrain) + group = sigverify.Drain(group, pkt, in, + sigverify.FairShare(len(in), workers, sigverify.MaxDrain)) payloads = payloads[:0] for _, p := range group { 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/tui/theme.go b/pkg/tui/theme.go index eab5333d..803b422e 100644 --- a/pkg/tui/theme.go +++ b/pkg/tui/theme.go @@ -17,7 +17,7 @@ var ( ColorTextDisabled = lipgloss.Color("#606060") // hints, shortcuts — visible on dark bg // Semantic - ColorSuccess = MithrilTeal // teal doubles as success indicator + ColorSuccess = MithrilTeal // teal doubles as success indicator ColorError = lipgloss.Color("196") ColorWarn = lipgloss.Color("214") diff --git a/pkg/turbine/broadcast.go b/pkg/turbine/broadcast.go index fee22915..1a71530f 100644 --- a/pkg/turbine/broadcast.go +++ b/pkg/turbine/broadcast.go @@ -116,8 +116,8 @@ type BroadcastSessionConfig struct { // It seeds the chained merkle root embedded in this slot's first FEC batch. ParentChainedMerkleRoot solana.Hash Broadcaster PacketBroadcaster - UserAgent []byte - Version uint16 + UserAgent []byte + Version uint16 } func NewBroadcastSession(cfg BroadcastSessionConfig) *BroadcastSession { diff --git a/pkg/turbine/generate_test.go b/pkg/turbine/generate_test.go index 08313395..265c38e1 100644 --- a/pkg/turbine/generate_test.go +++ b/pkg/turbine/generate_test.go @@ -184,10 +184,10 @@ func TestMakeShredsFromAlpenglowBlock(t *testing.T) { } var ( - chainedRoot = solana.Hash{5} - nextData uint32 = 0 - nextCode uint32 = 0 - allDataShreds []*Shred + chainedRoot = solana.Hash{5} + nextData uint32 = 0 + nextCode uint32 = 0 + allDataShreds []*Shred ) for _, component := range buildAlpenglowSlot(t) { packets, root, newData, newCode, err := gen.MakeShredsFromData( 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/turbine/vector.go b/pkg/turbine/vector.go index 78355972..d1b02723 100644 --- a/pkg/turbine/vector.go +++ b/pkg/turbine/vector.go @@ -16,16 +16,16 @@ import ( const VectorVersion = 1 type VectorFixture struct { - Version int `json:"version"` - SelfPubkey string `json:"self_pubkey"` - SelfTVU string `json:"self_tvu,omitempty"` - SelfStake uint64 `json:"self_stake,omitempty"` - UseChaCha8 bool `json:"use_cha_cha_8"` - DedupTVUAddrs bool `json:"dedup_tvu_addrs"` - Slot uint64 `json:"slot"` - ShredType uint8 `json:"shred_type"` - MaxShredIndex uint32 `json:"max_shred_index"` - Peers []VectorPeer `json:"peers"` + Version int `json:"version"` + SelfPubkey string `json:"self_pubkey"` + SelfTVU string `json:"self_tvu,omitempty"` + SelfStake uint64 `json:"self_stake,omitempty"` + UseChaCha8 bool `json:"use_cha_cha_8"` + DedupTVUAddrs bool `json:"dedup_tvu_addrs"` + Slot uint64 `json:"slot"` + ShredType uint8 `json:"shred_type"` + MaxShredIndex uint32 `json:"max_shred_index"` + Peers []VectorPeer `json:"peers"` StakedNoContact []VectorStakeOnly `json:"staked_no_contact"` } @@ -56,11 +56,11 @@ type VectorBroadcastEntry struct { } type VectorOutput struct { - Version int `json:"version"` - Implementation string `json:"implementation"` - Fixture VectorFixture `json:"fixture"` - Nodes []VectorNode `json:"nodes"` - BroadcastPeers []VectorBroadcastEntry `json:"broadcast_peers"` + Version int `json:"version"` + Implementation string `json:"implementation"` + Fixture VectorFixture `json:"fixture"` + Nodes []VectorNode `json:"nodes"` + BroadcastPeers []VectorBroadcastEntry `json:"broadcast_peers"` } func LoadVectorFixture(path string) (VectorFixture, error) { diff --git a/pkg/turbine/weighted_shuffle.go b/pkg/turbine/weighted_shuffle.go index b9d708e1..35132b33 100644 --- a/pkg/turbine/weighted_shuffle.go +++ b/pkg/turbine/weighted_shuffle.go @@ -142,4 +142,4 @@ func rngUint64n(rng rngSource, n uint64) uint64 { return 0 } return rng.Uint64() % n -} \ No newline at end of file +} diff --git a/pkg/version/version.go b/pkg/version/version.go index 18120c72..b34d7cf9 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -1,9 +1,10 @@ package version // These variables are set at build time via ldflags: -// go build -ldflags "-X github.com/Overclock-Validator/mithril/pkg/version.Version=v1.0.0 -// -X github.com/Overclock-Validator/mithril/pkg/version.GitCommit=abc123 -// -X github.com/Overclock-Validator/mithril/pkg/version.BuildDate=2025-01-01" +// +// go build -ldflags "-X github.com/Overclock-Validator/mithril/pkg/version.Version=v1.0.0 +// -X github.com/Overclock-Validator/mithril/pkg/version.GitCommit=abc123 +// -X github.com/Overclock-Validator/mithril/pkg/version.BuildDate=2025-01-01" var ( // Version is the semantic version (e.g., "v1.0.0" or "dev") Version = "dev" From 941ef85a54b864083694b3e64d0bdb2439dd3648 Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Sun, 26 Jul 2026 01:23:42 -0500 Subject: [PATCH 07/23] revert unrelated gofmt churn A package-wide gofmt -w while iterating reformatted files this change does not touch. Whitespace-only, but it does not belong in a consensus-critical diff. Co-Authored-By: Claude Opus 5 --- pkg/costmodel/limits.go | 6 ++-- pkg/costmodel/transaction_cost.go | 6 ++-- pkg/leaderschedule/leader_schedule_test.go | 16 +++++----- pkg/replay/alpenglow_nanosecond_clock_test.go | 2 +- pkg/rpcserver/simulate_transaction.go | 1 - pkg/rpcserver/token_balances.go | 13 ++++---- pkg/tpu/quicserver/config.go | 4 +-- pkg/tpu/wire/wire.go | 14 ++++----- pkg/tui/theme.go | 2 +- pkg/turbine/broadcast.go | 4 +-- pkg/turbine/generate_test.go | 8 ++--- pkg/turbine/vector.go | 30 +++++++++---------- pkg/turbine/weighted_shuffle.go | 2 +- pkg/version/version.go | 7 ++--- 14 files changed, 56 insertions(+), 59 deletions(-) diff --git a/pkg/costmodel/limits.go b/pkg/costmodel/limits.go index 9a675b9a..fd73c275 100644 --- a/pkg/costmodel/limits.go +++ b/pkg/costmodel/limits.go @@ -4,12 +4,12 @@ package costmodel const ( ComputeUnitToUSRatio = 30 - SignatureCost = ComputeUnitToUSRatio * 24 // 720 + SignatureCost = ComputeUnitToUSRatio * 24 // 720 Secp256k1VerifyCost = ComputeUnitToUSRatio * 223 Ed25519VerifyStrictCost = ComputeUnitToUSRatio * 80 Secp256r1VerifyCost = ComputeUnitToUSRatio * 160 - WriteLockUnits = ComputeUnitToUSRatio * 10 // 300 - InstructionDataBytesCost = 140 / ComputeUnitToUSRatio // ~4 CU per byte + WriteLockUnits = ComputeUnitToUSRatio * 10 // 300 + InstructionDataBytesCost = 140 / ComputeUnitToUSRatio // ~4 CU per byte MaxBlockUnitsSIMD0256 = 60_000_000 MaxBlockUnitsSIMD0286 = 100_000_000 diff --git a/pkg/costmodel/transaction_cost.go b/pkg/costmodel/transaction_cost.go index 748f10e8..c4399bf5 100644 --- a/pkg/costmodel/transaction_cost.go +++ b/pkg/costmodel/transaction_cost.go @@ -41,9 +41,9 @@ func EstimateTransactionCost(tx *solana.Transaction, feats *features.Features) ( if err != nil { // Agave treats compute-budget parse failure as zero execution cost (tx won't execute). return TransactionCost{ - SignatureCost: signatureCost(tx), - WriteLockCost: writeLockCost(countWriteLocks(tx)), - DataBytesCost: instructionDataCost(tx), + SignatureCost: signatureCost(tx), + WriteLockCost: writeLockCost(countWriteLocks(tx)), + DataBytesCost: instructionDataCost(tx), WritableAccounts: writableAccounts(tx), }, nil } diff --git a/pkg/leaderschedule/leader_schedule_test.go b/pkg/leaderschedule/leader_schedule_test.go index b20ea54b..4cfcb22d 100644 --- a/pkg/leaderschedule/leader_schedule_test.go +++ b/pkg/leaderschedule/leader_schedule_test.go @@ -165,12 +165,11 @@ func TestEpoch905TieBreakPubkeys(t *testing.T) { } // pubkeyFromU16 creates a deterministic pubkey matching Agave's test helper -// -// fn pubkey_from_u16(n: u16) -> Pubkey { -// let mut bytes = [0; 32]; -// bytes[0..2].copy_from_slice(&n.to_le_bytes()); -// Pubkey::new_from_array(bytes) -// } +// fn pubkey_from_u16(n: u16) -> Pubkey { +// let mut bytes = [0; 32]; +// bytes[0..2].copy_from_slice(&n.to_le_bytes()); +// Pubkey::new_from_array(bytes) +// } func pubkeyFromU16(n uint16) solana.PublicKey { var bytes [32]byte binary.LittleEndian.PutUint16(bytes[:], n) @@ -440,9 +439,8 @@ func TestUint64nAgaveCompatibility(t *testing.T) { // Test vectors from firedancer/src/ballet/chacha20/test_chacha_rng_roll.c using MODE_MOD. // // Firedancer uses MODE_MOD for leader schedule (same as Agave's UniformU64Sampler): -// -// zone = ULONG_MAX - ((ULONG_MAX - n + 1) % n) -// accept if lo <= zone +// zone = ULONG_MAX - ((ULONG_MAX - n + 1) % n) +// accept if lo <= zone func TestUint64nFiredancerCompatibility(t *testing.T) { // Firedancer test seed: [0x41; 32] (all bytes set to 'A') var seedBytes [32]byte diff --git a/pkg/replay/alpenglow_nanosecond_clock_test.go b/pkg/replay/alpenglow_nanosecond_clock_test.go index 4661fdb0..a70badff 100644 --- a/pkg/replay/alpenglow_nanosecond_clock_test.go +++ b/pkg/replay/alpenglow_nanosecond_clock_test.go @@ -5,8 +5,8 @@ import ( "sync" "testing" - "github.com/Overclock-Validator/mithril/pkg/accounts" b "github.com/Overclock-Validator/mithril/pkg/block" + "github.com/Overclock-Validator/mithril/pkg/accounts" "github.com/Overclock-Validator/mithril/pkg/sealevel" "github.com/gagliardetto/solana-go" "github.com/stretchr/testify/require" diff --git a/pkg/rpcserver/simulate_transaction.go b/pkg/rpcserver/simulate_transaction.go index e8294545..98e88dc2 100644 --- a/pkg/rpcserver/simulate_transaction.go +++ b/pkg/rpcserver/simulate_transaction.go @@ -603,7 +603,6 @@ func ptrSliceTokenBalance(s []TokenBalancePayload) *[]TokenBalancePayload { // in caller-specified order. Lookup precedence: // 1. post-execution transaction context (most up-to-date for tx accounts) // 2. accountsdb fallback (for addresses NOT touched by the tx) -// // Each entry is nil (JSON null) when the address can't be resolved. // Always returns a slice of length len(conf.accounts.addresses), matching // Agave so clients can index by request order. diff --git a/pkg/rpcserver/token_balances.go b/pkg/rpcserver/token_balances.go index d321a513..b44e87f6 100644 --- a/pkg/rpcserver/token_balances.go +++ b/pkg/rpcserver/token_balances.go @@ -12,7 +12,7 @@ import ( // SPL Token program IDs. var ( - splTokenProgramID = solana.MustPublicKeyFromBase58("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") + splTokenProgramID = solana.MustPublicKeyFromBase58("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") splToken2022ProgramID = solana.MustPublicKeyFromBase58("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb") ) @@ -20,14 +20,14 @@ var ( // the legacy program and Token-2022 (Token-2022 stores extension data // past byte 165, but the leading account state has the same layout). const ( - tokenAccountSize = 165 - tokenAccountMintOffset = 0 - tokenAccountOwnerOffset = 32 + tokenAccountSize = 165 + tokenAccountMintOffset = 0 + tokenAccountOwnerOffset = 32 tokenAccountAmountOffset = 64 // Mint account layout: 82 bytes for legacy SPL Token. decimals lives at // offset 44 (mintAuthorityOption 4 + mintAuthority 32 + supply 8 = 44). - mintAccountSize = 82 - mintDecimalsOffset = 44 + mintAccountSize = 82 + mintDecimalsOffset = 44 ) // isTokenProgramOwner reports whether an account is owned by either the @@ -195,3 +195,4 @@ func tokenBalancesFromAccounts(txAccts []*accounts.Account, db *accountsdb.Accou mintDecimals := fetchMintDecimals(txAccts, db, slot) return extractTokenBalances(txAccts, mintDecimals) } + diff --git a/pkg/tpu/quicserver/config.go b/pkg/tpu/quicserver/config.go index 7c29f0f3..71dc0846 100644 --- a/pkg/tpu/quicserver/config.go +++ b/pkg/tpu/quicserver/config.go @@ -36,8 +36,8 @@ type ServerConfig struct { // Ingress receives completed transaction packets for the TPU pipeline. // When nil, packets are released after read. - Ingress chan<- packet.Packet - IngressStats *pipeline.IngressStats + Ingress chan<- packet.Packet + IngressStats *pipeline.IngressStats } func DefaultServerConfig() ServerConfig { diff --git a/pkg/tpu/wire/wire.go b/pkg/tpu/wire/wire.go index 16461ee4..16354b8b 100644 --- a/pkg/tpu/wire/wire.go +++ b/pkg/tpu/wire/wire.go @@ -10,13 +10,13 @@ import ( const PacketDataSize = 1232 var ( - ErrEmpty = errors.New("empty transaction") - ErrTooLarge = errors.New("transaction exceeds packet size") - ErrInvalidEncoding = errors.New("invalid compact-u16 encoding") - ErrInvalidSigCount = errors.New("invalid signature count") - ErrInvalidMessage = errors.New("invalid message encoding") - ErrSigCountMismatch = errors.New("signature count mismatch") - ErrInsufficientData = errors.New("insufficient transaction data") + ErrEmpty = errors.New("empty transaction") + ErrTooLarge = errors.New("transaction exceeds packet size") + ErrInvalidEncoding = errors.New("invalid compact-u16 encoding") + ErrInvalidSigCount = errors.New("invalid signature count") + ErrInvalidMessage = errors.New("invalid message encoding") + ErrSigCountMismatch = errors.New("signature count mismatch") + ErrInsufficientData = errors.New("insufficient transaction data") ) // View is a zero-copy parsed legacy transaction wire layout. diff --git a/pkg/tui/theme.go b/pkg/tui/theme.go index 803b422e..eab5333d 100644 --- a/pkg/tui/theme.go +++ b/pkg/tui/theme.go @@ -17,7 +17,7 @@ var ( ColorTextDisabled = lipgloss.Color("#606060") // hints, shortcuts — visible on dark bg // Semantic - ColorSuccess = MithrilTeal // teal doubles as success indicator + ColorSuccess = MithrilTeal // teal doubles as success indicator ColorError = lipgloss.Color("196") ColorWarn = lipgloss.Color("214") diff --git a/pkg/turbine/broadcast.go b/pkg/turbine/broadcast.go index 1a71530f..fee22915 100644 --- a/pkg/turbine/broadcast.go +++ b/pkg/turbine/broadcast.go @@ -116,8 +116,8 @@ type BroadcastSessionConfig struct { // It seeds the chained merkle root embedded in this slot's first FEC batch. ParentChainedMerkleRoot solana.Hash Broadcaster PacketBroadcaster - UserAgent []byte - Version uint16 + UserAgent []byte + Version uint16 } func NewBroadcastSession(cfg BroadcastSessionConfig) *BroadcastSession { diff --git a/pkg/turbine/generate_test.go b/pkg/turbine/generate_test.go index 265c38e1..08313395 100644 --- a/pkg/turbine/generate_test.go +++ b/pkg/turbine/generate_test.go @@ -184,10 +184,10 @@ func TestMakeShredsFromAlpenglowBlock(t *testing.T) { } var ( - chainedRoot = solana.Hash{5} - nextData uint32 = 0 - nextCode uint32 = 0 - allDataShreds []*Shred + chainedRoot = solana.Hash{5} + nextData uint32 = 0 + nextCode uint32 = 0 + allDataShreds []*Shred ) for _, component := range buildAlpenglowSlot(t) { packets, root, newData, newCode, err := gen.MakeShredsFromData( diff --git a/pkg/turbine/vector.go b/pkg/turbine/vector.go index d1b02723..78355972 100644 --- a/pkg/turbine/vector.go +++ b/pkg/turbine/vector.go @@ -16,16 +16,16 @@ import ( const VectorVersion = 1 type VectorFixture struct { - Version int `json:"version"` - SelfPubkey string `json:"self_pubkey"` - SelfTVU string `json:"self_tvu,omitempty"` - SelfStake uint64 `json:"self_stake,omitempty"` - UseChaCha8 bool `json:"use_cha_cha_8"` - DedupTVUAddrs bool `json:"dedup_tvu_addrs"` - Slot uint64 `json:"slot"` - ShredType uint8 `json:"shred_type"` - MaxShredIndex uint32 `json:"max_shred_index"` - Peers []VectorPeer `json:"peers"` + Version int `json:"version"` + SelfPubkey string `json:"self_pubkey"` + SelfTVU string `json:"self_tvu,omitempty"` + SelfStake uint64 `json:"self_stake,omitempty"` + UseChaCha8 bool `json:"use_cha_cha_8"` + DedupTVUAddrs bool `json:"dedup_tvu_addrs"` + Slot uint64 `json:"slot"` + ShredType uint8 `json:"shred_type"` + MaxShredIndex uint32 `json:"max_shred_index"` + Peers []VectorPeer `json:"peers"` StakedNoContact []VectorStakeOnly `json:"staked_no_contact"` } @@ -56,11 +56,11 @@ type VectorBroadcastEntry struct { } type VectorOutput struct { - Version int `json:"version"` - Implementation string `json:"implementation"` - Fixture VectorFixture `json:"fixture"` - Nodes []VectorNode `json:"nodes"` - BroadcastPeers []VectorBroadcastEntry `json:"broadcast_peers"` + Version int `json:"version"` + Implementation string `json:"implementation"` + Fixture VectorFixture `json:"fixture"` + Nodes []VectorNode `json:"nodes"` + BroadcastPeers []VectorBroadcastEntry `json:"broadcast_peers"` } func LoadVectorFixture(path string) (VectorFixture, error) { diff --git a/pkg/turbine/weighted_shuffle.go b/pkg/turbine/weighted_shuffle.go index 35132b33..b9d708e1 100644 --- a/pkg/turbine/weighted_shuffle.go +++ b/pkg/turbine/weighted_shuffle.go @@ -142,4 +142,4 @@ func rngUint64n(rng rngSource, n uint64) uint64 { return 0 } return rng.Uint64() % n -} +} \ No newline at end of file diff --git a/pkg/version/version.go b/pkg/version/version.go index b34d7cf9..18120c72 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -1,10 +1,9 @@ package version // These variables are set at build time via ldflags: -// -// go build -ldflags "-X github.com/Overclock-Validator/mithril/pkg/version.Version=v1.0.0 -// -X github.com/Overclock-Validator/mithril/pkg/version.GitCommit=abc123 -// -X github.com/Overclock-Validator/mithril/pkg/version.BuildDate=2025-01-01" +// go build -ldflags "-X github.com/Overclock-Validator/mithril/pkg/version.Version=v1.0.0 +// -X github.com/Overclock-Validator/mithril/pkg/version.GitCommit=abc123 +// -X github.com/Overclock-Validator/mithril/pkg/version.BuildDate=2025-01-01" var ( // Version is the semantic version (e.g., "v1.0.0" or "dev") Version = "dev" From c61e4d0b64690310789d89111e2d2d4fa66a2db9 Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Sun, 26 Jul 2026 01:28:47 -0500 Subject: [PATCH 08/23] replay: clarify what the no-stranding test actually asserts The comment said the test 'hangs rather than failing quietly', which reads backwards. The point is that asserting on the join completing -- rather than just on no error being returned -- means a stranded item surfaces as a timeout instead of slipping through as a pass. Co-Authored-By: Claude Opus 5 --- pkg/replay/sigverify_batch_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/replay/sigverify_batch_test.go b/pkg/replay/sigverify_batch_test.go index 002ba761..c8637533 100644 --- a/pkg/replay/sigverify_batch_test.go +++ b/pkg/replay/sigverify_batch_test.go @@ -119,8 +119,10 @@ func TestVerifySignatureBatchHaltsOnArityMismatch(t *testing.T) { // 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. // -// If a leftover could strand, wg.Wait() below never returns and this test hangs -// rather than failing quietly. +// 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) { From 2167b38e9bb59d8e91fcfc6408e8167602273e11 Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Sun, 26 Jul 2026 01:32:04 -0500 Subject: [PATCH 09/23] repair: note the batching shape inbound request verification will need VerifySignedRequest authenticates repair requests from peers. Mithril is requester-side only today so nothing outside tests reaches it, but serving repair is normal validator work, and when it is wired up this becomes a packet-rate consumer on a socket loop -- the shape that wants Drain/FairShare rather than a one-at-a-time verify. Co-Authored-By: Claude Opus 5 --- pkg/repair/protocol.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pkg/repair/protocol.go b/pkg/repair/protocol.go index cd457c36..77281f8a 100644 --- a/pkg/repair/protocol.go +++ b/pkg/repair/protocol.go @@ -137,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 From f4264c03ceb110eef198bc31dbdf53cb7a2c34ee Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Sun, 26 Jul 2026 01:37:24 -0500 Subject: [PATCH 10/23] sigverify: operator config, startup reporting, and Prometheus series Nothing selected a backend, so every verification ran on the portable pure-Go path: the strict predicate was live but none of the acceleration was, and the stdlib rollback switch was unreachable. This is the link that turns it on. --sigverify-backend / tuning.sigverify_backend resolves through the house flag-over-TOML precedence and installs the backend during config parsing, so a machine that cannot run the requested one fails at startup rather than at the first block. The resolved name is printed at startup because under 'auto' it is the only way an operator learns whether they got the accelerated path -- and because backend=stdlib deserves to say out loud that it accepts signatures mainnet rejects. Replay sigverify had no Prometheus series at all. It gets two: group duration and group width. 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 arriving too thinly to fill one. Co-Authored-By: Claude Opus 5 --- cmd/mithril/configcmd/configcmd.go | 1 + cmd/mithril/node/node.go | 29 +++++++++++++++++++ config.example.toml | 10 +++++++ pkg/replay/alpenglow_nanosecond_clock_test.go | 2 +- pkg/replay/transaction.go | 13 +++++++-- pkg/statsd/statsd.go | 14 ++++++++- 6 files changed, 65 insertions(+), 4 deletions(-) diff --git a/cmd/mithril/configcmd/configcmd.go b/cmd/mithril/configcmd/configcmd.go index 7fa51f46..dc11cbd9 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 is a rollback that weakens the predicate # ── Mithril's RPC server ───────────────────────────────────────────────── [rpc] diff --git a/cmd/mithril/node/node.go b/cmd/mithril/node/node.go index 43b2faaf..f0f871cc 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 (stdlib is a rollback that restores the pre-strict predicate)") 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") @@ -2931,6 +2948,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 = "ROLLBACK: non-strict, accepts signatures mainnet rejects" + } + 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/config.example.toml b/config.example.toml index c8119353..f0fb1b6c 100644 --- a/config.example.toml +++ b/config.example.toml @@ -519,6 +519,16 @@ 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 - ROLLBACK ONLY. Bypasses signature verification hardening and + # restores Go's stdlib predicate, which accepts small-order keys + # that Solana mainnet rejects. Diagnostic use only. + sigverify_backend = "auto" + # Enable/disable pool allocator for slices use_pool = true 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/transaction.go b/pkg/replay/transaction.go index aa04b53b..d67a53bf 100644 --- a/pkg/replay/transaction.go +++ b/pkg/replay/transaction.go @@ -20,6 +20,7 @@ import ( "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" @@ -554,9 +555,17 @@ func verifySignatureBatch(group []sigverifyJob, batch *sigverify.Batch) { // 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 sigverify batch-size metric carries the group width + // mean per group. The group-width metric below carries the missing factor, // so the pair stays interpretable. - metrics.GlobalBlockReplay.Sigverify.AddTimingSince(start) + 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/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, From 831567fb9b209bfb014c3a98796b24e65f0eb339 Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Sun, 26 Jul 2026 02:04:33 -0500 Subject: [PATCH 11/23] go.mod: depend on published narya-ed25519 instead of a local path The branch carried 'replace => ../narya' while the library was private, which made it unbuildable for anyone without that checkout sitting beside the repo -- CI included. narya-ed25519 is public now, so this pins an ordinary pseudo-version, matching how Overclock-Validator/crypto is consumed. Co-Authored-By: Claude Opus 5 --- go.mod | 4 +--- go.sum | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ed57d72a..27122d67 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-20260726065311-1625c1837692 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 @@ -110,7 +111,6 @@ require ( require ( filippo.io/edwards25519 v1.0.0 github.com/Overclock-Validator/bgls v0.0.0-20250309141600-b7db1bfbf3fa - github.com/Overclock-Validator/narya-ed25519 v0.0.0-00010101000000-000000000000 github.com/Overclock-Validator/solana-snapshot-finder-go v0.0.0-20260223201452-d8363b514fc0 github.com/Overclock-Validator/wide v0.0.0-20250221123529-f80959d02044 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect @@ -169,5 +169,3 @@ require ( golang.org/x/time v0.9.0 google.golang.org/protobuf v1.36.10 ) - -replace github.com/Overclock-Validator/narya-ed25519 => ../narya diff --git a/go.sum b/go.sum index 12e59a41..392f65d9 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-20260726065311-1625c1837692 h1:trNDlVdZDY84KNUP7ioyPMuRVBfgo7ghELqX8QUUW00= +github.com/Overclock-Validator/narya-ed25519 v0.0.0-20260726065311-1625c1837692/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= From 6b5d1fcde9235e8fd22a46140b07485e1d849b0e Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Tue, 28 Jul 2026 01:19:33 -0500 Subject: [PATCH 12/23] sealevel: accept a non-canonical A in the ed25519 precompile The strict-path options were built as a struct literal setting only AllowSmallOrderA, AllowSmallOrderR and CofactorlessVerify. Go zeroes every field a literal omits, so AllowNonCanonicalA silently became false and the precompile rejected public keys the reference predicate accepts. voi's own VerifyOptionsStdLib sets that field true, so the omission also inverted the library's default rather than merely leaving it. No constructible input separates the two behaviours: a non-canonical encoding requires y < 19, and every curve point with such a y whose discrete log is computable is small order, which AllowSmallOrderA already rejects. So this changes no verdict reachable today. It is worth fixing anyway, because the gap between "what we meant" and "what the literal said" is not visible at the call site, and the next field to go missing may not be unreachable. Moving the options into a named package-level value is the part that makes the invariant testable. The tests pin each field individually so a regression names the field it broke, and record why AllowNonCanonicalR stays unset: voi panics on AllowNonCanonicalR together with CofactorlessVerify, so setting it would crash on the first precompile instruction rather than loosen a check. Co-Authored-By: Claude Opus 5 --- pkg/sealevel/ed25519_program.go | 22 ++++++- pkg/sealevel/ed25519_program_test.go | 90 ++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 pkg/sealevel/ed25519_program_test.go diff --git a/pkg/sealevel/ed25519_program.go b/pkg/sealevel/ed25519_program.go index 3ee79890..ea7b4caf 100644 --- a/pkg/sealevel/ed25519_program.go +++ b/pkg/sealevel/ed25519_program.go @@ -17,6 +17,26 @@ const SignatureOffsetsSerializedSize = 14 const SignatureSerializedSize = 64 const PubkeySerializedSize = 32 +// ed25519PrecompileStrictVerifyOptions mirrors the reference precompile +// predicate once Ed25519PrecompileVerifyStrict is active: small-order A and R +// are rejected, but a non-canonical A encoding is accepted and its original +// bytes are what get hashed. +// +// AllowNonCanonicalA has to be set explicitly. A Go struct literal zeroes every +// field it omits, so leaving it out rejects non-canonical A -- stricter than the +// reference, and stricter than voi's own VerifyOptionsStdLib preset. Keeping the +// options in one named value rather than inline at the call site is what makes +// that omission testable; see TestEd25519PrecompileStrictVerifyOptions. +// +// AllowNonCanonicalR is deliberately unset: voi rejects the combination of +// AllowNonCanonicalR and CofactorlessVerify outright. +var ed25519PrecompileStrictVerifyOptions = ed25519.VerifyOptions{ + AllowSmallOrderA: false, + AllowSmallOrderR: false, + AllowNonCanonicalA: true, + CofactorlessVerify: true, +} + const Ed25519SignatureOffsetsSize = 14 type Ed25519SignatureOffsets struct { @@ -130,7 +150,7 @@ func Ed25519ProgramExecute(execCtx *ExecutionCtx) error { pk := ed25519.PublicKey(pubkey) if execCtx.Features.IsActive(features.Ed25519PrecompileVerifyStrict) { - verifyOptions := ed25519.VerifyOptions{AllowSmallOrderA: false, AllowSmallOrderR: false, CofactorlessVerify: true} + verifyOptions := ed25519PrecompileStrictVerifyOptions opts := ed25519.Options{Verify: &verifyOptions} if !ed25519.VerifyWithOptions(pk, msg[:offsets.MessageDataSize], signature[:64], &opts) { diff --git a/pkg/sealevel/ed25519_program_test.go b/pkg/sealevel/ed25519_program_test.go new file mode 100644 index 00000000..675fa81c --- /dev/null +++ b/pkg/sealevel/ed25519_program_test.go @@ -0,0 +1,90 @@ +package sealevel + +import ( + "testing" + + "github.com/oasisprotocol/curve25519-voi/primitives/ed25519" +) + +// The precompile predicate is expressed as a struct literal, and Go zeroes every +// field a literal omits. That makes "forgot a field" indistinguishable from +// "deliberately false" at the call site, and the two differ here: omitting +// AllowNonCanonicalA rejects public keys the reference accepts. +// +// This pins each field individually so a regression names the field it broke +// rather than failing on an opaque struct comparison. +func TestEd25519PrecompileStrictVerifyOptions(t *testing.T) { + opts := ed25519PrecompileStrictVerifyOptions + + for _, tc := range []struct { + field string + got bool + want bool + why string + }{ + { + field: "AllowSmallOrderA", + got: opts.AllowSmallOrderA, + want: false, + why: "strict verification rejects a small-order public key", + }, + { + field: "AllowSmallOrderR", + got: opts.AllowSmallOrderR, + want: false, + why: "strict verification rejects a small-order R", + }, + { + field: "AllowNonCanonicalA", + got: opts.AllowNonCanonicalA, + want: true, + why: "a non-canonical A is accepted and its original bytes are hashed; " + + "the zero value would reject it, which is stricter than the reference", + }, + { + field: "CofactorlessVerify", + got: opts.CofactorlessVerify, + want: true, + why: "the reference uses the cofactorless equation", + }, + } { + if tc.got != tc.want { + t.Errorf("%s = %v, want %v: %s", tc.field, tc.got, tc.want, tc.why) + } + } +} + +// voi does not return a verdict for AllowNonCanonicalR + CofactorlessVerify -- +// it panics. Setting that field would therefore crash the node on the first +// precompile instruction rather than merely loosening a check, so this pins both +// that the field stays unset and the reason it must. +func TestEd25519NonCanonicalRIsIncompatibleWithCofactorless(t *testing.T) { + if !ed25519PrecompileStrictVerifyOptions.CofactorlessVerify { + t.Fatal("precondition: options must use cofactorless verification") + } + if ed25519PrecompileStrictVerifyOptions.AllowNonCanonicalR { + t.Fatal("AllowNonCanonicalR must stay unset while CofactorlessVerify is set") + } + + panicked := func() (panicked bool) { + defer func() { panicked = recover() != nil }() + opts := ed25519.Options{ + Verify: &ed25519.VerifyOptions{ + AllowNonCanonicalR: true, + CofactorlessVerify: true, + }, + } + ed25519.VerifyWithOptions( + make([]byte, ed25519.PublicKeySize), + []byte("m"), + make([]byte, ed25519.SignatureSize), + &opts, + ) + return false + }() + + if !panicked { + t.Fatal("voi no longer panics on AllowNonCanonicalR + CofactorlessVerify; " + + "re-check whether that combination is now usable") + } +} From 52d08d92fca663957eb3f798b740e0205250ca9d Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Tue, 28 Jul 2026 01:26:50 -0500 Subject: [PATCH 13/23] sealevel: verify the ed25519 precompile through narya The precompile was the last verification site still on curve25519-voi, and the only one that did not honour the backend selection or the stdlib rollback switch that pkg/sigverify owns. Route the strict path through sigverify.VerifyOne so it uses the same narya DalekStrict predicate, the same backend, and the same kill switch as replay, TPU, gossip, repair and turbine. The non-strict branch, which runs only when replaying history from before Ed25519PrecompileVerifyStrict activated, now calls crypto/ed25519 directly. The reference used plain non-strict verification there -- cofactorless, no small-order rejection, non-canonical A accepted, R compared as bytes -- which is exactly what the standard library does. voi's default options were none of those things: they are cofactored and reject a non-canonical A, so that branch had been running a predicate the reference never used. narya exposes no StdlibCompat entry point, so the stdlib is both correct and the smallest dependency for that path. Signatures are still verified one at a time. The reference walks entries in order and returns the first error, so batching would let a later entry's offset error preempt an earlier entry's signature error, and that error code reaches the ledger. The ordering is consensus-visible and not worth trading for the throughput of a batch that is usually one or two signatures deep. This supersedes the AllowNonCanonicalA fix in the previous commit; the option struct it repaired no longer exists. The tests now guard the wiring rather than the struct literal, since the predicate itself is covered by narya's CCTV, Wycheproof and edge corpora. Verified against Agave v4.2.0-rc.0: precompiles is still pinned to ed25519-dalek "=1.0.1" and the workspace to 2.2, unchanged from beta.1. Co-Authored-By: Claude Opus 5 --- pkg/sealevel/ed25519_program.go | 50 +++++---- pkg/sealevel/ed25519_program_test.go | 146 +++++++++++++++------------ 2 files changed, 102 insertions(+), 94 deletions(-) diff --git a/pkg/sealevel/ed25519_program.go b/pkg/sealevel/ed25519_program.go index ea7b4caf..60c441f4 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) @@ -17,26 +19,6 @@ const SignatureOffsetsSerializedSize = 14 const SignatureSerializedSize = 64 const PubkeySerializedSize = 32 -// ed25519PrecompileStrictVerifyOptions mirrors the reference precompile -// predicate once Ed25519PrecompileVerifyStrict is active: small-order A and R -// are rejected, but a non-canonical A encoding is accepted and its original -// bytes are what get hashed. -// -// AllowNonCanonicalA has to be set explicitly. A Go struct literal zeroes every -// field it omits, so leaving it out rejects non-canonical A -- stricter than the -// reference, and stricter than voi's own VerifyOptionsStdLib preset. Keeping the -// options in one named value rather than inline at the call site is what makes -// that omission testable; see TestEd25519PrecompileStrictVerifyOptions. -// -// AllowNonCanonicalR is deliberately unset: voi rejects the combination of -// AllowNonCanonicalR and CofactorlessVerify outright. -var ed25519PrecompileStrictVerifyOptions = ed25519.VerifyOptions{ - AllowSmallOrderA: false, - AllowSmallOrderR: false, - AllowNonCanonicalA: true, - CofactorlessVerify: true, -} - const Ed25519SignatureOffsetsSize = 14 type Ed25519SignatureOffsets struct { @@ -147,17 +129,31 @@ 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 := ed25519PrecompileStrictVerifyOptions - 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. Routed through pkg/sigverify so + // the precompile honours the same backend selection and stdlib + // rollback switch as every other verification site. + 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 index 675fa81c..e18145bc 100644 --- a/pkg/sealevel/ed25519_program_test.go +++ b/pkg/sealevel/ed25519_program_test.go @@ -1,90 +1,102 @@ package sealevel import ( + stded25519 "crypto/ed25519" + + "bytes" "testing" - "github.com/oasisprotocol/curve25519-voi/primitives/ed25519" + "github.com/Overclock-Validator/mithril/pkg/sigverify" ) -// The precompile predicate is expressed as a struct literal, and Go zeroes every -// field a literal omits. That makes "forgot a field" indistinguishable from -// "deliberately false" at the call site, and the two differ here: omitting -// AllowNonCanonicalA rejects public keys the reference accepts. -// -// This pins each field individually so a regression names the field it broke -// rather than failing on an opaque struct comparison. -func TestEd25519PrecompileStrictVerifyOptions(t *testing.T) { - opts := ed25519PrecompileStrictVerifyOptions +// The predicate itself -- small-order rejection, non-canonical A acceptance, +// scalar canonicality -- is covered by narya's own CCTV, Wycheproof and edge +// corpora, so this file does not restate it. What it guards is the wiring: the +// strict precompile path must reach narya through pkg/sigverify, because that +// is what makes the backend selection and the stdlib rollback switch apply here +// as they do at every other verification site. An earlier revision of this file +// configured a second library inline and silently ran a stricter predicate. +func TestPrecompileStrictPathAcceptsAValidSignature(t *testing.T) { + pub, priv, err := stded25519.GenerateKey(nil) + if err != nil { + t.Fatalf("generate key: %v", err) + } + msg := []byte("ed25519 precompile wiring") + sig := stded25519.Sign(priv, msg) + + if len(pub) != PubkeySerializedSize { + t.Fatalf("public key is %d bytes, want %d", len(pub), PubkeySerializedSize) + } + if len(sig) != SignatureSerializedSize { + t.Fatalf("signature is %d bytes, want %d", len(sig), SignatureSerializedSize) + } + + if !sigverify.VerifyOne((*[32]byte)(pub), msg, sig) { + t.Fatal("a valid signature was rejected by the strict precompile path") + } +} + +func TestPrecompileStrictPathRejectsATamperedSignature(t *testing.T) { + pub, priv, err := stded25519.GenerateKey(nil) + if err != nil { + t.Fatalf("generate key: %v", err) + } + msg := []byte("ed25519 precompile wiring") + sig := stded25519.Sign(priv, msg) for _, tc := range []struct { - field string - got bool - want bool - why string + name string + mutry func() ([]byte, []byte, []byte) }{ { - field: "AllowSmallOrderA", - got: opts.AllowSmallOrderA, - want: false, - why: "strict verification rejects a small-order public key", - }, - { - field: "AllowSmallOrderR", - got: opts.AllowSmallOrderR, - want: false, - why: "strict verification rejects a small-order R", + name: "flipped signature bit", + mutry: func() ([]byte, []byte, []byte) { + bad := bytes.Clone(sig) + bad[0] ^= 0x01 + return pub, msg, bad + }, }, { - field: "AllowNonCanonicalA", - got: opts.AllowNonCanonicalA, - want: true, - why: "a non-canonical A is accepted and its original bytes are hashed; " + - "the zero value would reject it, which is stricter than the reference", + name: "different message", + mutry: func() ([]byte, []byte, []byte) { + return pub, []byte("a different message entirely"), sig + }, }, { - field: "CofactorlessVerify", - got: opts.CofactorlessVerify, - want: true, - why: "the reference uses the cofactorless equation", + name: "small-order public key", + mutry: func() ([]byte, []byte, []byte) { + // The order-4 point: y = 0, canonical spelling. Strict + // verification must reject it before evaluating the equation. + return make([]byte, PubkeySerializedSize), msg, sig + }, }, } { - if tc.got != tc.want { - t.Errorf("%s = %v, want %v: %s", tc.field, tc.got, tc.want, tc.why) - } + t.Run(tc.name, func(t *testing.T) { + p, m, s := tc.mutry() + if sigverify.VerifyOne((*[32]byte)(p), m, s) { + t.Error("expected rejection, got acceptance") + } + }) } } -// voi does not return a verdict for AllowNonCanonicalR + CofactorlessVerify -- -// it panics. Setting that field would therefore crash the node on the first -// precompile instruction rather than merely loosening a check, so this pins both -// that the field stays unset and the reason it must. -func TestEd25519NonCanonicalRIsIncompatibleWithCofactorless(t *testing.T) { - if !ed25519PrecompileStrictVerifyOptions.CofactorlessVerify { - t.Fatal("precondition: options must use cofactorless verification") - } - if ed25519PrecompileStrictVerifyOptions.AllowNonCanonicalR { - t.Fatal("AllowNonCanonicalR must stay unset while CofactorlessVerify is set") - } +// The non-strict branch runs only when Ed25519PrecompileVerifyStrict is +// inactive, i.e. when replaying history from before activation. The reference +// used plain non-strict verification there, which is exactly crypto/ed25519: +// cofactorless, no small-order rejection, R compared as bytes. This pins that +// the two branches genuinely differ, so a future refactor cannot collapse them. +func TestPrecompileNonStrictBranchAcceptsSmallOrderKeys(t *testing.T) { + smallOrder := make([]byte, PubkeySerializedSize) + sig := make([]byte, SignatureSerializedSize) + msg := []byte("m") - panicked := func() (panicked bool) { - defer func() { panicked = recover() != nil }() - opts := ed25519.Options{ - Verify: &ed25519.VerifyOptions{ - AllowNonCanonicalR: true, - CofactorlessVerify: true, - }, - } - ed25519.VerifyWithOptions( - make([]byte, ed25519.PublicKeySize), - []byte("m"), - make([]byte, ed25519.SignatureSize), - &opts, - ) - return false - }() - - if !panicked { - t.Fatal("voi no longer panics on AllowNonCanonicalR + CofactorlessVerify; " + - "re-check whether that combination is now usable") + // Both must reject this particular input, but for different reasons: strict + // rejects on the small-order gate, stdlib on the equation. The assertion + // that matters is that the strict path is not simply calling the stdlib. + if sigverify.VerifyOne((*[32]byte)(smallOrder), msg, sig) { + t.Error("strict path accepted a small-order key") + } + if stded25519.Verify(stded25519.PublicKey(smallOrder), msg, sig) { + t.Error("stdlib accepted a garbage signature; test premise is wrong") } } From 1ba4d678d4f08d709df8ef86001771769b2f6537 Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Tue, 28 Jul 2026 01:42:03 -0500 Subject: [PATCH 14/23] conformance: repair the Firedancer fixture harness The precompile conformance tests read test-vectors/precompile/fixtures/, a layout that no longer exists upstream. ReadDir failed, the assertion failed, and no fixture was ever executed. Firedancer now publishes every precompile fixture in one flat instr/fixtures/precompile directory and distinguishes them by the program id inside the fixture, so filtering has to parse rather than glob. A second and quieter problem sat underneath. The fixture schema gained a metadata message at field 1 and pushed input and output to fields 2 and 3, but the generated bindings here still say input is 1 and output is 2. Protobuf does not error on that: it reads the metadata submessage as an InstrContext and yields a context with no program id and no accounts. Every test built on those bindings would have compared nothing while reporting success. unmarshalInstrFixture splits the wrapper by hand and decodes the two submessages with the existing inner types, which have not drifted, and refuses a fixture with no metadata field rather than guessing at an older shape. parseAndConfigureFeatures dereferenced through Input.EpochContext unconditionally and panicked on the precompile corpus, which mostly carries no epoch context. It now treats a missing one as "defaults only". Its per-feature logging moved behind MITHRIL_CONFORMANCE_VERBOSE, which the vm-programs test already used; unguarded it emits a line per feature per fixture across thousands of fixtures. Results against firedancer-io/test-vectors: ed25519 3316 / 3479 secp256k1 2523 / 2628 secp256r1 13185 / 13185 secp256r1 is the largest fixture set in the corpus and had no test at all. The remaining ed25519 and secp256k1 failures share one cause, and it is not the predicate. InstrContext.epoch_context also moved, from field 9 to field 10, so the bindings read nil, parseAndConfigureFeatures sees no active features, and every feature-gated branch evaluates as though nothing had ever activated. For the ed25519 precompile that selects the pre-activation non-strict path, which accepts signatures strict rejects -- exactly the observed "we accepted, fixture expects an error". Field 10 is not universally an epoch context across the corpus, so recovering it by hand is a guess rather than a fix; regenerating the bindings from firedancer-io/protosol v5.3.0 is the correct repair and is left as follow-up work. Tests skip rather than fail when the corpus is absent. It is a ~7 GB external checkout that is deliberately gitignored, so 'make conformance-vectors' fetches or updates it and 'make test-conformance-precompiles' runs all three suites. Co-Authored-By: Claude Opus 5 --- Makefile | 14 ++- conformance/ed25519_precompile_test.go | 65 +------------- conformance/fixture_compat_test.go | 107 +++++++++++++++++++++++ conformance/precompile_common_test.go | 106 ++++++++++++++++++++++ conformance/secp256k1_precompile_test.go | 65 +------------- conformance/secp256r1_precompile_test.go | 10 +++ conformance/test_common.go | 14 ++- 7 files changed, 255 insertions(+), 126 deletions(-) create mode 100644 conformance/fixture_compat_test.go create mode 100644 conformance/precompile_common_test.go create mode 100644 conformance/secp256r1_precompile_test.go diff --git a/Makefile b/Makefile index 2a579c80..c40674cd 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,18 @@ disk-setup: tune: ./scripts/performance-tune.sh $(ARGS) +# Firedancer's fixture corpus is ~7 GB and gitignored, so it is fetched rather +# than vendored. Re-run to update; the conformance tests skip without it. +conformance-vectors: + @if [ -d conformance/test-vectors/.git ]; then \ + git -C conformance/test-vectors pull --ff-only; \ + else \ + git clone --depth 1 https://github.com/firedancer-io/test-vectors.git conformance/test-vectors; \ + fi + +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/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/fixture_compat_test.go b/conformance/fixture_compat_test.go new file mode 100644 index 00000000..588d7e45 --- /dev/null +++ b/conformance/fixture_compat_test.go @@ -0,0 +1,107 @@ +package conformance + +import ( + "encoding/binary" + "fmt" + + "google.golang.org/protobuf/proto" +) + +// Firedancer's fixture schema gained a metadata message at field 1 and pushed +// input and output to fields 2 and 3. The generated bindings in this package +// still describe the older shape, where input is field 1 and output is field 2, +// so proto.Unmarshal on a current fixture reads the metadata submessage as an +// InstrContext. Protobuf is permissive about that: it does not error, it +// produces a context with no program id and no accounts, and every test built +// on it silently compares nothing. +// +// Only the outer wrapper drifted. InstrContext and InstrEffects still match, so +// rather than regenerate the descriptors (which needs protoc and a protosol +// checkout) this splits the wrapper by hand and unmarshals the two submessages +// with the existing types. +// +// The proper fix is to regenerate from firedancer-io/protosol v5.3.0, the +// version solana-conformance pins. Until then this keeps the corpus usable and +// fails loudly on a shape it does not recognise instead of quietly passing. +const ( + fixtureFieldMetadata = 1 + fixtureFieldInput = 2 + fixtureFieldOutput = 3 +) + +// unmarshalInstrFixture decodes a fixture under the current upstream schema. +func unmarshalInstrFixture(raw []byte) (*InstrFixture, error) { + parts, err := splitTopLevelMessage(raw) + if err != nil { + return nil, err + } + + // A fixture written against the old schema has no metadata field. Refuse it + // rather than guessing, so a corpus mismatch cannot look like a pass. + if _, ok := parts[fixtureFieldMetadata]; !ok { + return nil, fmt.Errorf("fixture has no metadata field; corpus predates the schema this shim targets") + } + inputBytes, ok := parts[fixtureFieldInput] + if !ok { + return nil, fmt.Errorf("fixture has no input field") + } + + fixture := &InstrFixture{Input: &InstrContext{}, Output: &InstrEffects{}} + if err := proto.Unmarshal(inputBytes, fixture.Input); err != nil { + return nil, fmt.Errorf("decode input: %w", err) + } + + if outputBytes, ok := parts[fixtureFieldOutput]; ok { + if err := proto.Unmarshal(outputBytes, fixture.Output); err != nil { + return nil, fmt.Errorf("decode output: %w", err) + } + } + return fixture, nil +} + +// splitTopLevelMessage returns the raw bytes of each length-delimited field in +// a protobuf message, keyed by field number. Non-length-delimited fields are +// skipped: the fixture wrapper has none, and reading past one would desync the +// whole parse. +func splitTopLevelMessage(raw []byte) (map[int][]byte, error) { + parts := make(map[int][]byte) + for offset := 0; offset < len(raw); { + key, n := binary.Uvarint(raw[offset:]) + if n <= 0 { + return nil, fmt.Errorf("malformed field key at byte %d", offset) + } + offset += n + + field, wireType := int(key>>3), int(key&7) + switch wireType { + case 0: // varint + _, n := binary.Uvarint(raw[offset:]) + if n <= 0 { + return nil, fmt.Errorf("malformed varint for field %d", field) + } + offset += n + case 1: // 64-bit + offset += 8 + case 2: // length-delimited + length, n := binary.Uvarint(raw[offset:]) + if n <= 0 { + return nil, fmt.Errorf("malformed length for field %d", field) + } + offset += n + end := offset + int(length) + if end > len(raw) || end < offset { + return nil, fmt.Errorf("field %d length %d overruns the message", field, length) + } + parts[field] = raw[offset:end] + offset = end + case 5: // 32-bit + offset += 4 + default: + return nil, fmt.Errorf("unsupported wire type %d for field %d", wireType, field) + } + if offset > len(raw) { + return nil, fmt.Errorf("field %d overruns the message", field) + } + } + return parts, nil +} diff --git a/conformance/precompile_common_test.go b/conformance/precompile_common_test.go new file mode 100644 index 00000000..5e72b932 --- /dev/null +++ b/conformance/precompile_common_test.go @@ -0,0 +1,106 @@ +package conformance + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/gagliardetto/solana-go" +) + +// 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, err := unmarshalInstrFixture(raw) + if 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, err := unmarshalInstrFixture(raw) + if 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/test_common.go b/conformance/test_common.go index d082ace7..70ea340e 100644 --- a/conformance/test_common.go +++ b/conformance/test_common.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/binary" "fmt" + "os" "testing" "github.com/Overclock-Validator/mithril/pkg/accounts" @@ -211,11 +212,22 @@ func parseAndConfigureFeatures(execCtx *sealevel.ExecutionCtx, fixture *InstrFix f := features.NewFeaturesDefault() execCtx.Features = *f + // 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.EpochContext == nil || + fixture.Input.EpochContext.Features == nil { + return + } + + verbose := os.Getenv("MITHRIL_CONFORMANCE_VERBOSE") != "" for _, ftr := range fixture.Input.EpochContext.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) } } From 3885ce33d2dbd105e717b8841244cd1c724e9add Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Tue, 28 Jul 2026 09:32:57 -0500 Subject: [PATCH 15/23] conformance: regenerate protobuf bindings from protosol v5.4.0 The generated bindings described a schema several versions old. Three changes mattered: InstrFixture gained metadata at field 1, pushing input to 2 and output to 3 InstrContext dropped epoch_context and slot_context; the FeatureSet now sits directly at field 10 AcctState dropped rent_epoch Protobuf does not complain about any of that. It read the metadata submessage as an InstrContext and produced a context whose program id was the ASCII of "sol_compat_instr_execute_v1" and whose account list was empty, so the precompile suites compared nothing. The previous commit worked around the outer drift with a hand-rolled wrapper splitter; that shim is deleted here, since correct descriptors make it unnecessary. The field-10 FeatureSet is what the remaining failures were about. Reading the old field number yielded nil, parseAndConfigureFeatures saw no active features, and every feature-gated branch evaluated as though nothing had ever activated. For the Ed25519 precompile that selected the pre-activation non-strict path, which accepts signatures strict rejects. ed25519 3316 / 3479 -> 3479 / 3479 secp256k1 2523 / 2628 -> 2628 / 2628 secp256r1 13185 / 13185 -> 13185 / 13185 So none of those 268 disagreements were predicate differences. vm-programs is unchanged at 144/3398, verified by running the suite either side of this commit. It already carried its own compatibility path for the feature set, which is why it alone kept working; that path now goes through the regenerated types like everything else. Generated with protoc v3.21.12 and protoc-gen-go v1.34.2, matching the versions recorded in the previous files so the diff shows schema movement rather than codegen churn. elf.pb.go is untouched: protosol no longer ships elf.proto. Co-Authored-By: Claude Opus 5 --- conformance/bpf_loader_program_test.go | 4 +- conformance/context.pb.go | 347 +++++++----- conformance/firedancer_fixture_test.go | 12 +- conformance/fixture_compat_test.go | 107 ---- conformance/invoke.pb.go | 122 +++-- conformance/metadata.pb.go | 144 +++++ conformance/precompile_common_test.go | 9 +- conformance/serialize.pb.go | 5 +- conformance/test_common.go | 11 +- conformance/txn.pb.go | 725 +++++++++++-------------- conformance/vm.pb.go | 629 +++++++++++++++------ conformance/vm_programs_test.go | 10 +- 12 files changed, 1206 insertions(+), 919 deletions(-) delete mode 100644 conformance/fixture_compat_test.go create mode 100644 conformance/metadata.pb.go 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/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/fixture_compat_test.go b/conformance/fixture_compat_test.go deleted file mode 100644 index 588d7e45..00000000 --- a/conformance/fixture_compat_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package conformance - -import ( - "encoding/binary" - "fmt" - - "google.golang.org/protobuf/proto" -) - -// Firedancer's fixture schema gained a metadata message at field 1 and pushed -// input and output to fields 2 and 3. The generated bindings in this package -// still describe the older shape, where input is field 1 and output is field 2, -// so proto.Unmarshal on a current fixture reads the metadata submessage as an -// InstrContext. Protobuf is permissive about that: it does not error, it -// produces a context with no program id and no accounts, and every test built -// on it silently compares nothing. -// -// Only the outer wrapper drifted. InstrContext and InstrEffects still match, so -// rather than regenerate the descriptors (which needs protoc and a protosol -// checkout) this splits the wrapper by hand and unmarshals the two submessages -// with the existing types. -// -// The proper fix is to regenerate from firedancer-io/protosol v5.3.0, the -// version solana-conformance pins. Until then this keeps the corpus usable and -// fails loudly on a shape it does not recognise instead of quietly passing. -const ( - fixtureFieldMetadata = 1 - fixtureFieldInput = 2 - fixtureFieldOutput = 3 -) - -// unmarshalInstrFixture decodes a fixture under the current upstream schema. -func unmarshalInstrFixture(raw []byte) (*InstrFixture, error) { - parts, err := splitTopLevelMessage(raw) - if err != nil { - return nil, err - } - - // A fixture written against the old schema has no metadata field. Refuse it - // rather than guessing, so a corpus mismatch cannot look like a pass. - if _, ok := parts[fixtureFieldMetadata]; !ok { - return nil, fmt.Errorf("fixture has no metadata field; corpus predates the schema this shim targets") - } - inputBytes, ok := parts[fixtureFieldInput] - if !ok { - return nil, fmt.Errorf("fixture has no input field") - } - - fixture := &InstrFixture{Input: &InstrContext{}, Output: &InstrEffects{}} - if err := proto.Unmarshal(inputBytes, fixture.Input); err != nil { - return nil, fmt.Errorf("decode input: %w", err) - } - - if outputBytes, ok := parts[fixtureFieldOutput]; ok { - if err := proto.Unmarshal(outputBytes, fixture.Output); err != nil { - return nil, fmt.Errorf("decode output: %w", err) - } - } - return fixture, nil -} - -// splitTopLevelMessage returns the raw bytes of each length-delimited field in -// a protobuf message, keyed by field number. Non-length-delimited fields are -// skipped: the fixture wrapper has none, and reading past one would desync the -// whole parse. -func splitTopLevelMessage(raw []byte) (map[int][]byte, error) { - parts := make(map[int][]byte) - for offset := 0; offset < len(raw); { - key, n := binary.Uvarint(raw[offset:]) - if n <= 0 { - return nil, fmt.Errorf("malformed field key at byte %d", offset) - } - offset += n - - field, wireType := int(key>>3), int(key&7) - switch wireType { - case 0: // varint - _, n := binary.Uvarint(raw[offset:]) - if n <= 0 { - return nil, fmt.Errorf("malformed varint for field %d", field) - } - offset += n - case 1: // 64-bit - offset += 8 - case 2: // length-delimited - length, n := binary.Uvarint(raw[offset:]) - if n <= 0 { - return nil, fmt.Errorf("malformed length for field %d", field) - } - offset += n - end := offset + int(length) - if end > len(raw) || end < offset { - return nil, fmt.Errorf("field %d length %d overruns the message", field, length) - } - parts[field] = raw[offset:end] - offset = end - case 5: // 32-bit - offset += 4 - default: - return nil, fmt.Errorf("unsupported wire type %d for field %d", wireType, field) - } - if offset > len(raw) { - return nil, fmt.Errorf("field %d overruns the message", field) - } - } - return parts, nil -} 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 index 5e72b932..3132d1e6 100644 --- a/conformance/precompile_common_test.go +++ b/conformance/precompile_common_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/gagliardetto/solana-go" + "google.golang.org/protobuf/proto" ) // Firedancer publishes every precompile fixture in one flat directory and @@ -47,8 +48,8 @@ func loadPrecompileFixtures(t *testing.T, program solana.PublicKey) []string { if err != nil { t.Fatalf("read fixture %s: %v", path, err) } - fixture, err := unmarshalInstrFixture(raw) - if err != nil { + 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[:]) { @@ -77,8 +78,8 @@ func runPrecompileFixtures(t *testing.T, label string, paths []string) { if err != nil { t.Fatalf("read fixture %s: %v", path, err) } - fixture, err := unmarshalInstrFixture(raw) - if err != nil { + fixture := &InstrFixture{} + if err := proto.Unmarshal(raw, fixture); err != nil { t.Fatalf("decode fixture %s: %v", path, err) } 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 70ea340e..6237ff76 100644 --- a/conformance/test_common.go +++ b/conformance/test_common.go @@ -23,7 +23,6 @@ func fixtureAcctStateToAccount(acctState *AcctState) accounts.Account { acct.Lamports = acctState.Lamports acct.Data = acctState.Data acct.Executable = acctState.Executable - acct.RentEpoch = acctState.RentEpoch copy(acct.Owner[:], acctState.Owner) return acct } @@ -215,13 +214,12 @@ func parseAndConfigureFeatures(execCtx *sealevel.ExecutionCtx, fixture *InstrFix // 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.EpochContext == nil || - fixture.Input.EpochContext.Features == nil { + if fixture.Input == nil || fixture.Input.Features == nil { return } verbose := os.Getenv("MITHRIL_CONFORMANCE_VERBOSE") != "" - for _, ftr := range fixture.Input.EpochContext.Features.Features { + for _, ftr := range fixture.Input.Features.Features { for _, featureGate := range features.AllFeatureGates { featureIdInt := binary.LittleEndian.Uint64(featureGate.Address[:8]) if featureIdInt == ftr { @@ -335,9 +333,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(), From 89ce38871ad1ddc4324bebab799de4836a2d1b02 Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:00:55 -0500 Subject: [PATCH 16/23] sigverify: stop letting the backend flag change the predicate backend=stdlib bypassed the library and called crypto/ed25519 directly. That is non-strict: it accepts small-order A and small-order R, which the strict predicate rejects and mainnet rejects. So an operator flag decided which signatures the node accepts. Two nodes on different settings would disagree on block validity, and it is the flag reached for under exactly the pressure that makes a silent fork worst. The package comment already said the predicate "is not optional and not configurable", and Configure sets DalekStrict before selecting anything. The bypass contradicted both. The library has its own crypto/ed25519-backed backend, and its rejection pre-pass runs before dispatch regardless of which backend is active. Selecting that instead keeps the diagnostic value -- it swaps out the r51 assembly, the comb tables and the batch kernels, which is where an implementation bug would realistically live -- while leaving acceptance untouched. The bypass flag is gone entirely; every verification now goes through the library. The test that pinned the divergence as deliberate is inverted to pin its absence. It has to re-execute itself in a child process: backend selection is one-shot per process by design, so the library can never hold key tables in two formats, which means any earlier Configure would make this test skip. A test that skips during "go test ./..." guards nothing. Co-Authored-By: Claude Opus 5 --- pkg/sigverify/sigverify.go | 49 +++++++++---------------------- pkg/sigverify/sigverify_test.go | 51 ++++++++++++++++++++++++++------- 2 files changed, 54 insertions(+), 46 deletions(-) diff --git a/pkg/sigverify/sigverify.go b/pkg/sigverify/sigverify.go index f1addf86..82eba096 100644 --- a/pkg/sigverify/sigverify.go +++ b/pkg/sigverify/sigverify.go @@ -19,9 +19,7 @@ package sigverify import ( - stded25519 "crypto/ed25519" "fmt" - "sync/atomic" narya "github.com/Overclock-Validator/narya-ed25519/ed25519" ) @@ -36,12 +34,18 @@ const ( BackendR51 = "r51" // BackendGeneric forces the portable pure-Go backend. BackendGeneric = "generic" - // BackendStdlib bypasses the library entirely and calls crypto/ed25519 - // directly. This is the rollback switch: it restores the exact behaviour - // Mithril had before this package existed, INCLUDING the non-strict - // predicate, so it reintroduces the small-order divergence described above. - // It exists so an operator can eliminate this package as a suspect without - // rebuilding, not as a supported steady state. + // BackendStdlib selects the library's own crypto/ed25519-backed arithmetic. + // It swaps out the r51 assembly, the comb tables and the batch kernels -- + // where an implementation bug would realistically live -- while leaving the + // acceptance rule untouched, so an operator can rule those out without + // rebuilding. + // + // This deliberately does NOT bypass the library. An earlier revision routed + // this name straight at crypto/ed25519, which silently dropped the + // small-order rejection and made an operator flag change which signatures + // the node accepts. Two nodes on different settings would disagree on block + // validity, and the flag would be reached for under exactly the pressure + // that makes a silent fork worst. The predicate is not an operator knob. BackendStdlib = "stdlib" ) @@ -59,11 +63,6 @@ func Defaults() Config { return Config{Backend: BackendAuto} } // read-only afterwards. It follows the same shape as replay.TrailingVerifierCfg. var Cfg = Defaults() -// bypass is read on every verification, so it is an atomic rather than a plain -// bool: Configure runs during startup but the verifiers run on pool goroutines, -// and the race detector is correctly unhappy about an unsynchronised handoff. -var bypass atomic.Bool - // 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 @@ -85,10 +84,6 @@ func Configure(cfg Config) (string, error) { narya.SetDefaultProfile(narya.DalekStrict) switch cfg.Backend { - case BackendStdlib: - bypass.Store(true) - return BackendStdlib, nil - 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 @@ -101,7 +96,7 @@ func Configure(cfg Config) (string, error) { } return narya.ActiveBackend(), nil - case BackendR51, BackendGeneric: + case BackendR51, BackendGeneric, BackendStdlib: if err := narya.SetBackend(cfg.Backend); err != nil { return "", fmt.Errorf("sigverify: select backend %q: %w", cfg.Backend, err) } @@ -116,9 +111,6 @@ func Configure(cfg Config) (string, error) { // Backend reports the backend in use, for metrics and diagnostics. func Backend() string { - if bypass.Load() { - return BackendStdlib - } return narya.ActiveBackend() } @@ -127,9 +119,6 @@ func Backend() string { // 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 { - if bypass.Load() { - return 0 - } return narya.ActiveBackendStats().InternalFaultFallbacks } @@ -142,9 +131,6 @@ func VerifyOne(pub *[32]byte, msg, sig []byte) bool { if pub == nil { return false } - if bypass.Load() { - return stded25519.Verify(pub[:], msg, sig) - } return narya.VerifyStrict(pub[:], msg, sig) } @@ -193,15 +179,6 @@ func (b *Batch) Verify() bool { if len(b.pubs) == 0 { return true } - if bypass.Load() { - all := true - for i, pub := range b.pubs { - verdict := pub != nil && stded25519.Verify(pub[:], b.msgs[i], b.sigs[i]) - b.ok[i] = verdict - all = all && verdict - } - return all - } return narya.VerifyBatchStrict(b.pubs, b.msgs, b.sigs, b.ok) } diff --git a/pkg/sigverify/sigverify_test.go b/pkg/sigverify/sigverify_test.go index 867c2180..bf52a340 100644 --- a/pkg/sigverify/sigverify_test.go +++ b/pkg/sigverify/sigverify_test.go @@ -4,6 +4,8 @@ import ( stded25519 "crypto/ed25519" "crypto/rand" "fmt" + "os" + "os/exec" "testing" "filippo.io/edwards25519" @@ -59,19 +61,48 @@ func TestSmallOrderForgeryIsAcceptedByStdlibAndRejectedHere(t *testing.T) { assert.False(t, batch.OK(0)) } -// The bypass switch is a rollback to the pre-existing behaviour, divergence -// included. Pinning that here keeps it an informed choice rather than a -// surprise, and fails loudly if someone later "fixes" the bypass into -// something that is no longer a faithful rollback. -func TestStdlibBypassReintroducesTheDivergence(t *testing.T) { - bypass.Store(true) - t.Cleanup(func() { bypass.Store(false) }) - +// 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) - assert.True(t, VerifyOne(&pub, message, sig), - "backend=stdlib is a rollback: it accepts what stdlib accepts, small-order included") + // 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 { From 6e1816021ac456d3ec3a331428ee828f76fc29ea Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:32:47 -0500 Subject: [PATCH 17/23] sealevel: end-to-end tests for the ed25519 precompile Drives ProcessInstruction rather than the verifier underneath it, so offsets parsing, program-id routing and the predicate are covered together. Three cases that discriminate: a valid signature accepted, small-order A and small-order R rejected, and a tampered-but-well-formed signature failing on the equation rather than on a byte-level gate. The Firedancer corpus covers this path far more broadly at 3479 fixtures, but it needs a ~7 GB external checkout and skips without it. These run in an ordinary "go test ./..." and cost microseconds. Deliberately absent: a non-canonical-A case. verify_strict accepts a non-canonical A and hashes its original bytes, so pinning that looks like the obvious fourth case, but it cannot be built. 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 the small-order gate rejects it before canonicality is consulted. All 152 non-canonical-A fixtures in the Firedancer corpus expect an error for that reason, which also means they do not discriminate: they pass whether or not an implementation handles non-canonical A correctly. A test for that bullet would read as coverage and prove nothing, so the reasoning is recorded in a comment instead. Test files never enter the binary, so none of this costs the node anything. Co-Authored-By: Claude Opus 5 --- pkg/sealevel/ed25519_program_test.go | 210 ++++++++++++++++----------- 1 file changed, 126 insertions(+), 84 deletions(-) diff --git a/pkg/sealevel/ed25519_program_test.go b/pkg/sealevel/ed25519_program_test.go index e18145bc..d918b0e2 100644 --- a/pkg/sealevel/ed25519_program_test.go +++ b/pkg/sealevel/ed25519_program_test.go @@ -3,100 +3,142 @@ package sealevel import ( stded25519 "crypto/ed25519" - "bytes" + "crypto/rand" + "encoding/binary" "testing" - "github.com/Overclock-Validator/mithril/pkg/sigverify" + "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" ) -// The predicate itself -- small-order rejection, non-canonical A acceptance, -// scalar canonicality -- is covered by narya's own CCTV, Wycheproof and edge -// corpora, so this file does not restate it. What it guards is the wiring: the -// strict precompile path must reach narya through pkg/sigverify, because that -// is what makes the backend selection and the stdlib rollback switch apply here -// as they do at every other verification site. An earlier revision of this file -// configured a second library inline and silently ran a stricter predicate. -func TestPrecompileStrictPathAcceptsAValidSignature(t *testing.T) { - pub, priv, err := stded25519.GenerateKey(nil) - if err != nil { - t.Fatalf("generate key: %v", err) - } - msg := []byte("ed25519 precompile wiring") - sig := stded25519.Sign(priv, msg) +// 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. - if len(pub) != PubkeySerializedSize { - t.Fatalf("public key is %d bytes, want %d", len(pub), PubkeySerializedSize) - } - if len(sig) != SignatureSerializedSize { - t.Fatalf("signature is %d bytes, want %d", len(sig), SignatureSerializedSize) - } +// 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 - if !sigverify.VerifyOne((*[32]byte)(pub), msg, sig) { - t.Fatal("a valid signature was rejected by the strict precompile path") - } + 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...) } -func TestPrecompileStrictPathRejectsATamperedSignature(t *testing.T) { - pub, priv, err := stded25519.GenerateKey(nil) - if err != nil { - t.Fatalf("generate key: %v", err) +// 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() + + programAcct := accounts.Account{ + Key: solana.PublicKeyFromBytes(a.Ed25519PrecompileAddr[:]), + Lamports: 1, + Data: []byte{}, + Owner: a.NativeLoaderAddr, + Executable: true, } - msg := []byte("ed25519 precompile wiring") - sig := stded25519.Sign(priv, msg) - - for _, tc := range []struct { - name string - mutry func() ([]byte, []byte, []byte) - }{ - { - name: "flipped signature bit", - mutry: func() ([]byte, []byte, []byte) { - bad := bytes.Clone(sig) - bad[0] ^= 0x01 - return pub, msg, bad - }, - }, - { - name: "different message", - mutry: func() ([]byte, []byte, []byte) { - return pub, []byte("a different message entirely"), sig - }, - }, - { - name: "small-order public key", - mutry: func() ([]byte, []byte, []byte) { - // The order-4 point: y = 0, canonical spelling. Strict - // verification must reject it before evaluating the equation. - return make([]byte, PubkeySerializedSize), msg, sig - }, - }, - } { - t.Run(tc.name, func(t *testing.T) { - p, m, s := tc.mutry() - if sigverify.VerifyOne((*[32]byte)(p), m, s) { - t.Error("expected rejection, got acceptance") - } - }) + 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() + execCtx.Features.EnableFeature(features.Ed25519PrecompileVerifyStrict, 0) + + return execCtx.ProcessInstruction(data, []InstructionAccount{}, []uint64{0}) } -// The non-strict branch runs only when Ed25519PrecompileVerifyStrict is -// inactive, i.e. when replaying history from before activation. The reference -// used plain non-strict verification there, which is exactly crypto/ed25519: -// cofactorless, no small-order rejection, R compared as bytes. This pins that -// the two branches genuinely differ, so a future refactor cannot collapse them. -func TestPrecompileNonStrictBranchAcceptsSmallOrderKeys(t *testing.T) { - smallOrder := make([]byte, PubkeySerializedSize) - sig := make([]byte, SignatureSerializedSize) - msg := []byte("m") - - // Both must reject this particular input, but for different reasons: strict - // rejects on the small-order gate, stdlib on the equation. The assertion - // that matters is that the strict path is not simply calling the stdlib. - if sigverify.VerifyOne((*[32]byte)(smallOrder), msg, sig) { - t.Error("strict path accepted a small-order key") - } - if stded25519.Verify(stded25519.PublicKey(smallOrder), msg, sig) { - t.Error("stdlib accepted a garbage signature; test premise is wrong") - } +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") } From 5d8b629d4ef1c992a49d29083afed2ce59ef101b Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Tue, 28 Jul 2026 11:12:59 -0500 Subject: [PATCH 18/23] sealevel: pin both sides of the ed25519 precompile feature gate The non-strict branch stays, so historical replay remains possible: blocks from before Ed25519PrecompileVerifyStrict activated were validated without it, and re-verifying them strictly would reject transactions the network accepted and produce a different bank hash. Keeping an untested branch is what made it dangerous, not the branch itself. One input, both feature states, opposite verdicts. The signature is a small-order construction that genuinely satisfies the stdlib equation -- A is the identity, so [s]B - [k]A collapses to [s]B, which is the R it carries. It is rejected only because strict verification refuses a small-order public key, which makes it the cleanest probe for which predicate ran. This also guards the failure mode that actually occurred. 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 silently became an acceptance change. With both directions pinned, that surfaces as a test failure rather than a quiet divergence. Co-Authored-By: Claude Opus 5 --- pkg/sealevel/ed25519_program_test.go | 68 +++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/pkg/sealevel/ed25519_program_test.go b/pkg/sealevel/ed25519_program_test.go index d918b0e2..d5bc6dae 100644 --- a/pkg/sealevel/ed25519_program_test.go +++ b/pkg/sealevel/ed25519_program_test.go @@ -7,6 +7,7 @@ import ( "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" @@ -62,6 +63,15 @@ func buildEd25519Instruction(pubkey, signature, message []byte) []byte { // 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[:]), @@ -81,7 +91,9 @@ func runEd25519Precompile(t *testing.T, data []byte) error { } execCtx.Accounts = accounts.NewMemAccounts() execCtx.Features = *features.NewFeaturesDefault() - execCtx.Features.EnableFeature(features.Ed25519PrecompileVerifyStrict, 0) + if strict { + execCtx.Features.EnableFeature(features.Ed25519PrecompileVerifyStrict, 0) + } return execCtx.ProcessInstruction(data, []InstructionAccount{}, []uint64{0}) } @@ -142,3 +154,57 @@ func TestEd25519PrecompileRejectsATamperedSignature(t *testing.T) { 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") + }) +} From 98361035a2c53e9b674915d7888cd8359bb56d4c Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Tue, 28 Jul 2026 11:26:38 -0500 Subject: [PATCH 19/23] go.mod: update narya-ed25519 to the reviewed main Moves from 1625c183 to da0d045, about a hundred commits. The pin was an ancestor of main, so this is a fast-forward rather than a branch change. The reason to do it now is one commit in that range: 5e7ead9, "r51x5: stop dereferencing a skipped lane's nil table". A malformed lane in a warm group dereferenced a nil per-key table, which is reachable from network input. The pin predates that fix. Also picked up: 53868d9, which makes a nil Cache receiver fail closed uniformly instead of depending on the input, and 41720a2, which gates r51 availability on the kernels actually compiled in rather than on CPU features alone. The rest is the sigprep extraction, the fixed-base and x8 Niels work, and documentation. 118 files, none of which change the acceptance predicate. Verified after the bump: pkg/sigverify and the ed25519 precompile tests pass, and the Firedancer conformance suites are unchanged at ed25519 3479/3479, secp256k1 2628/2628, secp256r1 13185/13185. One pre-existing failure is untouched by this and should not be read as fallout from it: TestExecute_AddrLookupTable_Program_Test_Create_Lookup_Table_Not_Idempotent fails identically on the old pin, verified by reverting go.mod and rerunning it. It is in the address lookup table program and has no path to ed25519. Co-Authored-By: Claude Opus 5 --- go.mod | 4 ++-- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 27122d67..95a8dee2 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +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-20260726065311-1625c1837692 + 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 @@ -146,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 392f65d9..f1c4e7e5 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +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-20260726065311-1625c1837692 h1:trNDlVdZDY84KNUP7ioyPMuRVBfgo7ghELqX8QUUW00= -github.com/Overclock-Validator/narya-ed25519 v0.0.0-20260726065311-1625c1837692/go.mod h1:B7/xqV/5NtGJa8OlZAa9TRMHgeIE+VEJNiPzMP4FrIg= +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= From 935350577699dea74810dd083eeb4f5ac3d06461 Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Tue, 28 Jul 2026 11:34:20 -0500 Subject: [PATCH 20/23] sealevel: dispatch the address lookup table, config and stake programs resolveNativeProgramById had no case for AddressLookupTableAddr, so AddressLookupTableExecute was unreachable: every instruction for that program returned InstrErrUnsupportedProgramId and all 31 address-lookup unit tests failed. The implementation was complete and exported the whole time, which is why nothing caught it at build time. Adding the case makes those 31 pass. Config and Stake were missing the same way. Both have been migrated to BPF on mainnet, so their accounts are loader-owned and route through the loader; this resolver is only consulted when the program account's owner is NativeLoader, which is the pre-migration shape. Wiring them therefore changes nothing about current execution and restores historical replay. A resolver test now pins every native program to its case. This class of bug is silent by construction -- an implementation with no case still compiles, still exports, and simply never runs -- so the mapping needs an explicit assertion rather than relying on each program's own tests to notice. Two fixes from the same review: Conformance accounts get RentEpoch math.MaxUint64. protosol v5.4.0 removed rent_epoch from AcctState, and I had dropped the field with it, but a converted account's implied value is the maximum rather than zero. Leaving the Go zero modelled every account as rent-paying. The corpus revision is pinned at a87fc430 instead of tracking main. Both the schema and the per-suite counts move with the corpus, so following main makes a passing run unreproducible and makes a regression indistinguishable from an upstream edit. That revision is the one every count in these commits was measured against. Precompile suites unchanged: ed25519 3479/3479, secp256k1 2628/2628, secp256r1 13185/13185. Co-Authored-By: Claude Opus 5 --- Makefile | 16 ++++-- conformance/test_common.go | 9 +++- pkg/sealevel/native_programs_common.go | 11 ++++ pkg/sealevel/native_programs_common_test.go | 56 +++++++++++++++++++++ 4 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 pkg/sealevel/native_programs_common_test.go diff --git a/Makefile b/Makefile index c40674cd..164155b7 100644 --- a/Makefile +++ b/Makefile @@ -30,13 +30,19 @@ tune: ./scripts/performance-tune.sh $(ARGS) # Firedancer's fixture corpus is ~7 GB and gitignored, so it is fetched rather -# than vendored. Re-run to update; the conformance tests skip without it. +# 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 -C conformance/test-vectors pull --ff-only; \ - else \ - git clone --depth 1 https://github.com/firedancer-io/test-vectors.git conformance/test-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 diff --git a/conformance/test_common.go b/conformance/test_common.go index 6237ff76..c3978fce 100644 --- a/conformance/test_common.go +++ b/conformance/test_common.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/binary" "fmt" + "math" "os" "testing" @@ -19,6 +20,9 @@ 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 @@ -29,7 +33,10 @@ func fixtureAcctStateToAccount(acctState *AcctState) accounts.Account { 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 } 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) +} From 37c668b7006e34b8b6e30669109e90c1747bfd52 Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Tue, 28 Jul 2026 11:39:00 -0500 Subject: [PATCH 21/23] sigverify: make Configure the one-time operation it already claimed to be The doc comment said "Calling it twice returns an error rather than silently ignoring the second call". Nothing enforced that. A second call re-ran the whole path, and for BackendAuto it would return successfully while the library had already pinned a different backend, so the caller was told it got something it did not get. Three changes: Repeat calls are refused, with the already-resolved backend named in the error. Refusing a repeat of the *same* backend too is deliberate: treating it as a harmless no-op would make "Configure ran twice" invisible, and the second caller still has no way to learn its configuration was discarded. Validation now happens before anything is published. Cfg was assigned from the argument before the backend name was checked, so a rejected name still became visible to Backend() and to the startup log. Backend installation moved into a helper, leaving Configure as validate, install, publish. The r51 case grew a comment recording that the absence of a fallback is the contract, not an oversight. The tests run each case in a child process. Backend selection is one-shot by design, so an in-process table would let the first Configure win and turn every later case into a vacuous pass. That risk is not hypothetical: an earlier version of the stdlib test skipped whenever another test had configured first, which guarded nothing during "go test ./...". TestConfigureChildPlumbingActuallyRuns guards the harness itself. If the child marker or the -test.run pattern stopped matching, every subprocess test would spawn a child that ran nothing, exit zero, and report a pass. The r51 assertion holds on both kinds of machine: it accepts a resolution to r51 or a clear error, and rejects only the outcome that would be a bug, which is success while a different backend is actually active. Passes under -race with -count=3. Co-Authored-By: Claude Opus 5 --- pkg/sigverify/configure_test.go | 175 ++++++++++++++++++++++++++++++++ pkg/sigverify/sigverify.go | 55 ++++++++-- 2 files changed, 222 insertions(+), 8 deletions(-) create mode 100644 pkg/sigverify/configure_test.go 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/sigverify.go b/pkg/sigverify/sigverify.go index 82eba096..6ef0af43 100644 --- a/pkg/sigverify/sigverify.go +++ b/pkg/sigverify/sigverify.go @@ -20,6 +20,7 @@ package sigverify import ( "fmt" + "sync" narya "github.com/Overclock-Validator/narya-ed25519/ed25519" ) @@ -76,14 +77,51 @@ 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 cfg.Backend { + 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 @@ -97,16 +135,17 @@ func Configure(cfg Config) (string, error) { return narya.ActiveBackend(), nil case BackendR51, BackendGeneric, BackendStdlib: - if err := narya.SetBackend(cfg.Backend); err != nil { - return "", fmt.Errorf("sigverify: select backend %q: %w", cfg.Backend, err) + // 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 - - default: - return "", fmt.Errorf( - "sigverify.backend must be one of %q, %q, %q, %q; got %q", - BackendAuto, BackendR51, BackendGeneric, BackendStdlib, cfg.Backend) } + + // 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. From 832cccc44171a62016ea85c6a6f357707eb21b94 Mon Sep 17 00:00:00 2001 From: 7layermagik <7layermagik@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:50:49 -0500 Subject: [PATCH 22/23] sigverify: record backend, faults and batch-width distribution per run Three things were unobservable at runtime: which backend actually resolved, whether the accelerated one ever fell back on an internal fault, and how wide the batches reaching it were. The third is the one that matters and the one a counter cannot answer. Cost per signature is a strong function of batch width -- 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 million signatures arriving one at a time and the same million arriving in eights produce identical totals. Only the distribution separates them, so this is a histogram, with per-width buckets up to 8 where the group boundary sits and coarse buckets above it. Recorded in Batch.Verify rather than at each drain site, so a new caller cannot forget to instrument itself. It is an atomic add into a fixed array: no allocation, no lock, nothing that can fail on the verification path. pkg/sigverify keeps zero Mithril dependencies. It exposes Stats(); the reporter lives in cmd/mithril/node and owns the logging. Output goes to /sigverify.log via mlog.NamedFilef and nowhere else. Terminal output is unchanged -- the only edit to node.go is the two-line call that starts the reporter. Operator stderr already carries replay progress, and batch width is diagnostic rather than something to watch live. Lines are startup, per-interval and shutdown. Intervals report deltas, because counters are monotonic for the process lifetime and a cumulative-only view lets a long-healthy run hide a recent collapse in batch width. The shutdown line means a short run still leaves a record, which matters most when the run ended because of a verification problem. One exception to the file-only rule: a rise in InternalFaultFallbacks also warns to the operator log. It means the accelerated backend produced a result it could not trust and recomputed on the portable path. That should never happen, and it is a backend bug rather than an input condition, so silence is the wrong default. diffWidths matches buckets by upper bound rather than position, because Stats omits empty buckets and two snapshots need not share a shape. Getting that wrong would be quiet: the reporter would keep emitting plausible lines while misattributing counts across boundaries. Passes under -race. Co-Authored-By: Claude Opus 5 --- cmd/mithril/node/node.go | 2 + cmd/mithril/node/sigverify_reporter.go | 104 +++++++++++++ cmd/mithril/node/sigverify_reporter_test.go | 45 ++++++ pkg/sigverify/sigverify.go | 4 + pkg/sigverify/stats.go | 159 ++++++++++++++++++++ pkg/sigverify/stats_test.go | 108 +++++++++++++ 6 files changed, 422 insertions(+) create mode 100644 cmd/mithril/node/sigverify_reporter.go create mode 100644 cmd/mithril/node/sigverify_reporter_test.go create mode 100644 pkg/sigverify/stats.go create mode 100644 pkg/sigverify/stats_test.go diff --git a/cmd/mithril/node/node.go b/cmd/mithril/node/node.go index f0f871cc..b8076e9f 100644 --- a/cmd/mithril/node/node.go +++ b/cmd/mithril/node/node.go @@ -2649,6 +2649,8 @@ postBootstrap: }() } + startSigverifyReporter(ctx) + turbineAlpenglowAddr := "" if alpenglowMode { turbineAlpenglowAddr = alpenglowAddrForGossip(alpenglowObserverBindAddr) 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/pkg/sigverify/sigverify.go b/pkg/sigverify/sigverify.go index 6ef0af43..12f5e7d3 100644 --- a/pkg/sigverify/sigverify.go +++ b/pkg/sigverify/sigverify.go @@ -215,6 +215,10 @@ func (b *Batch) Len() int { return len(b.pubs) } // 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 } 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 +} From 79d9677a4d302e505f61959f0cfd6a09b6cffa0c Mon Sep 17 00:00:00 2001 From: smcio Date: Fri, 31 Jul 2026 07:46:57 +0200 Subject: [PATCH 23/23] clarify wording on strictness checks in relation to `stdlib` backend --- cmd/mithril/configcmd/configcmd.go | 2 +- cmd/mithril/node/node.go | 4 ++-- config.example.toml | 4 +--- pkg/sealevel/ed25519_program.go | 4 +--- pkg/sigverify/sigverify.go | 14 ++------------ 5 files changed, 7 insertions(+), 21 deletions(-) diff --git a/cmd/mithril/configcmd/configcmd.go b/cmd/mithril/configcmd/configcmd.go index dc11cbd9..62badcaf 100644 --- a/cmd/mithril/configcmd/configcmd.go +++ b/cmd/mithril/configcmd/configcmd.go @@ -260,7 +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 is a rollback that weakens the predicate +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 b8076e9f..2c15a588 100644 --- a/cmd/mithril/node/node.go +++ b/cmd/mithril/node/node.go @@ -423,7 +423,7 @@ func init() { 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 (stdlib is a rollback that restores the pre-strict predicate)") + "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") @@ -2956,7 +2956,7 @@ func printStartupInfo(commandName string) { case sigverify.BackendGeneric: sigverifyDesc = "portable; no AVX512-IFMA on this CPU" case sigverify.BackendStdlib: - sigverifyDesc = "ROLLBACK: non-strict, accepts signatures mainnet rejects" + 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) diff --git a/config.example.toml b/config.example.toml index f0fb1b6c..72eb0af7 100644 --- a/config.example.toml +++ b/config.example.toml @@ -524,9 +524,7 @@ name = "mithril" # 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 - ROLLBACK ONLY. Bypasses signature verification hardening and - # restores Go's stdlib predicate, which accepts small-order keys - # that Solana mainnet rejects. Diagnostic use only. + # stdlib - use Go’s crypto/ed25519 implementation after the mandatory strict rejection checks. sigverify_backend = "auto" # Enable/disable pool allocator for slices diff --git a/pkg/sealevel/ed25519_program.go b/pkg/sealevel/ed25519_program.go index 60c441f4..179c6e22 100644 --- a/pkg/sealevel/ed25519_program.go +++ b/pkg/sealevel/ed25519_program.go @@ -141,9 +141,7 @@ func Ed25519ProgramExecute(execCtx *ExecutionCtx) error { // throughput of a batch that is usually one or two signatures deep. if execCtx.Features.IsActive(features.Ed25519PrecompileVerifyStrict) { // DalekStrict: reject small-order A and R, accept a non-canonical - // A and hash its original bytes. Routed through pkg/sigverify so - // the precompile honours the same backend selection and stdlib - // rollback switch as every other verification site. + // A and hash its original bytes. if !sigverify.VerifyOne((*[32]byte)(pubkey), msg[:offsets.MessageDataSize], signature[:64]) { return PrecompileErrSignature } diff --git a/pkg/sigverify/sigverify.go b/pkg/sigverify/sigverify.go index 12f5e7d3..fae00447 100644 --- a/pkg/sigverify/sigverify.go +++ b/pkg/sigverify/sigverify.go @@ -35,18 +35,8 @@ const ( BackendR51 = "r51" // BackendGeneric forces the portable pure-Go backend. BackendGeneric = "generic" - // BackendStdlib selects the library's own crypto/ed25519-backed arithmetic. - // It swaps out the r51 assembly, the comb tables and the batch kernels -- - // where an implementation bug would realistically live -- while leaving the - // acceptance rule untouched, so an operator can rule those out without - // rebuilding. - // - // This deliberately does NOT bypass the library. An earlier revision routed - // this name straight at crypto/ed25519, which silently dropped the - // small-order rejection and made an operator flag change which signatures - // the node accepts. Two nodes on different settings would disagree on block - // validity, and the flag would be reached for under exactly the pressure - // that makes a silent fork worst. The predicate is not an operator knob. + // BackendStdlib uses Go's crypto/ed25519-backed arithmetic, but with the + // strictness checks applied first. BackendStdlib = "stdlib" )