genesis: install the M-Chain plugin under the vmID genesis actually declares

The build installed M-Chain's plugin binary as
tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t — the CB58 of the
retired `thresholdvm` identifier — while the genesis builder declares the
chain with constants.MPCVMID (qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS).
The plugin registry resolves a CreateChainTx's vmID to an implementation
by filename, so the two never met: M-Chain was declared in genesis and no
node could start it. The Dockerfile comment had already been renamed to
"mpcvm" without the CB58 being recomputed, which is why it read as correct.

Fixes all five sites (plugin build target, the build-verify list, the
runtime COPY, the comment, and publish_plugin_set.sh) and adds
genesis/builder/mchain_test.go, which pins the vmID literally and asserts
M-Chain is present in the height-0 chain set for mainnet, testnet and
local. A vmID is an immutable one-way door once a chain is created with
it, so it is now covered by a test rather than by five copies of a string.

Bumps luxfi/genesis to v1.16.3, where mchain.json states its quorum as
"policy": "7-of-10" instead of a bare mpcThreshold that reads as the
signer count to an operator and as the polynomial degree to a library.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
zeekay
2026-07-25 14:53:07 -07:00
co-authored by Hanzo Dev
parent 66d5940f88
commit d5eff75934
5 changed files with 151 additions and 10 deletions
+4 -4
View File
@@ -322,7 +322,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
# oraclevm -> r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS
# quantumvm -> ry9Sg8rZdT26iEKvJDmC2wkESs4SDKgZEhk5BgLSwg1EpcNug
# relayvm -> sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz
# mpcvm -> tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t
# mpcvm -> qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS
# zkvm -> vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9
# MUST track node's go.mod luxfi/chains (the D-Chain dexvm + 10 VM plugins).
@@ -366,7 +366,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
( cd /tmp/chains/relayvm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz ./cmd/plugin ) || echo "WARN: relayvm plugin build skipped" ; \
( cd /tmp/chains/mpcvm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t ./cmd/plugin ) || echo "WARN: mpcvm plugin build skipped" ; \
-o /luxd/build/plugins/qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS ./cmd/plugin ) || echo "WARN: mpcvm plugin build skipped" ; \
( cd /tmp/chains/zkvm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9 ./cmd/plugin ) || echo "WARN: zkvm plugin build skipped" ; \
( chmod +x /luxd/build/plugins/* 2>/dev/null || true ) && \
@@ -379,7 +379,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS \
ry9Sg8rZdT26iEKvJDmC2wkESs4SDKgZEhk5BgLSwg1EpcNug \
sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz \
tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t \
qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS \
vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9 ; do \
test -s /luxd/build/plugins/$p \
|| { echo "FATAL: required chain-VM plugin $p missing/empty — its build failed above (see the matching WARN line); the runtime-stage hard COPY would otherwise fail cryptically. Surface & fix the real Go build error, or remove the plugin from BOTH the build list and the runtime COPY."; exit 1; } ; \
@@ -489,7 +489,7 @@ COPY --from=builder \
/luxd/build/plugins/r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS \
/luxd/build/plugins/ry9Sg8rZdT26iEKvJDmC2wkESs4SDKgZEhk5BgLSwg1EpcNug \
/luxd/build/plugins/sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz \
/luxd/build/plugins/tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t \
/luxd/build/plugins/qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS \
/luxd/build/plugins/vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9 \
/luxd/build/plugins/
WORKDIR /luxd/build
+140
View File
@@ -0,0 +1,140 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package builder
import (
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/luxfi/constants"
"github.com/luxfi/node/vms/platformvm/genesis"
pchaintxs "github.com/luxfi/node/vms/platformvm/txs"
)
// parsePolicy reads the operator form "3-of-5" into (K, N).
//
// The canonical parser is luxfi/threshold/pkg/quorum, but the node's genesis
// builder is a boot-critical path and does not take a dependency on a
// threshold-crypto library to read three integers. What this test pins is the
// FORM — that the genesis blob states a quorum in a spelling that cannot be
// confused with a polynomial degree — and the form is checkable here.
func parsePolicy(t *testing.T, s string) (k, n int) {
t.Helper()
_, err := fmt.Sscanf(s, "%d-of-%d", &k, &n)
require.NoErrorf(t, err, "policy %q is not in the operator form \"3-of-5\"", s)
require.Greaterf(t, k, 1, "policy %q: k=1 is not a threshold policy", s)
require.LessOrEqualf(t, k, n, "policy %q can never be satisfied", s)
return k, n
}
// canonicalMPCVMID is M-Chain's vmID in the encoding the node's plugin registry
// resolves against. Pinned literally: the plugin binary's FILENAME must equal
// it, and a change that leaves the two out of step produces a chain that is
// declared in genesis but that no node can start.
//
// The predecessor value, tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t, is
// the retired `thresholdvm` identifier. It was what the Dockerfile installed
// the plugin under while genesis declared MPCVMID, so the two never met.
const canonicalMPCVMID = "qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS"
// M-Chain must be a GENESIS chain — in the P-Chain's chain set at height 0,
// tracked by every validator from boot — not a chain created later by a
// CreateChainTx someone has to remember to submit.
//
// The difference matters for custody specifically. A post-genesis chain exists
// only from the height its tx landed, so a node syncing from genesis has a
// window with no custody state, and the chain's very existence depends on an
// operator action that can be forgotten or fumbled. Bridged funds should not
// depend on that.
func TestMChainIsAGenesisChain(t *testing.T) {
for _, networkID := range []uint32{
constants.MainnetID,
constants.TestnetID,
constants.LocalID,
} {
cfg := GetConfig(networkID)
require.NotNilf(t, cfg, "network %d has no genesis config", networkID)
require.NotEmptyf(t, cfg.MChainGenesis,
"network %d carries no mchain.json, so the builder skips M-Chain entirely", networkID)
raw, _, err := FromConfig(cfg)
require.NoErrorf(t, err, "network %d genesis build", networkID)
parsed, err := genesis.Parse(raw)
require.NoErrorf(t, err, "network %d genesis parse", networkID)
// Genesis chains are CreateChainTx entries executed at height 0 — the
// chain exists from the first block, without anyone submitting a tx.
var found *pchaintxs.CreateChainTx
for _, c := range parsed.Chains {
u, ok := c.Unsigned.(*pchaintxs.CreateChainTx)
require.True(t, ok, "a genesis chain entry must be a CreateChainTx")
if u.VMID() == constants.MPCVMID {
found = u
break
}
}
require.NotNilf(t, found,
"network %d: no genesis chain with vmID %s (M-Chain)", networkID, constants.MPCVMID)
require.Equal(t, "M-Chain", found.BlockchainName())
require.Equalf(t, []byte(cfg.MChainGenesis), found.GenesisData(),
"network %d: the VM must receive exactly the mchain.json bytes", networkID)
require.Equalf(t, canonicalMPCVMID, found.VMID().String(),
"network %d: genesis declares a vmID the plugin registry will look up by filename", networkID)
}
}
// The genesis blob decides how many custodians must cooperate to move bridged
// funds, so it must state that in a form nothing can misread as a polynomial
// degree. A bare `"threshold": 3` reads as the signer count to an operator and
// as the degree to every threshold library, and those differ by one.
func TestMChainGenesisPolicyIsUnambiguousAndDeployable(t *testing.T) {
for _, tc := range []struct {
networkID uint32
want string
}{
{constants.MainnetID, "7-of-10"},
{constants.TestnetID, "3-of-5"},
{constants.LocalID, "3-of-5"},
} {
cfg := GetConfig(tc.networkID)
require.NotNil(t, cfg)
var blob struct {
Policy string `json:"policy"`
VM string `json:"vm"`
}
require.NoErrorf(t, json.Unmarshal([]byte(cfg.MChainGenesis), &blob),
"network %d mchain.json must decode, policy included", tc.networkID)
require.Equalf(t, tc.want, blob.Policy, "network %d policy", tc.networkID)
k, n := parsePolicy(t, blob.Policy)
require.Greaterf(t, 2*k, n,
"network %d: %s admits two disjoint quorums, so two halves of the committee could authorise contradictory releases",
tc.networkID, blob.Policy)
require.Equalf(t, "mpcvm", blob.VM,
"network %d must name the current VM, not the retired ThresholdVM", tc.networkID)
}
}
// The ambiguous fields must be gone, not merely ignored. Leaving them in place
// means the next reader has two numbers to choose between, and the whole point
// of the policy field is that there is exactly one.
func TestMChainGenesisHasNoAmbiguousThresholdFields(t *testing.T) {
for _, networkID := range []uint32{constants.MainnetID, constants.TestnetID, constants.LocalID} {
cfg := GetConfig(networkID)
require.NotNil(t, cfg)
var blob map[string]any
require.NoError(t, json.Unmarshal([]byte(cfg.MChainGenesis), &blob))
for _, dead := range []string{"mpcThreshold", "mpcParties", "threshold", "totalParties"} {
require.NotContainsf(t, blob, dead,
"network %d mchain.json still carries %q; the policy field is the only quorum statement",
networkID, dead)
}
}
}
+2 -1
View File
@@ -126,7 +126,7 @@ require (
github.com/luxfi/constants v1.6.2
github.com/luxfi/container v0.2.1
github.com/luxfi/filesystem v0.0.1
github.com/luxfi/genesis v1.16.2
github.com/luxfi/genesis v1.16.3
github.com/luxfi/genesis/pkg/genesis/security v1.13.8
github.com/luxfi/geth v1.20.1
github.com/luxfi/go-bip39 v1.2.0
@@ -196,6 +196,7 @@ require (
github.com/luxfi/dkg v0.3.5 // indirect
github.com/luxfi/lattice/v7 v7.1.4 // indirect
github.com/luxfi/lens v0.2.1 // indirect
github.com/luxfi/light v1.0.0 // indirect
github.com/luxfi/magnetar v1.2.3 // indirect
github.com/luxfi/mdns v0.1.1 // indirect
github.com/luxfi/mlwe v0.3.0 // indirect
+4 -4
View File
@@ -329,16 +329,14 @@ github.com/luxfi/crypto/ipa v1.2.4 h1:6xfwhI9/HrcDkF3Ti5/NxsNQIWbwYDJmRSNIHRQ/xf
github.com/luxfi/crypto/ipa v1.2.4/go.mod h1:43J6f6rcfUMrZt4cQectMOZb6Ps+fAEj8ZTPC3Kk+gE=
github.com/luxfi/database v1.21.1 h1:GNnoWVa82l+n2dK7x2aG8LR2NEToq6ZCRX0sQjmK0OM=
github.com/luxfi/database v1.21.1/go.mod h1:Gc7Z2OPrrcYLnAL8B1trOnguXauOlSDV5tkviLN6Xec=
github.com/luxfi/dex v1.14.0 h1:4PtorVbRmbD73S6YWPvgp5GRCb9jeuaedl7mo1CbzCI=
github.com/luxfi/dex v1.14.0/go.mod h1:wYWQmwospvdKBWQ6OJMXb8I+kfgEtE46R1rD8H7vHCQ=
github.com/luxfi/dkg v0.3.5 h1:s2L2mMQaz+n9m0b0ghvoV5VZNxiwb2z4WrGugvK0udY=
github.com/luxfi/dkg v0.3.5/go.mod h1:M+WH7GFRN+YUD851Rlnumdp0Md98kplNN8pVx65U8I8=
github.com/luxfi/filesystem v0.0.1 h1:VZ6xMFKaAPBW/ddlMsDnI2G0VU1lV5rYaVcW5d+KwEY=
github.com/luxfi/filesystem v0.0.1/go.mod h1:OQVSU6XNwqrr1AI+MqkID2taHUclx7NYmmr3svgttec=
github.com/luxfi/formatting v1.1.1 h1:MJhVXIPh1dbysvYEjtaEA/Z0FUTiI7n0DwOF54FS08c=
github.com/luxfi/formatting v1.1.1/go.mod h1:zhBWp6fLZduhpiAdPgVDdPVOyhw4FvwRUksF6+xKQCE=
github.com/luxfi/genesis v1.16.2 h1:OLQg5+ln8qgORBYnJuQsZxar8AfZlsXg5g/f95AraPI=
github.com/luxfi/genesis v1.16.2/go.mod h1:piaSqJY80eVpgov8DUWQXll9IdlCroWgvhnwC7/3lTA=
github.com/luxfi/genesis v1.16.3 h1:P9/ixEOTC9yVvhhcRT3femRMMVIQxKINet1X1GwE4tQ=
github.com/luxfi/genesis v1.16.3/go.mod h1:0F6hV6GfwDSmIWsueB7/vqPZFxyS7A4Jo3p1QU87fFc=
github.com/luxfi/genesis/pkg/genesis/security v1.13.8 h1:9ier0p55ErSpoXHRJpZ04WV5HwwMB1uDrU7PHGBKG2U=
github.com/luxfi/genesis/pkg/genesis/security v1.13.8/go.mod h1:DzU+GYUFv12ja4Vc46bWKNBBmNYbcow3u/DASx4wpfI=
github.com/luxfi/geth v1.20.1 h1:QUGQr4AKvADjwMi7t8a0OfoyxShgEcI9pwie1jFYfm0=
@@ -357,6 +355,8 @@ github.com/luxfi/lattice/v7 v7.1.4 h1:hQR02M6cHTAV5+joOPi9gb9Gm+z/hKJnhJF4IlciIJ
github.com/luxfi/lattice/v7 v7.1.4/go.mod h1:DmIQFi3mJiehVsR235l1NKYEU0JhU649OX5p7gMEW2c=
github.com/luxfi/lens v0.2.1 h1:5Qd0GdjbM+XUVgwDbZ452tKkR7yeE8QnBTHHaH8fJNY=
github.com/luxfi/lens v0.2.1/go.mod h1:6FIhC8weEE5RbNMF3SaE+XPSB9cr6FmjypYBoHkz4JQ=
github.com/luxfi/light v1.0.0 h1:zTJgp5M0xX8rIwRjYDQgOGhLas3rgXyhkszrTX+ne3s=
github.com/luxfi/light v1.0.0/go.mod h1:1G0kgjEe/srlBMCIrFq4IvhnrMuHKFG18CRUWfw1z30=
github.com/luxfi/log v1.4.3 h1:xkUKRWvQ4ZwvlUC2e0/RTtHYZOYSMvSQ9W9lbjwBmiI=
github.com/luxfi/log v1.4.3/go.mod h1:myIkufyiQomSQH34K981kbz6cG4WUoerRUh7F4XhlQI=
github.com/luxfi/magnetar v1.2.3 h1:n4UrJZLK+mhDDZr1HLl2H/KgA6o6v62r5oiC61R7awE=
+1 -1
View File
@@ -60,7 +60,7 @@ PLUGINS=(
r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS # oraclevm
ry9Sg8rZdT26iEKvJDmC2wkESs4SDKgZEhk5BgLSwg1EpcNug # quantumvm
sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz # relayvm
tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t # mpcvm
qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS # mpcvm
vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9 # zkvm
)