mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
Kills node/vms/pcodecs from the M-Chain (MPC custody VM) — the LAST of the three app-chain VMs blocking pcodecs deletion. bridgevm + zkvm already native. Codec kill (wire.go): the legacy pcodecs LinearCodec keyed on serialize:"true" tags; every mpcvm type carries only json: tags, so the old codec marshaled ZERO fields — a Block round-tripped to all-zero and every block ID collided at sha256(0x0000). This is a latent corruption bug the native wire FIXES. wire.go owns Marshal/parse for Block (nested []*Operation as u32-length-list + concat sub-object blob), ManagedKey (PartyIDs []party.ID as packed string list; CreatedAt/LastUsedAt as UnixNano; Config/CMPConfig json:"-" excluded), CrossChainMPCRequest, Operation. Block.ID_ is derived (computeID=sha256(Marshal)) so it is excluded from wire; recomputed on ParseBlock, set from DB key on getBlock. ThresholdConfig (init.Config) + Genesis stay JSON at the boundary. Deleted mpcvm/codec.go; swapped 9 call sites. Parse rejects trailing bytes. Round-trips green (Operation/Block/ManagedKey/CrossChainMPCRequest incl empty-ops nil-slice). Threshold signing (sign_distributed.go + transport/attest_bridge): CGGMP21/CMP threshold-ECDSA ceremony across the M committee over native app-gossip; each validator runs its own executor + per-session gossipRouter; honest signers derive the SAME standard secp256k1 signature. B→M attestation seam is domain-bound and replay-safe. Tests: transport-free seam (bridge_seam_test) + full 3-validator in-process fabric driving the real Broadcast→Gossip→router.Deliver path (bridge_transport_test). 3 of 3 chains VMs native. chains repo now has ZERO pcodecs imports. Co-authored-by: Hanzo Dev <[email protected]>
113 lines
4.2 KiB
Go
113 lines
4.2 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package mpcvm
|
|
|
|
// bridge_seam_test.go — fast, transport-free tests of the B→M attestation seam:
|
|
// the digest is domain-bound and replay-safe, and a signature over one transfer
|
|
// verifies for THAT transfer and no other. These use an ordinary secp256k1 key
|
|
// (a threshold signature verifies identically to a single-key one), so they run
|
|
// in -short without the DKG/sign ceremony.
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/luxfi/crypto/secp256k1"
|
|
"github.com/luxfi/threshold/pkg/party"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func sampleTransfer() BridgeTransfer {
|
|
var asset [32]byte
|
|
copy(asset[:], []byte("LUX"))
|
|
var recip [20]byte
|
|
copy(recip[:], []byte("recipient-0xdeadbeef"))
|
|
return BridgeTransfer{
|
|
SrcChainID: 200201,
|
|
DstChainID: 97368,
|
|
Asset: asset,
|
|
Amount: 1_000_000,
|
|
Recipient: recip,
|
|
Nonce: 42,
|
|
}
|
|
}
|
|
|
|
// TestBridgeAttestation_ReplaySafe proves a signature over one transfer's
|
|
// domain-bound digest verifies for that transfer and fails for any transfer
|
|
// that differs in ANY digest-committed field — no replay across routes,
|
|
// amounts, recipients, or nonces.
|
|
func TestBridgeAttestation_ReplaySafe(t *testing.T) {
|
|
priv, err := secp256k1.NewPrivateKey()
|
|
require.NoError(t, err)
|
|
pub := priv.PublicKey().CompressedBytes()
|
|
require.Len(t, pub, 33)
|
|
|
|
bt := sampleTransfer()
|
|
digest := bt.Digest()
|
|
sig, err := priv.SignHash(digest[:])
|
|
require.NoError(t, err)
|
|
|
|
// Correct transfer verifies.
|
|
require.True(t, VerifyBridgeAttestation(pub, bt, sig),
|
|
"signature over the correct transfer must verify")
|
|
|
|
// Every digest-committed field is binding: flip it and the SAME signature
|
|
// must be rejected.
|
|
mutations := map[string]func(BridgeTransfer) BridgeTransfer{
|
|
"srcChainID": func(b BridgeTransfer) BridgeTransfer { b.SrcChainID++; return b },
|
|
"dstChainID": func(b BridgeTransfer) BridgeTransfer { b.DstChainID++; return b },
|
|
"asset": func(b BridgeTransfer) BridgeTransfer { b.Asset[0] ^= 0x01; return b },
|
|
"amount": func(b BridgeTransfer) BridgeTransfer { b.Amount++; return b },
|
|
"recipient": func(b BridgeTransfer) BridgeTransfer { b.Recipient[0] ^= 0x01; return b },
|
|
"nonce": func(b BridgeTransfer) BridgeTransfer { b.Nonce++; return b },
|
|
}
|
|
for field, mutate := range mutations {
|
|
require.Falsef(t, VerifyBridgeAttestation(pub, mutate(bt), sig),
|
|
"signature must NOT verify after mutating %s (replay/rebind)", field)
|
|
}
|
|
|
|
// A truncated / malformed signature is rejected, not panicked on.
|
|
require.False(t, VerifyBridgeAttestation(pub, bt, sig[:10]))
|
|
require.False(t, VerifyBridgeAttestation(nil, bt, sig))
|
|
}
|
|
|
|
// TestBridgeDigest_DomainSeparated proves the digest is stable and that the
|
|
// domain tag is mixed in (a transfer's digest is not the bare field hash),
|
|
// so a bridge attestation can never be replayed as another message M signs.
|
|
func TestBridgeDigest_DomainSeparated(t *testing.T) {
|
|
bt := sampleTransfer()
|
|
d1 := bt.Digest()
|
|
d2 := bt.Digest()
|
|
require.Equal(t, d1, d2, "digest must be deterministic")
|
|
|
|
// Distinct transfers produce distinct digests.
|
|
other := bt
|
|
other.Nonce++
|
|
require.NotEqual(t, bt.Digest(), other.Digest())
|
|
|
|
// The domain separator is registered so QuantumAttestation-style tooling
|
|
// recognises the bridge domain.
|
|
require.Equal(t, []byte(bridgeTransferDomainTag), domainSeparators[DomainBridgeTransfer])
|
|
}
|
|
|
|
// TestCeremonyID_DeterministicAndBinding proves the derived ceremony id lets
|
|
// honest validators converge without an announce round, and separates distinct
|
|
// keys / digests / quorums while being independent of signer ordering.
|
|
func TestCeremonyID_DeterministicAndBinding(t *testing.T) {
|
|
digest := sampleTransfer().Digest()
|
|
signers := []party.ID{"a", "b", "c"}
|
|
|
|
base := ceremonyID("k1", digest[:], signers)
|
|
|
|
// Deterministic and signer-order independent.
|
|
require.Equal(t, base, ceremonyID("k1", digest[:], []party.ID{"c", "a", "b"}))
|
|
|
|
// Bound to key, digest and signer set.
|
|
require.NotEqual(t, base, ceremonyID("k2", digest[:], signers))
|
|
other := sampleTransfer()
|
|
other.Amount++
|
|
od := other.Digest()
|
|
require.NotEqual(t, base, ceremonyID("k1", od[:], signers))
|
|
require.NotEqual(t, base, ceremonyID("k1", digest[:], []party.ID{"a", "b", "d"}))
|
|
}
|