mirror of
https://github.com/luxfi/genesis.git
synced 2026-07-27 05:54:08 +00:00
pkg/genesis: ChainPrefix type — decomplect chain prefix from HRP
formatBech32WithChain("P", hrp, addr) braided chain identity into bech32
formatting at every call site. Chain prefix (P-/X-) is per-chain metadata;
HRP (lux/test/dev/local) is a per-network parameter — they should be
orthogonal.
Introduce ChainPrefix as a named string type with PChainPrefix / XChainPrefix
constants and a Format(hrp, addr) method that delegates to luxfi/address.
Call sites compose by method invocation: PChainPrefix.Format(hrp, addr[:]).
The deprecated formatBech32WithChain is kept as a thin alias for any
remaining caller; output bytes are identical to the pre-decomplect form.
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package genesis
|
||||
|
||||
import "github.com/luxfi/address"
|
||||
|
||||
// ChainPrefix is the bech32 chain prefix that prefixes the HRP-encoded
|
||||
// portion of a chain-scoped address. P-Chain UTXOs use "P", X-Chain UTXOs
|
||||
// use "X". They are orthogonal to the per-network HRP (lux/test/dev/local):
|
||||
// a prefix identifies the chain, an HRP identifies the network.
|
||||
//
|
||||
// Decomplects chain identity from network parameter at the type level —
|
||||
// callers compose the two by method invocation rather than threading a
|
||||
// magic string through formatter call sites.
|
||||
type ChainPrefix string
|
||||
|
||||
// Canonical chain prefixes. The X-Chain (UTXO Exchange) and P-Chain
|
||||
// (ProtocolVM) share the same 20-byte ShortID space; only the prefix
|
||||
// distinguishes which chain the address binds to.
|
||||
const (
|
||||
PChainPrefix ChainPrefix = "P"
|
||||
XChainPrefix ChainPrefix = "X"
|
||||
)
|
||||
|
||||
// Format builds a chain-scoped bech32 address of the form
|
||||
// "<prefix>-<hrp>1<data><checksum>". The bech32 checksum is computed
|
||||
// over the HRP and address bytes only — the chain prefix is a textual
|
||||
// tag joined with "-" after encoding (see luxfi/address.Format).
|
||||
func (cp ChainPrefix) Format(hrp string, addr []byte) (string, error) {
|
||||
return address.Format(string(cp), hrp, addr)
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package genesis
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/address"
|
||||
"github.com/luxfi/ids"
|
||||
)
|
||||
|
||||
// TestChainPrefixFormat covers the decomplected entry point: ChainPrefix carries
|
||||
// the per-chain identity, hrp carries the per-network identity, and the two
|
||||
// compose at the call site. Output must be byte-identical to upstream
|
||||
// address.Format and to the deprecated formatBech32WithChain alias.
|
||||
func TestChainPrefixFormat(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
prefix ChainPrefix
|
||||
hrp string
|
||||
hex string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "P_mainnet_lux",
|
||||
prefix: PChainPrefix,
|
||||
hrp: "lux",
|
||||
hex: "9011E888251AB053B7bD1cdB598Db4f9DEd94714",
|
||||
expected: "P-lux1jqg73zp9r2c98daarnd4nrd5l80dj3c5eha5fl",
|
||||
},
|
||||
{
|
||||
name: "X_testnet_test",
|
||||
prefix: XChainPrefix,
|
||||
hrp: "test",
|
||||
hex: "9011E888251AB053B7bD1cdB598Db4f9DEd94714",
|
||||
expected: "X-test1jqg73zp9r2c98daarnd4nrd5l80dj3c5644e09",
|
||||
},
|
||||
{
|
||||
name: "P_local",
|
||||
prefix: PChainPrefix,
|
||||
hrp: "local",
|
||||
hex: "9011E888251AB053B7bD1cdB598Db4f9DEd94714",
|
||||
expected: "P-local1jqg73zp9r2c98daarnd4nrd5l80dj3c56acgey",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
raw, err := hex.DecodeString(tc.hex)
|
||||
if err != nil {
|
||||
t.Fatalf("decode hex: %v", err)
|
||||
}
|
||||
|
||||
got, err := tc.prefix.Format(tc.hrp, raw)
|
||||
if err != nil {
|
||||
t.Fatalf("ChainPrefix.Format: %v", err)
|
||||
}
|
||||
if got != tc.expected {
|
||||
t.Errorf("ChainPrefix.Format = %s, want %s", got, tc.expected)
|
||||
}
|
||||
|
||||
// Must match upstream address.Format byte-for-byte.
|
||||
up, err := address.Format(string(tc.prefix), tc.hrp, raw)
|
||||
if err != nil {
|
||||
t.Fatalf("address.Format: %v", err)
|
||||
}
|
||||
if got != up {
|
||||
t.Errorf("ChainPrefix.Format diverges from address.Format:\n got: %s\n want: %s", got, up)
|
||||
}
|
||||
|
||||
// Must match the deprecated alias byte-for-byte.
|
||||
var sid ids.ShortID
|
||||
copy(sid[:], raw)
|
||||
old := formatBech32WithChain(string(tc.prefix), tc.hrp, sid)
|
||||
if got != old {
|
||||
t.Errorf("ChainPrefix.Format diverges from deprecated formatBech32WithChain:\n new: %s\n old: %s", got, old)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestChainPrefixOrthogonal asserts the decomplection invariant: a prefix
|
||||
// instance produces correctly-prefixed output across every HRP, and an HRP
|
||||
// passes through unchanged across every prefix. Prefix and HRP do not braid.
|
||||
func TestChainPrefixOrthogonal(t *testing.T) {
|
||||
addr, err := hex.DecodeString("9011E888251AB053B7bD1cdB598Db4f9DEd94714")
|
||||
if err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
|
||||
prefixes := []ChainPrefix{PChainPrefix, XChainPrefix}
|
||||
hrps := []string{"lux", "test", "local"}
|
||||
|
||||
for _, p := range prefixes {
|
||||
for _, h := range hrps {
|
||||
out, err := p.Format(h, addr)
|
||||
if err != nil {
|
||||
t.Fatalf("%s/%s: %v", p, h, err)
|
||||
}
|
||||
// The output begins with "<prefix>-<hrp>1" — chain and HRP are
|
||||
// independently composed into the textual envelope.
|
||||
want := string(p) + "-" + h + "1"
|
||||
if len(out) < len(want) || out[:len(want)] != want {
|
||||
t.Errorf("ChainPrefix(%q).Format(%q, _) = %q; want prefix %q", p, h, out, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-7
@@ -7,7 +7,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/address"
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/ids"
|
||||
)
|
||||
@@ -340,9 +339,10 @@ func (c *Config) ToJSON(hrp string) *ConfigOutput {
|
||||
|
||||
allocations := make([]AllocationJSON, 0, len(c.Allocations))
|
||||
for _, a := range c.Allocations {
|
||||
utxoAddr, _ := PChainPrefix.Format(hrp, a.UTXOAddr[:])
|
||||
allocations = append(allocations, AllocationJSON{
|
||||
EVMAddr: fmt.Sprintf("0x%s", a.EVMAddr.Hex()),
|
||||
UTXOAddr: formatBech32WithChain("P", hrp, a.UTXOAddr),
|
||||
UTXOAddr: utxoAddr,
|
||||
InitialAmount: a.InitialAmount,
|
||||
UnlockSchedule: a.UnlockSchedule,
|
||||
})
|
||||
@@ -350,14 +350,16 @@ func (c *Config) ToJSON(hrp string) *ConfigOutput {
|
||||
|
||||
stakedFunds := make([]string, 0, len(c.InitialStakedFunds))
|
||||
for _, addr := range c.InitialStakedFunds {
|
||||
stakedFunds = append(stakedFunds, formatBech32WithChain("P", hrp, addr))
|
||||
s, _ := PChainPrefix.Format(hrp, addr[:])
|
||||
stakedFunds = append(stakedFunds, s)
|
||||
}
|
||||
|
||||
stakers := make([]StakerJSON, 0, len(c.InitialStakers))
|
||||
for _, s := range c.InitialStakers {
|
||||
rewardAddr, _ := PChainPrefix.Format(hrp, s.RewardAddress[:])
|
||||
stakers = append(stakers, StakerJSON{
|
||||
NodeID: s.NodeID.String(),
|
||||
RewardAddress: formatBech32WithChain("P", hrp, s.RewardAddress),
|
||||
RewardAddress: rewardAddr,
|
||||
DelegationFee: s.DelegationFee,
|
||||
Signer: s.Signer,
|
||||
PQIdentity: s.PQIdentity,
|
||||
@@ -393,11 +395,17 @@ func (c *Config) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(c.ToJSON(hrp))
|
||||
}
|
||||
|
||||
// formatBech32WithChain formats an address with chain prefix (e.g., "P-lux1...")
|
||||
// formatBech32WithChain formats an address with chain prefix (e.g., "P-lux1...").
|
||||
//
|
||||
// Deprecated: prefer ChainPrefix.Format, which decomplects the per-chain prefix
|
||||
// from the per-network HRP. Kept as a thin alias for callers that haven't yet
|
||||
// migrated; falls back to a hex-encoded form if bech32 encoding fails (cannot
|
||||
// happen for a well-formed 20-byte ShortID — preserved for byte-identical
|
||||
// behavior with the pre-decomplect helper).
|
||||
func formatBech32WithChain(chainID, hrp string, addr ids.ShortID) string {
|
||||
bech32Addr, err := address.FormatBech32(hrp, addr[:])
|
||||
out, err := ChainPrefix(chainID).Format(hrp, addr[:])
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%s-%s1%s", chainID, hrp, addr.Hex())
|
||||
}
|
||||
return fmt.Sprintf("%s-%s", chainID, bech32Addr)
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user