mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
Accept wrote the block, the tip pointer and the height through the version layer and never committed, so a node that had accepted a thousand blocks came back from a restart naming genesis — while its peers held it to the tip it had already told them about. The three writes were also three early returns, so a failure part-way left the block stored under a tip that named its parent. They are now one commit with a rollback, alongside the height index that makes GetBlockIDAtHeight an answer rather than a refusal. Verify checked only that the parent existed, and only above height one. A height-one block could name a parent no node has ever seen; any block could name genesis as the parent of height 500, rewind chain time to revive expired quantum stamps, or jump forward and expire every stamp in flight. It now sits on its parent in height and in time, under a skew cap — and BuildBlock never stamps behind its own parent, so a node whose clock trails a slightly fast peer does not build blocks it will itself refuse. Verify held the VM's read lock and then called GetBlock, which takes it again. Go queues a waiting writer ahead of any later reader, so a build arriving between the two acquisitions deadlocked the chain with nothing to time out. The Quasar validator id was the CHAIN id — a field never assigned — so every node signed as the empty id, each peer's signature arrived as a duplicate of the first, and no threshold above one could ever be met. It is the node id. The default threshold was derived from a committee size that had not yet been defaulted, so a config naming neither got one-of-three. Cleanup released only FINALIZED entries, which are exactly the ones that do not accumulate. Corona keyed its PRF off the first 32 bytes of the message, so any shorter message through StampBlock killed the process; the signature counts were read outside the lock that appends them. A transaction whose quantum stamp has aged out never verifies again; left in the pool it held its slot for good, and enough of them stopped the chain accepting work. BuildBlock evicts what it could not verify, and re-arms the builder when the batch limit leaves work behind — the latch holds one signal, so the remainder had nothing to wake it. A wire too short to hold the header decoded to height 0, time 0 and the empty parent rather than failing, so every truncation named one value under as many ids as an attacker cared to send. Deleted, all proven unreferenced: the dlopen GPU plugin bridge (a second way to do what quantum.ParallelVerifyWithThreshold already does through accel), the stamper package (near-verbatim copy of the signer's accel path, with a mode-confusion bug the original does not have, and the only reason the tree needed geth/core), the Q-witness adapter, the NTT helpers (whose inverse did not invert), and a signature cache that was written and never read — and whose ids.ToID over a >32-byte input gave every entry the empty id, so all ten thousand slots were one. Coverage 20.0% -> 97.0%, with every load-bearing guard mutation-checked.
167 lines
6.6 KiB
Go
167 lines
6.6 KiB
Go
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package quantumvm
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/luxfi/chains/quantumvm/quantum"
|
|
"github.com/luxfi/ids"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func jsonBody(s string) io.Reader { return strings.NewReader(s) }
|
|
|
|
// TestServiceGetBlockAnswersOnlyForBlocksHeld. The block id comes off the wire,
|
|
// so the two ways it can be wrong — unparseable, and parseable but unknown —
|
|
// both have to be refusals rather than an empty reply that reads as a block.
|
|
func TestServiceGetBlockAnswersOnlyForBlocksHeld(t *testing.T) {
|
|
vm, _ := bootVM(t, quietConfig())
|
|
svc := &Service{vm: vm}
|
|
blk := advance(t, vm, 1)
|
|
|
|
var reply GetBlockReply
|
|
require.NoError(t, svc.GetBlock(nil, &GetBlockArgs{BlockID: blk.id.String()}, &reply))
|
|
require.Equal(t, uint64(1), reply.Height)
|
|
require.Equal(t, blk.timestamp.Unix(), reply.Timestamp)
|
|
require.False(t, reply.QuantumSig, "blocks carry no stamp on the wire")
|
|
|
|
require.Equal(t, blk.id.String(), reply.Block.ID)
|
|
require.Equal(t, blk.parentID.String(), reply.Block.ParentID)
|
|
require.Equal(t, blk.height, reply.Block.Height)
|
|
|
|
require.Error(t, svc.GetBlock(nil, &GetBlockArgs{BlockID: "not-an-id"}, &reply))
|
|
require.Error(t, svc.GetBlock(nil, &GetBlockArgs{BlockID: ids.GenerateTestID().String()}, &reply))
|
|
}
|
|
|
|
// TestServiceGenerateCoronaKeyHonoursTheSwitch: a chain running without Corona
|
|
// must not hand out Corona keys, and one running with it hands out a key of the
|
|
// width its algorithm says.
|
|
func TestServiceGenerateCoronaKeyHonoursTheSwitch(t *testing.T) {
|
|
vm, _ := bootVM(t, quietConfig()) // Corona off
|
|
svc := &Service{vm: vm}
|
|
|
|
var reply GenerateCoronaKeyReply
|
|
require.Error(t, svc.GenerateCoronaKey(nil, &GenerateCoronaKeyArgs{}, &reply))
|
|
require.Empty(t, reply.PublicKey, "a refused request still produced key material")
|
|
|
|
vm.Config.CoronaEnabled = true
|
|
require.NoError(t, svc.GenerateCoronaKey(nil, &GenerateCoronaKeyArgs{}, &reply))
|
|
require.Equal(t, vm.quantumSigner.GetPublicKeySize(), reply.KeySize)
|
|
require.Len(t, reply.PublicKey, reply.KeySize*2, "the key is reported as hex")
|
|
}
|
|
|
|
// TestServiceVerifyQuantumSignature reports the verdict rather than failing the
|
|
// call: a caller asking whether a signature is good gets an answer either way,
|
|
// and the answer is the signer's.
|
|
func TestServiceVerifyQuantumSignature(t *testing.T) {
|
|
vm, _ := bootVM(t, quietConfig())
|
|
svc := &Service{vm: vm}
|
|
|
|
var reply VerifyQuantumSignatureReply
|
|
require.Error(t, svc.VerifyQuantumSignature(nil,
|
|
&VerifyQuantumSignatureArgs{Message: "m", Signature: json.RawMessage(`{}`)}, &reply),
|
|
"stamps are off, so the endpoint refuses rather than answering")
|
|
|
|
vm.Config.QuantumStampEnabled = true
|
|
msg := []byte("the message that was signed")
|
|
key, err := vm.quantumSigner.GenerateCoronaKey()
|
|
require.NoError(t, err)
|
|
sig, err := vm.quantumSigner.Sign(msg, key)
|
|
require.NoError(t, err)
|
|
raw, err := json.Marshal(sig)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, svc.VerifyQuantumSignature(nil,
|
|
&VerifyQuantumSignatureArgs{Message: string(msg), Signature: raw}, &reply))
|
|
require.True(t, reply.Valid)
|
|
require.Equal(t, sig.Algorithm, reply.Algorithm)
|
|
|
|
// The same signature over other bytes is not valid, and says so.
|
|
require.NoError(t, svc.VerifyQuantumSignature(nil,
|
|
&VerifyQuantumSignatureArgs{Message: "other bytes entirely", Signature: raw}, &reply))
|
|
require.False(t, reply.Valid)
|
|
|
|
// Bytes that are not a signature at all are a bad request, not a verdict.
|
|
require.Error(t, svc.VerifyQuantumSignature(nil,
|
|
&VerifyQuantumSignatureArgs{Message: "m", Signature: json.RawMessage(`not json`)}, &reply))
|
|
}
|
|
|
|
// TestServiceGetPendingTransactionsIsBounded. The limit comes off the wire, so
|
|
// a caller asking for everything must not be able to make the node serialize
|
|
// the whole mempool in one reply.
|
|
func TestServiceGetPendingTransactionsIsBounded(t *testing.T) {
|
|
vm, _ := bootVM(t, quietConfig())
|
|
svc := &Service{vm: vm}
|
|
|
|
for i := 0; i < 7; i++ {
|
|
require.NoError(t, vm.txPool.AddTransaction(stampedTx(uint64(i), "op")))
|
|
}
|
|
|
|
var reply GetPendingTransactionsReply
|
|
require.NoError(t, svc.GetPendingTransactions(nil, &GetPendingTransactionsArgs{Limit: 3}, &reply))
|
|
require.Equal(t, 3, reply.Count)
|
|
require.Len(t, reply.Transactions, 3)
|
|
|
|
for _, limit := range []int{0, -1, 1 << 20} {
|
|
require.NoError(t, svc.GetPendingTransactions(nil, &GetPendingTransactionsArgs{Limit: limit}, &reply))
|
|
require.Equal(t, 7, reply.Count, "limit %d did not settle on the pool's size", limit)
|
|
}
|
|
|
|
require.NotEmpty(t, reply.Transactions[0].ID)
|
|
require.Equal(t, chainTime.Unix(), reply.Transactions[0].Timestamp)
|
|
}
|
|
|
|
// TestServiceGetHealthAndConfigReportTheRunningVM, not a copy of the defaults.
|
|
func TestServiceGetHealthAndConfigReportTheRunningVM(t *testing.T) {
|
|
vm, _ := bootVM(t, quietConfig())
|
|
svc := &Service{vm: vm}
|
|
require.NoError(t, vm.txPool.AddTransaction(stampedTx(1, "op")))
|
|
|
|
var health GetHealthReply
|
|
require.NoError(t, svc.GetHealth(nil, &GetHealthArgs{}, &health))
|
|
require.True(t, health.Healthy)
|
|
require.Equal(t, Version, health.Version)
|
|
require.Equal(t, 1, health.PendingTxCount)
|
|
require.Equal(t, vm.Config.MaxParallelTxs, health.ParallelWorkers)
|
|
require.False(t, health.QuantumEnabled)
|
|
require.False(t, health.CoronaEnabled)
|
|
|
|
var cfg GetConfigReply
|
|
require.NoError(t, svc.GetConfig(nil, &GetConfigArgs{}, &cfg))
|
|
require.Equal(t, vm.Config.MaxParallelTxs, cfg.MaxParallelTxs)
|
|
require.Equal(t, vm.Config.ParallelBatchSize, cfg.ParallelBatchSize)
|
|
require.Equal(t, vm.Config.QuantumAlgorithmVersion, cfg.QuantumAlgorithmVersion)
|
|
|
|
// A shut-down VM says so rather than reporting itself healthy.
|
|
require.NoError(t, vm.Shutdown(context.Background()))
|
|
require.NoError(t, svc.GetHealth(nil, &GetHealthArgs{}, &health))
|
|
require.False(t, health.Healthy)
|
|
}
|
|
|
|
// TestServiceRepliesCarryNoSecrets. GenerateCoronaKey mints a keypair; the
|
|
// caller gets the public half. The private half leaving here would hand every
|
|
// unauthenticated caller a validator identity.
|
|
func TestServiceRepliesCarryNoSecrets(t *testing.T) {
|
|
vm, _ := bootVM(t, quietConfig())
|
|
vm.Config.CoronaEnabled = true
|
|
svc := &Service{vm: vm}
|
|
|
|
var reply GenerateCoronaKeyReply
|
|
require.NoError(t, svc.GenerateCoronaKey(nil, &GenerateCoronaKeyArgs{}, &reply))
|
|
|
|
raw, err := json.Marshal(reply)
|
|
require.NoError(t, err)
|
|
require.NotContains(t, strings.ToLower(string(raw)), "private")
|
|
|
|
// And the signature type the service echoes back holds no secret either.
|
|
sigRaw, err := json.Marshal(&quantum.QuantumSignature{Signature: []byte{1}})
|
|
require.NoError(t, err)
|
|
require.NotContains(t, strings.ToLower(string(sigRaw)), "private")
|
|
}
|