chains: comments state the contract, not the history behind it

Across these files the comments explained a rule by recounting what happened
without it — which key was mistaken for which, which payer a full pool released,
what a verifier accepted, what a chain did with work it never announced. That
tells a reader what changed rather than what holds, and it dates: the account
stays in the file long after anyone needs it, while the rule it was meant to
convey has to be inferred back out.

They now state the rule. A circuit is judged on its own key. A witness is
exactly as long as the key's circuit says. A count describes the records it is
read from. A full pool releases what pays least. Custody membership costs stake
because an identifier can be aimed and stake cannot. Consensus builds when
WaitForEvent returns, so accepting work and reporting it are one step.

Behaviour is unchanged; the diff is comments, with one exception worth naming:
gofmt realigned a block of one-line methods in quantumvm/feegate_test.go that
was already unformatted before this commit.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
Hanzo AI
2026-08-26 13:17:16 -07:00
parent 4f4cd52651
commit 8dd094e098
15 changed files with 107 additions and 149 deletions
+2 -2
View File
@@ -243,8 +243,8 @@ type VM struct {
// Consensus
toEngine chan<- vmcore.Message
// work wakes WaitForEvent when there is something to build. A burst of
// submissions coalesces into one signal rather than flooding the engine —
// the concern WaitForEvent used to answer by never returning at all.
// submissions coalesces into one signal, so the engine is told that there
// is work rather than how much.
work vmcore.Latch
// Logging
+4 -5
View File
@@ -537,8 +537,8 @@ func (vm *VM) LastAccepted(ctx context.Context) (ids.ID, error) {
}
// CreateHandlers implements the common.VM interface. The bridge's API is the
// JSON-RPC service in rpc.go — estimate a fee, submit a request, ask after one,
// read the signer set. It was written and never served.
// JSON-RPC service in rpc.go: estimate a fee, submit a request, ask after one,
// read the signer set.
func (vm *VM) CreateHandlers(ctx context.Context) (map[string]http.Handler, error) {
return vm.CreateRPCHandlers()
}
@@ -570,9 +570,8 @@ func (vm *VM) readiness() (ready bool, reason string, chains int) {
}
}
// HealthCheck implements the common.VM interface. Answering healthy whatever the
// state is tells an operator nothing and routes traffic at a node that cannot
// bridge.
// HealthCheck implements the common.VM interface, reporting what this node can
// do so traffic reaches one that can bridge.
func (vm *VM) HealthCheck(ctx context.Context) (chain.HealthResult, error) {
ready, reason, chains := vm.readiness()
return chain.HealthResult{
+4 -10
View File
@@ -229,10 +229,6 @@ func TestWatchDoesNotAdvancePastAChainItCannotRead(t *testing.T) {
// TestALockBecomesABlock is the whole path in one test: a gateway locks value
// on the source chain, the watcher reads it, consensus is told there is work,
// and the block that gets built carries exactly that transfer.
//
// Before the watcher existed nothing wrote to the pending queue outside of
// tests, so BuildBlock answered "no pending bridge requests" forever and B could
// not leave genesis however much was bridged.
func TestALockBecomesABlock(t *testing.T) {
vm, _ := vmWithPending(t, 0)
vm.evmByChainID = map[uint32]ChainClient{96368: nil}
@@ -275,10 +271,9 @@ func TestALockBecomesABlock(t *testing.T) {
}
}
// TestHealthSaysWhatItCannotDo. HealthCheck answered healthy whatever the state
// was, which routes traffic at a node that cannot bridge and tells an operator
// nothing about why. Releasing needs a threshold key to attest with and a chain
// to broadcast to.
// TestHealthSaysWhatItCannotDo. Releasing needs a threshold key to attest with
// and a chain to broadcast to, so health reports whether this node has both and
// names what is missing when it does not.
func TestHealthSaysWhatItCannotDo(t *testing.T) {
// A node with no chains wired is not a relayer. That is a configuration,
// not a fault, and calling it unhealthy would take validators that were
@@ -337,8 +332,7 @@ func TestHealthEndpointAgreesWithTheNodeCheck(t *testing.T) {
}
}
// The bridge's API is the JSON-RPC service; the canned handlers that used to
// stand in front of it answered fiction.
// The bridge's API is the JSON-RPC service, and that is what gets served.
func TestTheServedAPIIsTheRealOne(t *testing.T) {
vm := &VM{log: log.NewNoOpLogger()}
handlers, err := vm.CreateHandlers(context.Background())
+2 -4
View File
@@ -13,10 +13,8 @@ import (
// TestIssuingACredentialBuildsABlock is the whole path: a credential is issued,
// consensus is told there is work, and the block that gets built carries it.
//
// WaitForEvent used to wait only on the context, so it never returned, BuildBlock
// was never called, and I-Chain could not leave genesis however many credentials
// were issued.
// Consensus builds only when WaitForEvent returns, so issuing and reporting are
// one step.
func TestIssuingACredentialBuildsABlock(t *testing.T) {
require := require.New(t)
vm := setupTestVM(t)
+10 -11
View File
@@ -126,18 +126,17 @@ func keygenCeremonyID(keyID string, policy quorum.Policy, participants []party.I
// custodySet chooses the N validators that will hold shares of a key: the N with
// the most stake, ties broken by node id.
//
// Selection used to be "the first N in canonical order", and canonical order is a
// lexicographic sort of node ids. A node id is a hash of the node's certificate,
// so an attacker grinds certificates offline until its ids sort first — roughly
// 256 hashes per leading zero byte, and a zero byte encodes as the smallest
// base58 digit. Two ground certificates would then sit in the share set of EVERY
// key the chain generates, which under a 2-of-3 policy is a quorum. Nothing needs
// reconstructing at that point: the holder runs the signing protocol locally with
// both shares and signs whatever it likes, off-chain, with no block to detect.
// Stake decides membership because a node id cannot. An id is a hash of the
// node's certificate, so ids can be aimed: certificates are cheap to generate
// offline and one that sorts early is a matter of how many were tried. An order
// over ids alone would therefore let a party choose its own seats, and seats in
// every key the chain generates — under a 2-of-3 policy, a quorum of them, held
// by one party that can then sign off-chain with no block to detect it.
//
// Weight cannot be ground. Entering the set costs stake proportional to the stake
// already there — the same cost as attacking consensus — so custody stops being
// the cheaper target. Selection stays a pure function of replicated state, so
// Stake is not chosen that way. A seat costs stake proportional to the stake
// already there, which is the same price as attacking consensus, so custody is
// not the cheaper target. Ties break lexicographically, which is safe once stake
// has chosen the set. Selection stays a pure function of replicated state, so
// every validator derives the same set without negotiating one.
func (vm *VM) custodySet(ctx context.Context, pchainHeight uint64, n int) ([]party.ID, error) {
if vm.rt == nil || vm.rt.ValidatorState == nil {
+6 -10
View File
@@ -264,9 +264,9 @@ func (vm *VM) Initialize(
// Resume from the accepted tip, or install genesis on a fresh database.
// This is the difference between a chain and a cache: a node that restarts
// must come back at the height it left, not at zero. Recomputing genesis
// unconditionally (as this VM previously did) discards every accepted block
// and re-runs history from an empty registry.
// comes back at the height it left, not at zero. Recomputing genesis
// unconditionally would discard every accepted block and re-run history
// from an empty registry.
tip, found, err := vm.state.LastAccepted()
if err != nil {
return fmt.Errorf("mpcvm: read accepted tip: %w", err)
@@ -493,11 +493,9 @@ func (vm *VM) parseConfig(configBytes []byte) error {
// =============================================================================
//
// Every entry point below delegates to the ceremony lifecycle in custody.go.
// There is no second path that reaches a key or a signature: the legacy
// in-memory session machinery this VM used to carry (keygenSessions,
// signingSessions, an activeKeyID, a `keys` map persisted only at shutdown) was
// a parallel, unreplicated copy of state that could and did disagree with the
// chain. It is gone.
// There is no second path that reaches a key or a signature, and no in-memory
// session state beside the chain: a parallel copy of who holds what would be
// unreplicated, and two answers to that question is one too many.
// StartKeygen generates a custody key using the chain's default policy.
func (vm *VM) StartKeygen(ctx context.Context, keyID string, by Caller) (*Operation, error) {
@@ -1060,8 +1058,6 @@ func uncompressedXY(pubKey []byte) []byte {
}
}
// computeRecoveryID is no longer needed - we use sig.V() from the Signature interface
// =============================================================================
// Session-Ready: Attestation Domains
// =============================================================================
+11 -10
View File
@@ -22,13 +22,15 @@ type feeTestTx struct {
fee uint64
}
func (t *feeTestTx) ID() ids.ID { return t.id }
func (t *feeTestTx) Bytes() []byte { return t.id[:] }
func (t *feeTestTx) Verify() error { return nil }
func (t *feeTestTx) Execute() error { return nil }
func (t *feeTestTx) GetQuantumSignature() *quantum.QuantumSignature { return &quantum.QuantumSignature{} }
func (t *feeTestTx) Timestamp() time.Time { return time.Unix(0, 0) }
func (t *feeTestTx) Fee() uint64 { return t.fee }
func (t *feeTestTx) ID() ids.ID { return t.id }
func (t *feeTestTx) Bytes() []byte { return t.id[:] }
func (t *feeTestTx) Verify() error { return nil }
func (t *feeTestTx) Execute() error { return nil }
func (t *feeTestTx) GetQuantumSignature() *quantum.QuantumSignature {
return &quantum.QuantumSignature{}
}
func (t *feeTestTx) Timestamp() time.Time { return time.Unix(0, 0) }
func (t *feeTestTx) Fee() uint64 { return t.fee }
// newQuantumVMWithPolicy wires a VM with the canonical policy directly,
// bypassing the full Initialize (which requires Quasar bridge setup,
@@ -70,9 +72,8 @@ func TestQuantumVM_IssueTx_RejectsAllUserTx(t *testing.T) {
}
// TestPoolTellsConsensusThereIsWork: a transaction the pool accepted is worth
// nothing until consensus is told about it. WaitForEvent used to wait only on
// the context, so it never returned, BuildBlock was never called, and the chain
// could not leave genesis however many transactions arrived.
// nothing until consensus is told about it. Consensus builds only when
// WaitForEvent returns, so accepting work and reporting it are one step.
func TestPoolTellsConsensusThereIsWork(t *testing.T) {
pool := NewTransactionPool(8, 8, log.NewNoOpLogger())
+6 -7
View File
@@ -78,11 +78,10 @@ func (mp *Mempool) AddTransaction(tx *Transaction) error {
txSize := uint64(256)
feePerByte := tx.Fee / txSize
// A full pool has to give something up, and it has to be whatever pays
// least. Dropping the best payer instead inverts the fee market and
// discards exactly the transaction a block would have taken first. If the
// arrival is itself the cheapest, the pool already holds better and there
// is nothing to gain by swapping.
// A full pool gives up whatever pays least, so what it holds is always the
// best it has been offered. An arrival that is itself the cheapest is
// refused: taking it would mean dropping a better transaction for a worse
// one.
if len(mp.txs) >= mp.maxSize {
cheapest := mp.cheapest()
if cheapest == nil || cheapest.feePerByte >= feePerByte {
@@ -135,8 +134,8 @@ func (mp *Mempool) RemoveTransaction(txID ids.ID) {
// cheapest returns the transaction paying least per byte, which is the one a
// full pool gives up. The heap is ordered highest-first, so the cheapest sits
// somewhere among its leaves rather than at its root; the pool is bounded and
// this runs only when it is full, so walking it is the whole of the work.
// among its leaves rather than at its root; the pool is bounded and this runs
// only when it is full, so walking it is the whole of the work.
func (mp *Mempool) cheapest() *MempoolTx {
var out *MempoolTx
for _, candidate := range mp.txHeap {
+8 -11
View File
@@ -28,9 +28,8 @@ func shieldedTransfer(fee uint64, n byte) *Transaction {
}
// TestMempoolTellsConsensusThereIsWork: a transaction the mempool accepted is
// worth nothing until consensus is told about it. WaitForEvent used to wait only
// on the context, so it never returned, BuildBlock was never called, and the
// chain could not leave genesis however many transactions arrived.
// worth nothing until consensus is told about it. Consensus builds only when
// WaitForEvent returns, so accepting work and reporting it are one step.
func TestMempoolTellsConsensusThereIsWork(t *testing.T) {
mp := NewMempool(10, log.NoLog{})
@@ -60,11 +59,9 @@ func TestMempoolTellsConsensusThereIsWork(t *testing.T) {
}
}
// TestFullPoolGivesUpTheCheapest. Eviction used to pop the heap, which is
// ordered highest-first, so a full pool discarded its best payer — inverting the
// fee market and throwing away exactly the transaction a block would have taken
// first. Fees are divided by a fixed 256-byte estimate, so they must differ by
// more than that to differ in priority at all.
// TestFullPoolGivesUpTheCheapest pins which transaction a full pool releases.
// Fees are divided by a fixed 256-byte estimate, so they must differ by more
// than that to differ in priority at all.
func TestFullPoolGivesUpTheCheapest(t *testing.T) {
const unit = 256
mp := NewMempool(2, log.NoLog{})
@@ -97,9 +94,9 @@ func TestFullPoolRefusesAnArrivalWorseThanItHolds(t *testing.T) {
}
// Two callers asking what to build at once must not write to each other's
// transactions. GetPendingTransactions sorts a copy of the heap, but the copy
// holds the same pointers, so anything the sort wrote through them was a write
// under a read lock. Run with -race.
// transactions. GetPendingTransactions sorts a copy of the heap and that copy
// holds the same pointers, so the sort must leave them untouched. Run with
// -race.
func TestReadingThePoolDoesNotWriteToIt(t *testing.T) {
mp := NewMempool(64, log.NoLog{})
for i := 0; i < 16; i++ {
+5 -7
View File
@@ -152,13 +152,11 @@ func (ndb *NullifierDB) GetNullifiersByHeight(height uint64) [][]byte {
return nullifiers
}
// GetNullifierCount returns the number of spent nullifiers.
//
// It counts the set rather than reading a running total kept beside it. A total
// is a second write, and a node that dies between the two comes back with a
// number that disagrees with its own records — from which one removal drives an
// unsigned counter below zero and reports 1.8e19 spent notes forever. Counting
// the set cannot disagree with the set.
// GetNullifierCount returns the number of spent nullifiers, counted off the set
// itself. Every record is loaded at startup and nullifiers are never pruned, so
// the set is the whole of them; a total kept alongside would be a second write
// that has to agree with the first, and this cannot disagree with what it
// describes.
func (ndb *NullifierDB) GetNullifierCount() uint64 {
ndb.mu.RLock()
defer ndb.mu.RUnlock()
+4 -9
View File
@@ -12,14 +12,9 @@ import (
"github.com/stretchr/testify/require"
)
// TestSpentCountIsReadOffTheRecords. The count used to be a running total
// written beside the records, so a node that died between the two writes came
// back with a number that disagreed with its own set — and one removal from
// there drove an unsigned counter below zero, reporting 1.8e19 spent notes to
// every dashboard from then on.
//
// Counting the set cannot disagree with the set, so this starts from the state
// that used to be unrecoverable: a record on disk with no counter beside it.
// TestSpentCountIsReadOffTheRecords pins that the count describes the records
// and cannot drift from them. It starts from the state a total kept alongside
// would disagree with: a record on disk with no counter beside it.
func TestSpentCountIsReadOffTheRecords(t *testing.T) {
db := memdb.New()
@@ -57,7 +52,7 @@ func TestSpentCountFollowsTheSet(t *testing.T) {
require.Equal(t, uint64(1), ndb.GetNullifierCount())
}
// TestNullifierReadDoesNotWriteTheSet. GetNullifierHeight used to memoise what
// TestNullifierReadDoesNotWriteTheSet. GetNullifierHeight must not memoise what
// it loaded while holding only the read lock, and a read lock promises every
// other reader that nothing is changing — so two callers missing at once wrote
// the same map at the same time, which is a runtime throw that takes the
+7 -9
View File
@@ -244,13 +244,11 @@ func buildPLONKInput(vk, proof []byte, numInputs uint32, inputs []byte) []byte {
return out
}
// TestPLONKVerifierNeverUniversalAccepts is the H4 regression test. The
// previous verifyPLONK computed a self-cancelling pairing, discarded the
// result, ignored the public inputs, and returned VALID (0x01) for ANY
// >=544-byte blob. We construct a well-formed-but-arbitrary proof+VK
// (7 valid G1 commitments + 3 scalars + a valid SRS G2) — exactly the
// shape the old bypass rubber-stamped — and assert the precompile does
// NOT return 0x01. It must fail closed (0x00).
// TestPLONKVerifierNeverUniversalAccepts pins that this verifier accepts
// nothing while the verification equation is unimplemented. It builds a
// well-formed but arbitrary proof and key — 7 valid G1 commitments, 3 scalars,
// a valid SRS G2 — which is the shape a structural check alone would pass, and
// requires 0x00.
func TestPLONKVerifierNeverUniversalAccepts(t *testing.T) {
_, _, g1, g2 := bn254.Generators()
@@ -547,8 +545,8 @@ func TestCrossChainVerifierRejectsTooShort(t *testing.T) {
func TestCrossChainVerifierRejectsStubs(t *testing.T) {
v := &CrossChainZKVerifier{ZChainID: ids.ID{}}
// STARK is no longer a stub — it routes to the strict-PQ STARK
// verifier on Z-Chain. Only Halo2/Nova remain unimplemented.
// STARK routes to the strict-PQ verifier on Z-Chain. Halo2 and Nova are
// the unimplemented ones.
for _, vtype := range []byte{VerifierTypeHalo2, VerifierTypeNova} {
result, err := v.Run([]byte{vtype, 0x00})
if err != errNotImplemented {
+5 -7
View File
@@ -14,7 +14,7 @@ import (
)
// TestRestartedNodeRefusesADuplicateCommitment. Block.Accept refuses a block
// carrying a commitment the set already holds. The set used to live only in
// carrying a commitment the set already holds. The set lives in
// memory and was never rebuilt, so that verdict depended on how recently the
// node was started: a restarted validator accepted what a running one refused,
// and the two ended up on different chains.
@@ -35,11 +35,9 @@ func TestRestartedNodeRefusesADuplicateCommitment(t *testing.T) {
require.Equal(t, uint64(1), restarted.GetUTXOCount())
}
// TestUTXOCountIsReadOffTheRecords. The count used to be a running total
// written beside the records, so a node that died between the two writes came
// back with a number that disagreed with its own set — and one removal from
// there drove an unsigned counter below zero. This starts from the state that
// used to be unrecoverable: a record on disk with no total beside it.
// TestUTXOCountIsReadOffTheRecords pins that the count describes the records and
// cannot drift from them. It starts from the state a total kept alongside would
// disagree with: a record on disk with no total beside it.
func TestUTXOCountIsReadOffTheRecords(t *testing.T) {
db := memdb.New()
utxo := &UTXO{TxID: ids.ID{1}, Commitment: []byte("orphan"), Height: 1}
@@ -92,7 +90,7 @@ func TestRestartRebuildsTheHeightIndex(t *testing.T) {
require.Empty(t, empty, "a height nothing was created at holds nothing")
}
// TestUTXOReadDoesNotWriteTheSet. GetUTXO used to memoise what it loaded while
// TestUTXOReadDoesNotWriteTheSet. GetUTXO must not memoise what it loads while
// holding only the read lock, and a read lock promises every other reader that
// nothing is changing — so two clients asking for uncached commitments wrote
// the same map at the same time, which is a runtime throw that takes the
+22 -26
View File
@@ -340,17 +340,14 @@ func (pv *ProofVerifier) verifyPublicInputs(tx *Transaction) error {
return nil
}
// loadVerifyingKeys takes the verifying key for each circuit the config keys,
// and installs nothing for the others.
// loadVerifyingKeys takes the verifying key for each circuit the config keys and
// installs nothing for the others.
//
// It used to install an all-zero key for an unkeyed circuit and then decide, for
// the whole verifier at once, whether the keys were "dummy" — true only while
// EVERY key was zero. So keying one circuit turned that protection off for the
// rest, and their all-zero keys were then used as if real. That is the worst
// direction for a rule to fail in: the more of a chain an operator configures,
// the less of it is guarded. A key that is absent cannot be mistaken for one
// that is present, so the placeholder is gone and the circuit is refused by
// name at the lookup that already checks for it.
// A circuit the operator did not key has no key here, so the lookup in front of
// each verify refuses it by name. That is the one place the question is asked,
// and it is asked per circuit: a key is judged on its own, never on what the
// verifier holds for some other circuit. An absent key cannot be mistaken for a
// present one.
func (pv *ProofVerifier) loadVerifyingKeys() error {
for _, ct := range []TransactionType{
TransactionTypeTransfer,
@@ -490,11 +487,11 @@ func (pv *ProofVerifier) verifyGroth16WithGnark(proof *ZKProof, vkBytes []byte)
// n points speaks about exactly n-1 public inputs — that count is a property of
// the circuit the key was made for, not something a transaction chooses.
//
// The witness arrives with the transaction, so a peer picks its length. Too many
// would read past the end of K. Too few is quieter and no better: the sum below
// runs over the witness, so a short one leaves the trailing K points out of the
// linear combination and checks a smaller statement than the circuit was
// compiled for. It has to be exactly what the key says.
// The witness arrives with the transaction, so a peer picks its length, and the
// sum below runs over the witness against K. A length either side of what the
// key states judges the proof against a statement the key does not describe —
// past the end of K in one direction, and short of the trailing K points in the
// other — so it has to be exactly what the key says.
func verifyGroth16Pairing(proof *Groth16Proof, vk *Groth16VerifyingKey, witness []fr.Element) error {
if want := len(vk.K) - 1; len(witness) != want {
return fmt.Errorf("public inputs: %d supplied, the verifying key's circuit takes %d",
@@ -548,12 +545,12 @@ func verifyGroth16Pairing(proof *Groth16Proof, vk *Groth16VerifyingKey, witness
}
// A point this verifier reads has to be in the prime-order subgroup and must
// not be the point at infinity. gnark encodes infinity as all-zero bytes and
// reports it as in-subgroup, and a pairing drops any term whose argument is
// infinity — so an element at infinity silently removes itself from the
// equation and leaves a weaker check than the one written down. checkG1 and
// checkG2 are the one place that decides what a usable point is; every
// decoder below routes through them.
// not be the point at infinity. Both halves are load-bearing: gnark encodes
// infinity as all-zero bytes and reports it as in-subgroup, and a pairing drops
// any term whose argument is infinity, so an element at infinity would remove
// itself from the equation and leave a weaker check than the one written down.
// checkG1 and checkG2 are the one place that decides what a usable point is,
// and every decoder below routes through them.
var (
errOffSubgroup = errors.New("point not in the prime-order subgroup")
errAtInfinity = errors.New("point at infinity")
@@ -811,11 +808,10 @@ func verifyPLONKPairing(_ *PLONKProof, _ *PLONKVerifyingKey, _ []fr.Element) err
const plonkProofSize = 9*64 + 5*32
// deserializePLONKProof reads a PLONK proof, which is exactly plonkProofSize
// bytes. It used to take anything from 544 bytes up and leave whichever
// evaluations were missing at zero, so a truncated proof decoded as a
// well-formed one carrying values nobody sent. Trailing bytes are refused for
// the same reason they are refused elsewhere: bytes the format does not
// describe are bytes two different proofs can differ in.
// bytes: every evaluation is part of the proof, so a frame that is short of one
// is not a proof rather than a proof with a zero in it. Trailing bytes are
// refused for the reason they are refused elsewhere here — bytes the format
// does not describe are bytes two proofs can differ in while decoding alike.
func deserializePLONKProof(data []byte) (*PLONKProof, error) {
if len(data) != plonkProofSize {
return nil, fmt.Errorf("PLONK proof is %d bytes, want %d", len(data), plonkProofSize)
+11 -21
View File
@@ -320,15 +320,10 @@ func TestCircuitWithoutAKeyIsRefused(t *testing.T) {
}
}
// TestKeyingOneCircuitDoesNotEnableTheOthers. An unkeyed circuit used to get an
// all-zero placeholder key, and whether those placeholders counted as real was
// decided for the whole verifier at once — "dummy" only while EVERY key was
// zero. So keying a single circuit turned the protection off for the rest, and
// their zeros were then used as verifying keys. The rule failed in the worst
// direction: the more of a chain an operator configured, the less of it was
// guarded.
//
// A circuit with no key now has no key, and is refused by name.
// TestKeyingOneCircuitDoesNotEnableTheOthers pins that each circuit is judged on
// its own key. Keying one circuit says nothing about any other, and a circuit
// the operator did not key is refused by name rather than judged against
// whatever stands in for a key.
func TestKeyingOneCircuitDoesNotEnableTheOthers(t *testing.T) {
pv := keyedVerifier(t, "groth16", map[string][]byte{
string(TransactionTypeTransfer): groth16Key(4),
@@ -364,11 +359,10 @@ func TestKeyingOneCircuitDoesNotEnableTheOthers(t *testing.T) {
}
}
// TestPLONKProofMustBeWholeAndNoMore. A short proof used to decode with
// whichever evaluations were missing left at zero, so a truncated proof became a
// well-formed one carrying values nobody sent. Trailing bytes are refused for
// the reason they are refused everywhere else: bytes the format does not
// describe are bytes two different proofs can differ in.
// TestPLONKProofMustBeWholeAndNoMore pins the proof's size exactly. Every
// evaluation is part of the proof, so a frame short of one is not a proof; and
// bytes the format does not describe are bytes two proofs can differ in while
// decoding alike.
func TestPLONKProofMustBeWholeAndNoMore(t *testing.T) {
whole := plonkFrame()
if len(whole) != plonkProofSize {
@@ -397,13 +391,9 @@ func TestPLONKProofMustBeWholeAndNoMore(t *testing.T) {
// TestWitnessMustMatchWhatTheKeysCircuitTakes. A verifying key holds one K point
// per public input plus a constant term, so it states exactly how many inputs
// the circuit it was made for takes. A transaction supplies that witness, and a
// peer chooses its length.
//
// Too many reads past the end of K. Too few is quieter and no better: the linear
// combination runs over the witness, so a short one leaves the trailing K points
// out and checks a smaller statement than the circuit was compiled for — the
// proof is then judged against something the key does not describe.
// the circuit it was made for takes, and a peer picks the length it sends.
// Either side of that count judges the proof against a statement the key does
// not describe.
func TestWitnessMustMatchWhatTheKeysCircuitTakes(t *testing.T) {
// Four K points is a circuit taking three public inputs.
vk, err := deserializeVerifyingKey(groth16Key(4))