Files
chains/mpcvm/bridge_seam_test.go
zeekayandHanzo Dev d682216d8c mpcvm: native ZAP struct-is-wire + cross-validator threshold signing
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]>
2026-07-12 16:44:17 -07:00

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"}))
}