mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
PLONK's verification equation was never written: verifyPLONKPairing refused every proof unconditionally, and above it sat some 200 lines decoding the proof and the key — exact sizes, subgroup checks, points at infinity — whose result that refusal discarded. The refusal is the whole of what the path does, so it is now the whole of what it says. The aggregate block proof went the same way. VerifyBlockProof re-verified the very transactions Verify had verified one line above, so it could only ever agree with them; nothing built a block carrying one; and the field stood on the wire and in the block id where a reader would take an aggregate guarantee to be. Two more second implementations: Verify type-asserted its parent to *Block after GetBlock had already refused anything that is not one, and Shutdown asked whether its logger was disabled before logging, which is what a logger decides. Coverage 95.5% -> 99.1%. What is left cannot be reached: four bn254.Pair error returns on one-element slices, two IsInSubGroup refusals on a curve whose G1 cofactor is 1 and whose Unmarshal already checks G2, and three boot gates — a store opened through a versiondb view, and fee.Validate on the floor chain.Floor returns.
99 lines
3.6 KiB
Go
99 lines
3.6 KiB
Go
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package zkvm
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/zap"
|
|
)
|
|
|
|
// blockTx is a transaction with something in every list, so a block built
|
|
// around it exercises the packed sub-frames rather than a trivial one.
|
|
func blockTx(fee uint64) *Transaction {
|
|
tx := &Transaction{
|
|
Type: TransactionTypeShield,
|
|
Version: 1,
|
|
Fee: fee,
|
|
TransparentInputs: []*TransparentInput{{TxID: ids.ID{1}, OutputIdx: 2, Amount: 100, Address: []byte("payer")}},
|
|
TransparentOutputs: []*TransparentOutput{{Amount: 90, AssetID: ids.ID{2}, Address: []byte("payee")}},
|
|
Nullifiers: [][]byte{[]byte("n1"), []byte("n2")},
|
|
Outputs: []*ShieldedOutput{{Commitment: []byte("c"), EncryptedNote: []byte("n"), EphemeralPubKey: []byte("e"), OutputProof: []byte("p")}},
|
|
Proof: &ZKProof{ProofType: "groth16", ProofData: []byte("pd"), PublicInputs: [][]byte{[]byte("a"), []byte("bb")}},
|
|
Expiry: 1 << 20,
|
|
Memo: []byte("memo"),
|
|
}
|
|
tx.ID = tx.ComputeID()
|
|
return tx
|
|
}
|
|
|
|
// blockAround hand-assembles a block frame holding one transaction blob, so the
|
|
// blob can carry padding an honest encoder never writes.
|
|
func blockAround(txBlob []byte) []byte {
|
|
bld := zap.NewBuilder(zap.HeaderSize + blkSize + len(txBlob) + 256)
|
|
txOff := writeU32List(bld, []uint32{uint32(len(txBlob))})
|
|
ob := bld.StartObject(blkSize)
|
|
var parent ids.ID
|
|
ob.SetBytesFixed(0, parent[:])
|
|
ob.SetUint64(32, 1)
|
|
ob.SetInt64(40, 1_600_000_000)
|
|
ob.SetList(48, txOff, 1)
|
|
ob.SetBytes(56, txBlob)
|
|
ob.SetBytes(64, []byte("root"))
|
|
ob.FinishAsRoot()
|
|
return bld.Finish()
|
|
}
|
|
|
|
// TestBlockRefusesPaddedTransaction: the block frame's own trailing-byte check
|
|
// cannot see padding inside a transaction blob, because that blob carries its
|
|
// own length and the outer size stays honest. Only the inner frame can refuse
|
|
// it. If it does not, the block parses and re-encodes to bytes that are not the
|
|
// bytes that arrived, so the block a node stores under an ID is not the block
|
|
// its peers gossiped under that ID.
|
|
func TestBlockRefusesPaddedTransaction(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
tx := blockTx(3)
|
|
txRaw := tx.Marshal()
|
|
|
|
// Positive control: the same hand-assembled shape without padding parses,
|
|
// so the refusal below is caused by the padding and nothing else.
|
|
var honest Block
|
|
require.NoError(parseBlockBytes(blockAround(txRaw), &honest))
|
|
require.Equal([]*Transaction{tx}, honest.Txs)
|
|
|
|
padded := append(append([]byte(nil), txRaw...), 0xAA, 0xBB, 0xCC, 0xDD)
|
|
var blk Block
|
|
require.ErrorIs(parseBlockBytes(blockAround(padded), &blk), errTrailingBytes)
|
|
}
|
|
|
|
// TestBlockReencodesToTheBytesItParsed is the property the padding attacks: a
|
|
// block that parses must produce the bytes it came from, or one block has many
|
|
// encodings and consensus names them all the same thing.
|
|
func TestBlockReencodesToTheBytesItParsed(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
blk := &Block{
|
|
ParentID_: ids.ID{0xEE},
|
|
BlockHeight: 7,
|
|
BlockTimestamp: 1_600_000_000,
|
|
Txs: []*Transaction{blockTx(1), blockTx(2)},
|
|
StateRoot: []byte("state-root"),
|
|
}
|
|
raw := blk.Marshal()
|
|
|
|
var got Block
|
|
require.NoError(parseBlockBytes(raw, &got))
|
|
require.Equal(blk.Txs, got.Txs)
|
|
require.Equal(raw, got.Bytes())
|
|
|
|
for _, tail := range [][]byte{{0}, {0xFF}, make([]byte, 64)} {
|
|
var trailing Block
|
|
require.ErrorIs(parseBlockBytes(append(append([]byte(nil), raw...), tail...), &trailing), errTrailingBytes)
|
|
}
|
|
}
|