mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
M threshold-signs a security crossing the way it signs a fungible transfer, and B carries the attestation to the destination register. Same leaderless ceremony, same authorisation, same group key travelling with the result so B verifies without asking M anything. What differs is the message, and therefore the digest. What B does here is narrower than what it does for a fungible asset. The fungible path releases by minting: a confirmed lock authorises N units to an account and nothing else has to be true. Whether a SECURITY may exist on the destination at all is the destination register's question — is this holder verified, do their claims satisfy the token, does a holding period follow the paper across — and the token asks it in the block that lands the position. B never decides a security may land. It decides the attestation is genuine and says what it claims to say. SecurityRequest is separate from BridgeRequest rather than a flag on it. The two are released by different calls onto the destination, and a mint against a register nobody asked is exactly the failure this path exists to prevent; one type that could be either leaves that difference to a branch somebody has to remember to write. Two fields the destination cannot look up for itself travel with the crossing — the holder's ONCHAINID on the source register, and when they first received there. Without the second, an arriving position anchors its clock to now, and a holding period a crossing resets is one a crossing washes off. A short identity is refused rather than read as an absence: treating it as absent lets a truncated field pass for "this holder had none". An absent one travels as zero, which is honest. Quantity is a uint256. A billion shares at eighteen decimals is an ordinary size for a register and does not fit the uint64 the fungible path carries; a truncated quantity is a position that arrives smaller than it left, so it is refused rather than truncated. Signer ids convert to strings at the boundary so a verifier on the far side needs nothing of M's type graph. The crossing type itself is aliased from internal/bridgeattest rather than copied — attest_bridge.go keeps its own copy of BridgeTransfer, and two definitions kept in step by hand is a way for a digest to drift across a boundary whose whole value is that it cannot.
124 lines
4.2 KiB
Go
124 lines
4.2 KiB
Go
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package bridgevm
|
|
|
|
import (
|
|
"bytes"
|
|
"math/big"
|
|
"testing"
|
|
|
|
"github.com/luxfi/ids"
|
|
)
|
|
|
|
func sampleSecurity() *SecurityRequest {
|
|
return &SecurityRequest{
|
|
SrcChainID: 96369,
|
|
DstChainID: 36963,
|
|
Nonce: 1,
|
|
Security: ids.ID(bytes.Repeat([]byte{0xAB}, 32)),
|
|
Quantity: new(big.Int).Mul(big.NewInt(1000), big.NewInt(1e18)),
|
|
Holder: bytes.Repeat([]byte{0x11}, 20),
|
|
Identity: bytes.Repeat([]byte{0x22}, 20),
|
|
Acquired: 1_700_000_000,
|
|
}
|
|
}
|
|
|
|
// The reason this message exists rather than a widened BridgeTransfer: a
|
|
// position of any real size overflows the uint64 the fungible path carries.
|
|
func TestAPositionLargerThanAUint64Crosses(t *testing.T) {
|
|
r := sampleSecurity()
|
|
// A billion shares at eighteen decimals — far past uint64, and an ordinary
|
|
// size for a register.
|
|
r.Quantity = new(big.Int).Mul(big.NewInt(1_000_000_000), big.NewInt(1e18))
|
|
|
|
sc, err := r.crossing()
|
|
if err != nil {
|
|
t.Fatalf("crossing: %v", err)
|
|
}
|
|
if got := new(big.Int).SetBytes(sc.Quantity[:]); got.Cmp(r.Quantity) != 0 {
|
|
t.Fatalf("quantity did not survive: got %s want %s", got, r.Quantity)
|
|
}
|
|
if r.Quantity.IsUint64() {
|
|
t.Fatal("control: this test's quantity fits a uint64, so it proves nothing")
|
|
}
|
|
}
|
|
|
|
// A short identity is a malformed request, not an absence. Reading it as
|
|
// absence would let a truncated field pass for "this holder had none".
|
|
func TestAShortIdentityIsRefusedRatherThanTreatedAsAbsent(t *testing.T) {
|
|
r := sampleSecurity()
|
|
r.Identity = bytes.Repeat([]byte{0x22}, 19)
|
|
|
|
if _, err := r.crossing(); err == nil {
|
|
t.Fatal("a 19-byte identity was accepted")
|
|
}
|
|
|
|
// Whereas none at all is honest, and travels as zero.
|
|
r.Identity = nil
|
|
sc, err := r.crossing()
|
|
if err != nil {
|
|
t.Fatalf("an absent identity should cross: %v", err)
|
|
}
|
|
if sc.Identity != ([20]byte{}) {
|
|
t.Fatal("an absent identity did not travel as zero")
|
|
}
|
|
}
|
|
|
|
// Everything the digest commits to is validated before a signature can exist,
|
|
// because an attestation over a malformed crossing authorises an arrival nobody
|
|
// can make sense of.
|
|
func TestAMalformedCrossingNeverReachesASignature(t *testing.T) {
|
|
for name, break_ := range map[string]func(*SecurityRequest){
|
|
"no destination": func(r *SecurityRequest) { r.DstChainID = 0 },
|
|
"short holder": func(r *SecurityRequest) { r.Holder = bytes.Repeat([]byte{1}, 19) },
|
|
"no holder": func(r *SecurityRequest) { r.Holder = nil },
|
|
"nil quantity": func(r *SecurityRequest) { r.Quantity = nil },
|
|
"zero quantity": func(r *SecurityRequest) { r.Quantity = big.NewInt(0) },
|
|
"negative amount": func(r *SecurityRequest) { r.Quantity = big.NewInt(-1) },
|
|
"over uint256": func(r *SecurityRequest) {
|
|
r.Quantity = new(big.Int).Lsh(big.NewInt(1), 256)
|
|
},
|
|
} {
|
|
r := sampleSecurity()
|
|
break_(r)
|
|
if _, err := r.crossing(); err == nil {
|
|
t.Fatalf("%s was accepted", name)
|
|
}
|
|
}
|
|
|
|
// Control: the unbroken request crosses, so the refusals above are the
|
|
// breakage and not a mapping that refuses everything.
|
|
if _, err := sampleSecurity().crossing(); err != nil {
|
|
t.Fatalf("control: a well-formed crossing was refused: %v", err)
|
|
}
|
|
}
|
|
|
|
// The route and the nonce are part of what one attestation authorises, so two
|
|
// requests that differ only in those must not produce the same digest.
|
|
func TestEachCrossingIsItsOwn(t *testing.T) {
|
|
base, err := sampleSecurity().crossing()
|
|
if err != nil {
|
|
t.Fatalf("crossing: %v", err)
|
|
}
|
|
for name, mutate := range map[string]func(*SecurityRequest){
|
|
"nonce": func(r *SecurityRequest) { r.Nonce++ },
|
|
"destination": func(r *SecurityRequest) { r.DstChainID++ },
|
|
"source": func(r *SecurityRequest) { r.SrcChainID++ },
|
|
"holder": func(r *SecurityRequest) { r.Holder[19] ^= 1 },
|
|
"identity": func(r *SecurityRequest) { r.Identity[19] ^= 1 },
|
|
"acquired": func(r *SecurityRequest) { r.Acquired++ },
|
|
"quantity": func(r *SecurityRequest) { r.Quantity = big.NewInt(1) },
|
|
} {
|
|
r := sampleSecurity()
|
|
mutate(r)
|
|
other, err := r.crossing()
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", name, err)
|
|
}
|
|
if other.Digest() == base.Digest() {
|
|
t.Fatalf("%s does not change the digest, so one attestation covers both", name)
|
|
}
|
|
}
|
|
}
|