mirror of
https://github.com/luxfi/genesis.git
synced 2026-07-27 05:54:08 +00:00
genesis: pin ChainSecurityProfile into Config + Resolve at load (closes F102 genesis layer)
Adds SecurityProfile to genesis.Config and ConfigOutput as an optional
pin-by-ID + pin-by-hash for the chain-wide ChainSecurityProfile. JSON
shape:
"securityProfile": {
"profileID": 1,
"profileHashHex": "<96 hex chars of SHA3-384 ComputeHash>"
}
SecurityProfile.Resolve() loads the canonical profile via
consensus/config.ProfileByID, runs Validate(), recomputes ComputeHash,
and refuses on any mismatch — a forked binary that swaps in a different
canonical profile content fails genesis boot.
This closes the genesis half of red-team F102: previously this package
never imported consensus/config.ChainSecurityProfile so no genesis
file could enforce the locked profile. The node-side wiring lands in
the follow-up commit.
Bumps luxfi/consensus v1.22.63 → v1.23.5 (the version that ships
ProfileByID, ProfileHash binding, and the closures for F96/F100/F101/
F107/F109).
Adds 6 regression tests for the load + JSON round-trip path.
This commit is contained in:
@@ -10,7 +10,7 @@ require (
|
||||
github.com/luxfi/go-bip39 v1.1.2
|
||||
github.com/luxfi/ids v1.2.9
|
||||
github.com/luxfi/log v1.4.1
|
||||
github.com/luxfi/math v1.2.3
|
||||
github.com/luxfi/math v1.4.0
|
||||
github.com/luxfi/node v1.23.1
|
||||
github.com/luxfi/tls v1.0.3
|
||||
github.com/luxfi/utxo v0.2.4
|
||||
@@ -26,7 +26,7 @@ require (
|
||||
github.com/luxfi/atomic v1.0.0 // indirect
|
||||
github.com/luxfi/compress v0.0.5 // indirect
|
||||
github.com/luxfi/concurrent v0.0.3 // indirect
|
||||
github.com/luxfi/database v1.17.40 // indirect
|
||||
github.com/luxfi/database v1.18.3 // indirect
|
||||
github.com/luxfi/keychain v1.0.2 // indirect
|
||||
github.com/luxfi/ordering v0.0.1 // indirect
|
||||
github.com/luxfi/runtime v1.0.1 // indirect
|
||||
@@ -47,11 +47,11 @@ require (
|
||||
github.com/luxfi/address v1.0.1
|
||||
github.com/luxfi/cache v1.2.1 // indirect
|
||||
github.com/luxfi/codec v1.1.4 // indirect
|
||||
github.com/luxfi/consensus v1.22.63 // indirect
|
||||
github.com/luxfi/consensus v1.23.5 // indirect
|
||||
github.com/luxfi/formatting v1.0.1
|
||||
github.com/luxfi/geth v1.16.73 // indirect
|
||||
github.com/luxfi/math/big v0.1.0 // indirect
|
||||
github.com/luxfi/metric v1.4.12 // indirect
|
||||
github.com/luxfi/metric v1.5.0 // indirect
|
||||
github.com/luxfi/mock v0.1.1 // indirect
|
||||
github.com/luxfi/p2p v1.18.9 // indirect
|
||||
github.com/luxfi/protocol v0.0.3
|
||||
|
||||
@@ -91,6 +91,8 @@ github.com/luxfi/concurrent v0.0.3 h1:eJyv1fhaC0jMLMw6+QS774cUmp7GK+ouMgvLCqnC7c
|
||||
github.com/luxfi/concurrent v0.0.3/go.mod h1:Aj/FR5NpM0cB2P4Nt3+tz9+dV6V+LUW4HuMgSjwq5hw=
|
||||
github.com/luxfi/consensus v1.22.63 h1:1oRAOvSjfeC5IwDkWHgFTHflQFwhCNYItkfh19UmzZk=
|
||||
github.com/luxfi/consensus v1.22.63/go.mod h1:PwMubdgQloJiYQPawTTJGhsaC4Ve3/BjCmNxTDjYdGg=
|
||||
github.com/luxfi/consensus v1.23.5 h1:bfEukqgDLOM1/NouHcVES2nxlbwrIKKZYgXyBXNJVCQ=
|
||||
github.com/luxfi/consensus v1.23.5/go.mod h1:YZfPqGx7NXlnd1x+RdKgV+TrtRPhUz4kdSQqpZoVDh8=
|
||||
github.com/luxfi/constants v1.4.6 h1:9a/ED2t/sRfeOfvNur2lssyjaw2I7tSyddTJHVSDYWg=
|
||||
github.com/luxfi/constants v1.4.6/go.mod h1:hOszZ2NDQ8gMZKncfcZ67PXkb5OIbnwAzXC3oFbQwW0=
|
||||
github.com/luxfi/container v0.0.4 h1:BXhF82WyfqVP5mjlNcr7tP0Fcnvl0Ap1rkiu+rq5XuM=
|
||||
@@ -99,6 +101,8 @@ github.com/luxfi/crypto v1.18.4 h1:n8/Udhv0RUW7j1ooEKflWnCNWDaunAsWrDXBHvHLLo4=
|
||||
github.com/luxfi/crypto v1.18.4/go.mod h1:h4LZbaHs3EX6t5sh+psT1JFrLRloTP8PfWSvGNFYtT8=
|
||||
github.com/luxfi/database v1.17.40 h1:RPtRLbKu5yBM5fCSjR7CWzNqR0N1fg8r9y73hUww5sM=
|
||||
github.com/luxfi/database v1.17.40/go.mod h1:du1q0PemBc0gZcdtHhsARs6mIsZM3a5R/C040DVYLCM=
|
||||
github.com/luxfi/database v1.18.3 h1:gg+xwhKUxXa7fDoOD8IS91E71QqoEtcDCl2nfS61Jgg=
|
||||
github.com/luxfi/database v1.18.3/go.mod h1:sv0pYCGKlK1aNJTICxFUDpVWCJTigoLlshHmV/1pg7c=
|
||||
github.com/luxfi/formatting v1.0.1 h1:ZnE1rAdEUds9yAegdVdGDOBGN6hLMPOv6E03Fp8IEYo=
|
||||
github.com/luxfi/formatting v1.0.1/go.mod h1:mYzNf5DJOiqSSKUPzNj5dKy4tstFbN3pZlkI5716eKc=
|
||||
github.com/luxfi/geth v1.16.73 h1:jbRaWQDXTadZ+fEh5Cd5jnkv1qq4yAoT5bVQFuql8ZU=
|
||||
@@ -115,10 +119,14 @@ github.com/luxfi/log v1.4.1 h1:rIfFRodb9jrD/w7KayaUk0Oc+37PaQQdKEEMJCjR8gw=
|
||||
github.com/luxfi/log v1.4.1/go.mod h1:64IE3xRMJcpkQwnPUfJw3pDj7wU0kRS7BZ9wM7R72jk=
|
||||
github.com/luxfi/math v1.2.3 h1:BgvIFw/srPXFLbcqtoDhLJOfmBsn86GPA1iWgsoyUb4=
|
||||
github.com/luxfi/math v1.2.3/go.mod h1:C8STnF2H+D6rqBPt248CiWY2TGuJgdtv/+4UqrT15iM=
|
||||
github.com/luxfi/math v1.4.0 h1:/sb7Grw3hfO+5INWAWdB95jTvCeXg8fSQxsxDzcFtd4=
|
||||
github.com/luxfi/math v1.4.0/go.mod h1:iW0FOCC8qF2mPE+MakG780CAHA83848lb1L04thA1Pg=
|
||||
github.com/luxfi/math/big v0.1.0 h1:Vz4c0RsZVPdIKPsHPgAJChH/R3p15WHRUz7LkLf+NIQ=
|
||||
github.com/luxfi/math/big v0.1.0/go.mod h1:BuxSu22RbO93xBLk5Eam5nldFponoJ73xDFz4uJ3Huk=
|
||||
github.com/luxfi/metric v1.4.12 h1:rdT1zdG3qsWjTTZprfPZoqZxXx8ZYWNwq2UYe0c70Y8=
|
||||
github.com/luxfi/metric v1.4.12/go.mod h1:EAFmkzd5EX654G48HXKIsyJufOUa4sxmOJKvwSRbjng=
|
||||
github.com/luxfi/metric v1.5.0 h1:11PKOjH6ntnnPF5GpspXFAqoKG9raqxT+zW5mN2jztI=
|
||||
github.com/luxfi/metric v1.5.0/go.mod h1:PkD4D4JoGuyKtfUkqPNYkrg9xKrJeNVZFdW/5XvAe/A=
|
||||
github.com/luxfi/mock v0.1.1 h1:0HEtIjg1J6CWz+IUyP6rsGqNWTcmxjFnSQIhaDuARwY=
|
||||
github.com/luxfi/mock v0.1.1/go.mod h1:jo35akl3Vtd8LbzDts8VJ0jmSVycrd1/eBi6g6t5hKU=
|
||||
github.com/luxfi/node v1.23.1 h1:bfpd9udWuDo6+TPB6Un6d96kM0yr7QU15Wwa2N1oQZI=
|
||||
@@ -179,6 +187,7 @@ github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo=
|
||||
@@ -201,6 +210,7 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
|
||||
@@ -107,6 +107,7 @@ func GetConfigFile(filepath string) (*Config, error) {
|
||||
ZChainGenesis: output.ZChainGenesis,
|
||||
GChainGenesis: output.GChainGenesis,
|
||||
KChainGenesis: output.KChainGenesis,
|
||||
SecurityProfile: output.SecurityProfile,
|
||||
Message: output.Message,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package genesis
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
consensusconfig "github.com/luxfi/consensus/config"
|
||||
)
|
||||
|
||||
// security_profile.go — genesis-level binding of the chain-wide
|
||||
// ChainSecurityProfile. Closes red-team finding F102: previously
|
||||
// neither this package nor luxfi/node imported the locked profile,
|
||||
// so the profile was pure documentation that no consumer checked.
|
||||
//
|
||||
// Wire form is pin-by-ID + pin-by-hash:
|
||||
// - ProfileID names the canonical ChainSecurityProfile (e.g. 0x01 =
|
||||
// LuxStrictPQ).
|
||||
// - ProfileHashHex is the 48-byte SHA3-384 ComputeHash of the
|
||||
// canonical profile, hex-encoded. The genesis loader recomputes
|
||||
// the hash at boot from consensusconfig.ProfileByID(ProfileID) and
|
||||
// refuses to start if the hex does not match. Any drift in the
|
||||
// canonical profile content invalidates every prior genesis that
|
||||
// pinned its hash.
|
||||
//
|
||||
// One-way load: deserialise → resolve → validate → ComputeHash →
|
||||
// constant-time compare. There is no upgrade path that re-derives the
|
||||
// hash from the pinned ID alone; that would defeat the lock.
|
||||
|
||||
// SecurityProfile is the genesis-level pin for a chain's locked
|
||||
// ChainSecurityProfile. ProfileID identifies the canonical row in the
|
||||
// consensusconfig registry; ProfileHashHex pins the expected content
|
||||
// hash. Both fields are mandatory on a profile-bound genesis.
|
||||
type SecurityProfile struct {
|
||||
// ProfileID is the wire byte that names the canonical profile.
|
||||
// 0x01 = LuxStrictPQ, 0x02 = LuxPermissive, 0x03 = LuxFIPS.
|
||||
// 0x80+ is reserved for downstream / white-label profiles (which
|
||||
// must register with the consensus team to obtain a byte).
|
||||
ProfileID uint8 `json:"profileID"`
|
||||
|
||||
// ProfileHashHex is the SHA3-384 ComputeHash of the canonical
|
||||
// ChainSecurityProfile at the time genesis was sealed, hex-encoded
|
||||
// (96 hex chars, no 0x prefix). The genesis loader rejects a
|
||||
// startup whose live ComputeHash does not match.
|
||||
ProfileHashHex string `json:"profileHashHex"`
|
||||
}
|
||||
|
||||
// SecurityProfileLoadErr is returned by Resolve when the pin fails to
|
||||
// resolve to a known canonical profile or its content hash mismatches.
|
||||
var (
|
||||
ErrSecurityProfileMissing = errors.New("genesis: security profile pin is absent on a profile-required genesis")
|
||||
ErrSecurityProfileInvalidID = errors.New("genesis: SecurityProfile.ProfileID is not a known consensus profile")
|
||||
ErrSecurityProfileInvalidHashHex = errors.New("genesis: SecurityProfile.ProfileHashHex is not 48 bytes of hex")
|
||||
ErrSecurityProfileValidateFailed = errors.New("genesis: canonical profile failed Validate() at boot")
|
||||
ErrSecurityProfileHashMismatch = errors.New("genesis: live ComputeHash() does not match genesis-pinned ProfileHashHex")
|
||||
ErrSecurityProfileComputeHashFail = errors.New("genesis: canonical profile ComputeHash() returned error")
|
||||
)
|
||||
|
||||
// Resolve loads the canonical ChainSecurityProfile named by ProfileID,
|
||||
// validates it, computes its hash, and confirms the hash matches the
|
||||
// genesis pin. Returns the canonical profile on success.
|
||||
//
|
||||
// This is the single load-and-verify entry point. Every consumer of
|
||||
// the locked profile (node bootstrap, signer registration, peer-cert
|
||||
// gate, mempool, validator registry, bridge oracle) calls Resolve once
|
||||
// at startup and threads the returned *ChainSecurityProfile through
|
||||
// the rest of the process — no re-resolution per request.
|
||||
//
|
||||
// The constant-time hash comparison is intentional: a runtime mutation
|
||||
// of the canonical profile that produces a different hash MUST fail
|
||||
// the genesis pin, and we don't want to leak which byte of the hash
|
||||
// matched via an early-exit byte loop. (Hash comparisons are not
|
||||
// secrets, but consistency with the rest of the security gate is
|
||||
// worth more than the four-line shortcut.)
|
||||
func (s *SecurityProfile) Resolve() (*consensusconfig.ChainSecurityProfile, error) {
|
||||
if s == nil {
|
||||
return nil, ErrSecurityProfileMissing
|
||||
}
|
||||
|
||||
pinned, err := hex.DecodeString(s.ProfileHashHex)
|
||||
if err != nil || len(pinned) != 48 {
|
||||
return nil, fmt.Errorf("%w: have %d bytes (%v)", ErrSecurityProfileInvalidHashHex, len(pinned), err)
|
||||
}
|
||||
|
||||
profile, err := consensusconfig.ProfileByID(consensusconfig.ProfileID(s.ProfileID))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: 0x%02x: %v", ErrSecurityProfileInvalidID, s.ProfileID, err)
|
||||
}
|
||||
if err := profile.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrSecurityProfileValidateFailed, err)
|
||||
}
|
||||
live, err := profile.ComputeHash()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrSecurityProfileComputeHashFail, err)
|
||||
}
|
||||
if !constantTimeEqual48(live[:], pinned) {
|
||||
return nil, fmt.Errorf("%w: pinned=%s live=%x",
|
||||
ErrSecurityProfileHashMismatch, s.ProfileHashHex, live[:])
|
||||
}
|
||||
// Stamp the live hash so consumers always see it on the struct.
|
||||
profile.ProfileHash = live
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
// constantTimeEqual48 returns true iff the two 48-byte hashes are equal,
|
||||
// in constant time over the byte slice contents. Subtle.ConstantTimeCompare
|
||||
// is generic over byte slices but pinned to 48 bytes here so a length
|
||||
// mismatch is caught explicitly above.
|
||||
func constantTimeEqual48(a, b []byte) bool {
|
||||
if len(a) != 48 || len(b) != 48 {
|
||||
return false
|
||||
}
|
||||
var v byte
|
||||
for i := 0; i < 48; i++ {
|
||||
v |= a[i] ^ b[i]
|
||||
}
|
||||
return v == 0
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package genesis
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
consensusconfig "github.com/luxfi/consensus/config"
|
||||
)
|
||||
|
||||
// TestSecurityProfile_Resolve_LuxStrictPQ proves a genesis pin for the
|
||||
// canonical LuxStrictPQ profile resolves, validates, and round-trips
|
||||
// through ComputeHash without mutation. Closes F102 at the genesis
|
||||
// load layer: the profile that ships in consensus/config is reachable
|
||||
// from the genesis package and its content hash is checked.
|
||||
func TestSecurityProfile_Resolve_LuxStrictPQ(t *testing.T) {
|
||||
canonical := consensusconfig.LuxStrictPQ()
|
||||
live, err := canonical.ComputeHash()
|
||||
if err != nil {
|
||||
t.Fatalf("LuxStrictPQ().ComputeHash() returned error: %v", err)
|
||||
}
|
||||
|
||||
pin := &SecurityProfile{
|
||||
ProfileID: uint8(consensusconfig.ProfileLuxStrictPQ),
|
||||
ProfileHashHex: hex.EncodeToString(live[:]),
|
||||
}
|
||||
got, err := pin.Resolve()
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() returned error: %v", err)
|
||||
}
|
||||
if got == nil {
|
||||
t.Fatalf("Resolve() returned nil profile")
|
||||
}
|
||||
if got.ProfileID != uint32(consensusconfig.ProfileLuxStrictPQ) {
|
||||
t.Errorf("Resolve() returned profile with ProfileID=%d; want %d",
|
||||
got.ProfileID, consensusconfig.ProfileLuxStrictPQ)
|
||||
}
|
||||
if got.ProfileHash != live {
|
||||
t.Errorf("Resolve() did not stamp the live hash onto the returned profile")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSecurityProfile_Resolve_HashMismatchRejected proves the pin
|
||||
// refuses a profile whose live hash diverges from the pinned hex.
|
||||
// This is the load-bearing F102 anti-regression: a forked binary that
|
||||
// swaps in a different canonical profile content must fail genesis
|
||||
// boot.
|
||||
func TestSecurityProfile_Resolve_HashMismatchRejected(t *testing.T) {
|
||||
// Pin a wrong hash for LuxStrictPQ.
|
||||
pin := &SecurityProfile{
|
||||
ProfileID: uint8(consensusconfig.ProfileLuxStrictPQ),
|
||||
ProfileHashHex: strings.Repeat("00", 48), // 96 hex zeros
|
||||
}
|
||||
_, err := pin.Resolve()
|
||||
if err == nil {
|
||||
t.Fatal("Resolve() accepted a wrong-hash pin")
|
||||
}
|
||||
if !errors.Is(err, ErrSecurityProfileHashMismatch) {
|
||||
t.Errorf("Resolve() returned %v; want ErrSecurityProfileHashMismatch", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSecurityProfile_Resolve_UnknownProfileIDRejected proves the
|
||||
// pin refuses an unknown ProfileID byte.
|
||||
func TestSecurityProfile_Resolve_UnknownProfileIDRejected(t *testing.T) {
|
||||
pin := &SecurityProfile{
|
||||
ProfileID: 0xFE, // unregistered
|
||||
ProfileHashHex: strings.Repeat("ab", 48),
|
||||
}
|
||||
_, err := pin.Resolve()
|
||||
if err == nil {
|
||||
t.Fatal("Resolve() accepted an unknown ProfileID")
|
||||
}
|
||||
if !errors.Is(err, ErrSecurityProfileInvalidID) {
|
||||
t.Errorf("Resolve() returned %v; want ErrSecurityProfileInvalidID", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSecurityProfile_Resolve_InvalidHashHexRejected proves
|
||||
// non-hex / wrong-length hash strings are refused.
|
||||
func TestSecurityProfile_Resolve_InvalidHashHexRejected(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"empty": "",
|
||||
"short": "abcd",
|
||||
"odd": strings.Repeat("a", 95),
|
||||
"too long": strings.Repeat("ab", 49),
|
||||
"not hex": strings.Repeat("zz", 48),
|
||||
"wrong padding": "0x" + strings.Repeat("ab", 48), // hex.DecodeString refuses 0x prefix
|
||||
}
|
||||
for name, hashHex := range cases {
|
||||
pin := &SecurityProfile{
|
||||
ProfileID: uint8(consensusconfig.ProfileLuxStrictPQ),
|
||||
ProfileHashHex: hashHex,
|
||||
}
|
||||
_, err := pin.Resolve()
|
||||
if err == nil {
|
||||
t.Errorf("%s: Resolve() accepted an invalid hash hex %q", name, hashHex)
|
||||
continue
|
||||
}
|
||||
if !errors.Is(err, ErrSecurityProfileInvalidHashHex) {
|
||||
t.Errorf("%s: Resolve() returned %v; want ErrSecurityProfileInvalidHashHex", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSecurityProfile_Resolve_NilRejected proves a missing pin is
|
||||
// detected at the API boundary.
|
||||
func TestSecurityProfile_Resolve_NilRejected(t *testing.T) {
|
||||
var pin *SecurityProfile
|
||||
_, err := pin.Resolve()
|
||||
if !errors.Is(err, ErrSecurityProfileMissing) {
|
||||
t.Errorf("nil pin: Resolve() returned %v; want ErrSecurityProfileMissing", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfig_SecurityProfile_JSONRoundtrip proves the SecurityProfile
|
||||
// field survives a Config → ConfigOutput → JSON → ConfigOutput round
|
||||
// trip. Closes the wire-form half of F102.
|
||||
func TestConfig_SecurityProfile_JSONRoundtrip(t *testing.T) {
|
||||
canonical := consensusconfig.LuxStrictPQ()
|
||||
live, err := canonical.ComputeHash()
|
||||
if err != nil {
|
||||
t.Fatalf("ComputeHash: %v", err)
|
||||
}
|
||||
pin := &SecurityProfile{
|
||||
ProfileID: uint8(consensusconfig.ProfileLuxStrictPQ),
|
||||
ProfileHashHex: hex.EncodeToString(live[:]),
|
||||
}
|
||||
|
||||
out := &ConfigOutput{
|
||||
NetworkID: 12345,
|
||||
Message: "test",
|
||||
SecurityProfile: pin,
|
||||
}
|
||||
raw, err := json.Marshal(out)
|
||||
if err != nil {
|
||||
t.Fatalf("json.Marshal: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(raw), `"securityProfile"`) {
|
||||
t.Fatalf("marshalled output missing securityProfile field: %s", raw)
|
||||
}
|
||||
|
||||
var got ConfigOutput
|
||||
if err := json.Unmarshal(raw, &got); err != nil {
|
||||
t.Fatalf("json.Unmarshal: %v", err)
|
||||
}
|
||||
if got.SecurityProfile == nil {
|
||||
t.Fatal("SecurityProfile was nil after round-trip")
|
||||
}
|
||||
if got.SecurityProfile.ProfileID != pin.ProfileID {
|
||||
t.Errorf("ProfileID = %d; want %d", got.SecurityProfile.ProfileID, pin.ProfileID)
|
||||
}
|
||||
if got.SecurityProfile.ProfileHashHex != pin.ProfileHashHex {
|
||||
t.Errorf("ProfileHashHex = %q; want %q", got.SecurityProfile.ProfileHashHex, pin.ProfileHashHex)
|
||||
}
|
||||
if _, err := got.SecurityProfile.Resolve(); err != nil {
|
||||
t.Errorf("Resolve() after round-trip returned %v", err)
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,17 @@ type Config struct {
|
||||
// This allows chains (P/X/C/Q/etc.) to have network-specific IDs that
|
||||
// can be migrated/upgraded over time via governance.
|
||||
ChainMapping *ChainMapping `json:"chainMapping,omitempty"`
|
||||
|
||||
// SecurityProfile is the genesis-level pin for this chain's locked
|
||||
// ChainSecurityProfile (consensus/config). Carries the ProfileID
|
||||
// byte plus the SHA3-384 content hash of the canonical profile.
|
||||
// luxfi/node loads this at startup and refuses to boot if the live
|
||||
// canonical profile content doesn't match the pinned hash.
|
||||
//
|
||||
// Optional in JSON so legacy genesis files keep parsing; node
|
||||
// bootstrap warns + refuses strict-PQ features when absent.
|
||||
// Closes red-team finding F102.
|
||||
SecurityProfile *SecurityProfile `json:"securityProfile,omitempty"`
|
||||
}
|
||||
|
||||
// Allocation represents a genesis allocation
|
||||
@@ -265,6 +276,7 @@ type ConfigOutput struct {
|
||||
GChainGenesis string `json:"gChainGenesis,omitempty"` // G-Chain: Graph VM genesis
|
||||
KChainGenesis string `json:"kChainGenesis,omitempty"` // K-Chain: KMS VM genesis
|
||||
Chains []ChainEntry `json:"chains,omitempty"` // Additional genesis chains
|
||||
SecurityProfile *SecurityProfile `json:"securityProfile,omitempty"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
@@ -320,6 +332,7 @@ func (c *Config) ToJSON(hrp string) *ConfigOutput {
|
||||
ZChainGenesis: c.ZChainGenesis,
|
||||
GChainGenesis: c.GChainGenesis,
|
||||
KChainGenesis: c.KChainGenesis,
|
||||
SecurityProfile: c.SecurityProfile,
|
||||
Message: c.Message,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user