Documentation
¶
Overview ¶
Package bridgeattest is the ONE canonical definition of the bridge-transfer attestation seam between B-Chain (bridgevm) and M-Chain (mpcvm).
M threshold-signs a domain-bound bridge-transfer digest; B (and every EVM gateway holding the group key's address) verifies that signature before it mints/releases. M owns the signature; B owns what the message authorises. The signature is a standard secp256k1 ECDSA signature, so anyone holding the group key verifies it with zero interaction — no callback to M.
This package is deliberately dependency-free (stdlib + luxfi/crypto/secp256k1 only). It is a value seam, not a place: the digest is a pure function of the transfer's field values, so it lives in one small package that both the M-Chain VM and the B-Chain VM import — never duplicated, never able to drift.
The digest layout, field order, domain tag, and signature encoding are FROZEN. The on-chain Solidity BridgeGateway (chains/bridgevm/contracts) recomputes the exact same sha256 preimage; the KAT vector in attest_test.go pins the bytes so any drift on any of the three surfaces (M Go, B Go, Solidity) trips a test.
Index ¶
Constants ¶
const DomainTag = "LUX_BRIDGE_TRANSFER_v1"
DomainTag is the domain-separation tag hashed into every bridge-transfer digest, so a bridge attestation can never be replayed as any other message M signs (oracle writes, session-complete, epoch beacons, ...). It is written raw (no length prefix) as the first bytes of the preimage.
const SecurityDomainTag = "LUX_SECURITY_CROSSING_v1"
SecurityDomainTag separates a security crossing from every other message M signs. Written raw, as the first bytes of the preimage, exactly as DomainTag is.
Variables ¶
This section is empty.
Functions ¶
func VerifyBridgeAttestation ¶
func VerifyBridgeAttestation(groupPubKey []byte, bt BridgeTransfer, sig []byte) bool
VerifyBridgeAttestation returns true iff sig is a valid ECDSA signature by groupPubKey over the transfer's domain-bound digest. Accepts r||s (64) or r||s||v (65). A threshold ECDSA signature verifies exactly like a single-key one, so there is no interaction with M.
func VerifySecurityAttestation ¶ added in v1.7.42
func VerifySecurityAttestation(groupPubKey []byte, sc SecurityCrossing, sig []byte) bool
VerifySecurityAttestation returns true iff sig is a valid ECDSA signature by groupPubKey over the crossing's domain-bound digest. Accepts r||s (64) or r||s||v (65). A threshold signature verifies exactly like a single-key one, so there is no interaction with M.
Types ¶
type Attestation ¶
type Attestation struct {
Transfer BridgeTransfer `json:"transfer"`
Digest [32]byte `json:"digest"`
Signature []byte `json:"signature"` // secp256k1 r(32)||s(32)||v(1)
GroupPubKey []byte `json:"groupPubKey"` // 33-byte compressed group key
Signers []string `json:"signers"` // party ids of the T+1 quorum
KeyID string `json:"keyId"`
CreatedAt int64 `json:"createdAt"`
}
Attestation is M's threshold signature over a transfer, plus the context B needs to verify it. Self-describing so B (or a relayer) can verify without re-querying M.
func (*Attestation) VerifyAgainst ¶ added in v1.7.29
func (a *Attestation) VerifyAgainst(expectedGroupKey []byte) bool
VerifyAgainst returns true iff the attestation's signature is a valid threshold signature, by the EXPECTED group key, over this transfer's domain-bound digest, and the self-reported digest matches the recomputed one.
The expected key is an argument because it is the whole check. This method used to read a.GroupPubKey — a field of the very struct being verified — so it answered "was this signed by whoever signed it", which every attestation satisfies, including one an attacker minted with its own key and shipped alongside. A signature is only evidence when the verifier already knows whose signature it is willing to accept.
The caller has that key: B-Chain holds the group public point from M-Chain's keygen and never holds a custody secret. Passing nil or a mismatched key fails closed.
type BridgeTransfer ¶
type BridgeTransfer struct {
SrcChainID uint32 `json:"srcChainId"` // source EVM chain id
DstChainID uint32 `json:"dstChainId"` // destination EVM chain id
Asset [32]byte `json:"asset"` // canonical cross-chain asset id
Amount uint64 `json:"amount"` // units locked on src == minted on dst
Recipient [20]byte `json:"recipient"` // destination recipient (20-byte account)
Nonce uint64 `json:"nonce"` // per-route monotonic nonce (replay guard)
}
BridgeTransfer is the domain-bound message B commits to on lock and M signs as its attestation. Field layout is FROZEN so the digest is canonical across validators and across the B/M/Solidity boundary.
One attestation over one BridgeTransfer authorises exactly one mint — of that amount, of that asset, to that recipient, on that route (src->dst), at that nonce — and nothing else.
func (BridgeTransfer) Digest ¶
func (bt BridgeTransfer) Digest() [32]byte
Digest is the canonical, domain-separated signing preimage for a transfer.
sha256( "LUX_BRIDGE_TRANSFER_v1" // 22-byte ASCII tag, raw || uint32_BE(SrcChainID) // 4 || uint32_BE(DstChainID) // 4 || Asset[32] // 32 || uint64_BE(Amount) // 8 || Recipient[20] // 20 || uint64_BE(Nonce) // 8 )
abi.encodePacked(bytes(DomainTag), uint32, uint32, bytes32, uint256(u64), bytes20, uint64) on the Solidity side produces the identical preimage.
type SecurityAttestation ¶ added in v1.7.42
type SecurityAttestation struct {
Crossing SecurityCrossing `json:"crossing"`
Digest [32]byte `json:"digest"`
Signature []byte `json:"signature"` // secp256k1 r||s||v
GroupPubKey []byte `json:"groupPubKey"` // 33-byte compressed group key
Signers []string `json:"signers"` // party ids of the T+1 quorum
KeyID string `json:"keyId"`
// CeremonyID is this attestation's entry in M-Chain's replicated ceremony
// log — the handle that turns "B was handed a signature" into "B can point
// at the consensus record that produced it".
CeremonyID string `json:"ceremonyId"`
CreatedAt int64 `json:"createdAt"`
}
SecurityAttestation is M's threshold signature over a crossing, plus the context B needs to verify it without re-querying M.
func (*SecurityAttestation) VerifyAgainst ¶ added in v1.7.42
func (a *SecurityAttestation) VerifyAgainst(expectedGroupKey []byte) bool
VerifyAgainst returns true iff the signature is a valid threshold signature, by the EXPECTED group key, over this crossing's domain-bound digest.
The expected key is an argument because it is the whole check — reading the key off the struct being verified answers "was this signed by whoever signed it", which every attestation satisfies, including one an attacker minted with its own key. See VerifyAgainst on Attestation, where that was a real defect.
type SecurityCrossing ¶ added in v1.7.42
type SecurityCrossing struct {
SrcChainID uint32 `json:"srcChainId"` // source EVM chain id
DstChainID uint32 `json:"dstChainId"` // destination EVM chain id
// Security is the canonical cross-chain id of the security itself, not the
// token contract on either side. The same paper has a different address on
// every register it appears on.
Security [32]byte `json:"security"`
// Quantity is 32 bytes big-endian, holding the full uint256 the token
// moves. A security's smallest unit is the register's business and an
// eighteen-decimal position overflows a uint64 at about 18.4 tokens, so a
// crossing that could not carry more than that would be a crossing for
// nothing.
Quantity [32]byte `json:"quantity"`
// Holder is the account the position lands on.
Holder [20]byte `json:"holder"`
// Identity is the holder's ONCHAINID on the SOURCE register.
//
// It travels because the register's clocks belong to the person and not to
// the key: a holding period that starts again on arrival is a holding
// period a crossing can wash off, and the destination cannot ask the source
// register anything. Zero where the source had no identity registered — an
// honest absence, which the destination weighs as it likes rather than
// reading as an identity of zero.
Identity [20]byte `json:"identity"`
// Acquired is when the holder first received on the SOURCE register, in
// seconds. The destination anchors the arriving position's clock to it
// rather than to now, so a §144 holding period or a Reg CF year is served
// once by the holder and not once per chain they visit. Zero means the
// source had no anchor to send.
Acquired uint64 `json:"acquired"`
// Nonce is the per-route monotonic replay guard.
Nonce uint64 `json:"nonce"`
}
SecurityCrossing is the domain-bound message B commits to when a security leaves one register, and M threshold-signs as its attestation. Field layout is FROZEN, for the same reason BridgeTransfer's is: the digest has to be identical across validators and across the B/M/Solidity boundary.
One attestation authorises exactly one arrival — of that quantity, of that security, to that holder, on that route, at that nonce — and nothing else.
func (SecurityCrossing) Digest ¶ added in v1.7.42
func (sc SecurityCrossing) Digest() [32]byte
Digest is the canonical, domain-separated signing preimage for a crossing.
sha256( "LUX_SECURITY_CROSSING_v1" // 24-byte ASCII tag, raw || uint32_BE(SrcChainID) // 4 || uint32_BE(DstChainID) // 4 || Security[32] // 32 || Quantity[32] // 32, big-endian uint256 || Holder[20] // 20 || Identity[20] // 20 || uint64_BE(Acquired) // 8 || uint64_BE(Nonce) // 8 )
abi.encodePacked(bytes(SecurityDomainTag), uint32, uint32, bytes32, uint256, bytes20, bytes20, uint64, uint64) on the Solidity side produces the identical preimage.