Files
warp/bridge/atomic_bridge_test.go
zeekay 5c8340d768 refactor(warp)!: Core -> Message — name the thing as it is
The cross-chain message {NetworkID, SourceChainID, Payload} is a message, not a
"core" (a positional non-name) and not "Warp" (that stutters as warp.Warp — the
same package-in-type braid we removed from WarpEnvelope). It is the value you
hash to D and the validators sign over; the signed wire form stays warp.Envelope.

  warp.Core   -> warp.Message
  warp.NewCore-> warp.NewMessage, ParseCore -> ParseMessage, coreDST -> messageDST

warp.Message reads as "a warp message" and matches warp's own existing
ErrInvalidMessage / MaxMessageSize naming. Identifiers only: digest D, ZAP c14n,
the BEAM/PULSE/MLDSA DSTs, the CORE-v1 domain string and the coreC14n fixture
keys are byte-unchanged — fuzz, fixture, ID-stability and teleport tests green.
2026-06-27 00:38:29 -07:00

754 lines
16 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package bridge
import (
"context"
"crypto/rand"
"crypto/sha256"
"math/big"
"sync"
"testing"
"time"
"github.com/luxfi/ids"
"github.com/luxfi/warp"
"github.com/luxfi/warp/payload"
"github.com/stretchr/testify/require"
)
// generateTestID creates a random ID for testing
func generateTestID() ids.ID {
var id ids.ID
rand.Read(id[:])
return id
}
// mockBackend implements backend.Backend for testing
type mockBackend struct {
messages []*warp.Message
mu sync.Mutex
}
func newMockBackend() *mockBackend {
return &mockBackend{
messages: make([]*warp.Message, 0),
}
}
func (m *mockBackend) AddMessage(message *warp.Message) error {
m.mu.Lock()
defer m.mu.Unlock()
m.messages = append(m.messages, message)
return nil
}
func (m *mockBackend) GetMessage(index uint32) (*warp.Envelope, error) {
m.mu.Lock()
defer m.mu.Unlock()
if int(index) >= len(m.messages) {
return nil, nil
}
return &warp.Envelope{
Message: *m.messages[index],
}, nil
}
func (m *mockBackend) GetValidatorState() warp.ValidatorState {
return nil
}
func (m *mockBackend) GetMessageCount() int {
m.mu.Lock()
defer m.mu.Unlock()
return len(m.messages)
}
func TestNewAtomicBridge(t *testing.T) {
require := require.New(t)
sourceChain := generateTestID()
destChain := generateTestID()
backend := newMockBackend()
cfg := &BridgeConfig{
NetworkID: 1,
SourceChainID: sourceChain,
DestChainID: destChain,
Backend: backend,
}
bridge := NewAtomicBridge(cfg)
require.NotNil(bridge)
require.Equal(uint32(1), bridge.networkID)
require.Equal(sourceChain, bridge.sourceChainID)
require.Equal(destChain, bridge.destChainID)
require.NotNil(bridge.swaps)
}
func TestSwapStateString(t *testing.T) {
tests := []struct {
state SwapState
expected string
}{
{SwapStatePending, "pending"},
{SwapStateLocked, "locked"},
{SwapStateMinted, "minted"},
{SwapStateSwapped, "swapped"},
{SwapStateSettled, "settled"},
{SwapStateCancelled, "canceled"},
{SwapStateExpired, "expired"},
{SwapState(99), "unknown"},
}
for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
require.Equal(t, tt.expected, tt.state.String())
})
}
}
func TestInitiateSwap(t *testing.T) {
require := require.New(t)
sourceChain := generateTestID()
destChain := generateTestID()
backend := newMockBackend()
cfg := &BridgeConfig{
NetworkID: 1,
SourceChainID: sourceChain,
DestChainID: destChain,
Backend: backend,
}
bridge := NewAtomicBridge(cfg)
sender := make([]byte, 20)
recipient := make([]byte, 20)
rand.Read(sender)
rand.Read(recipient)
var asset [32]byte
rand.Read(asset[:])
amount := big.NewInt(1000000) // 1 token with 6 decimals
minReceive := big.NewInt(990000)
deadline := uint64(time.Now().Add(1 * time.Hour).Unix())
// Create swap route
route := []payload.SwapRoute{
{
TokenIn: [20]byte{0x01},
TokenOut: [20]byte{0x02},
PoolFee: 3000,
TickSpacing: 60,
},
}
record, err := bridge.InitiateSwap(
context.Background(),
sender,
recipient,
asset,
amount,
minReceive,
deadline,
route,
)
require.NoError(err)
require.NotNil(record)
require.Equal(SwapStateLocked, record.State)
require.Equal(sourceChain, record.SourceChain)
require.Equal(destChain, record.DestChain)
require.Equal(amount, record.Amount)
require.Equal(minReceive, record.MinReceive)
require.Equal(deadline, record.Deadline)
require.Equal(uint64(0), record.Nonce) // First swap
require.NotEqual([32]byte{}, record.SwapID)
require.NotEqual([32]byte{}, record.HashLock)
require.NotEqual([32]byte{}, record.Preimage)
// Verify hashlock is correct
expectedHash := sha256.Sum256(record.Preimage[:])
require.Equal(expectedHash, record.HashLock)
// Verify message was sent
require.Equal(1, backend.GetMessageCount())
}
func TestInitiateSwapWithCallbacks(t *testing.T) {
require := require.New(t)
sourceChain := generateTestID()
destChain := generateTestID()
backend := newMockBackend()
cfg := &BridgeConfig{
NetworkID: 1,
SourceChainID: sourceChain,
DestChainID: destChain,
Backend: backend,
}
bridge := NewAtomicBridge(cfg)
// Track callback invocations
lockCalled := false
bridge.SetCallbacks(
func(ctx context.Context, swap *SwapRecord) error {
lockCalled = true
return nil
},
nil, nil, nil,
)
sender := make([]byte, 20)
recipient := make([]byte, 20)
var asset [32]byte
amount := big.NewInt(1000000)
deadline := uint64(time.Now().Add(1 * time.Hour).Unix())
_, err := bridge.InitiateSwap(
context.Background(),
sender,
recipient,
asset,
amount,
big.NewInt(0),
deadline,
nil,
)
require.NoError(err)
require.True(lockCalled)
}
func TestGetSwap(t *testing.T) {
require := require.New(t)
bridge := NewAtomicBridge(&BridgeConfig{
NetworkID: 1,
SourceChainID: generateTestID(),
DestChainID: generateTestID(),
Backend: newMockBackend(),
})
sender := make([]byte, 20)
recipient := make([]byte, 20)
var asset [32]byte
amount := big.NewInt(1000000)
deadline := uint64(time.Now().Add(1 * time.Hour).Unix())
record, err := bridge.InitiateSwap(
context.Background(),
sender,
recipient,
asset,
amount,
big.NewInt(0),
deadline,
nil,
)
require.NoError(err)
// Get by ID
found, err := bridge.GetSwap(record.SwapID)
require.NoError(err)
require.Equal(record.SwapID, found.SwapID)
// Get non-existent
var fakeID [32]byte
rand.Read(fakeID[:])
_, err = bridge.GetSwap(fakeID)
require.Error(err)
}
func TestGetSwapByHex(t *testing.T) {
require := require.New(t)
bridge := NewAtomicBridge(&BridgeConfig{
NetworkID: 1,
SourceChainID: generateTestID(),
DestChainID: generateTestID(),
Backend: newMockBackend(),
})
sender := make([]byte, 20)
recipient := make([]byte, 20)
var asset [32]byte
amount := big.NewInt(1000000)
deadline := uint64(time.Now().Add(1 * time.Hour).Unix())
record, err := bridge.InitiateSwap(
context.Background(),
sender,
recipient,
asset,
amount,
big.NewInt(0),
deadline,
nil,
)
require.NoError(err)
// Convert to hex and lookup
hexID := ""
for _, b := range record.SwapID {
hexID += string([]byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}[b>>4])
hexID += string([]byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}[b&0xf])
}
found, err := bridge.GetSwapByHex(hexID)
require.NoError(err)
require.Equal(record.SwapID, found.SwapID)
// Invalid hex
_, err = bridge.GetSwapByHex("invalid")
require.Error(err)
// Wrong length
_, err = bridge.GetSwapByHex("abcd")
require.Error(err)
}
func TestListSwaps(t *testing.T) {
require := require.New(t)
bridge := NewAtomicBridge(&BridgeConfig{
NetworkID: 1,
SourceChainID: generateTestID(),
DestChainID: generateTestID(),
Backend: newMockBackend(),
})
sender := make([]byte, 20)
recipient := make([]byte, 20)
var asset [32]byte
amount := big.NewInt(1000000)
deadline := uint64(time.Now().Add(1 * time.Hour).Unix())
// Create 3 swaps
for i := 0; i < 3; i++ {
_, err := bridge.InitiateSwap(
context.Background(),
sender,
recipient,
asset,
amount,
big.NewInt(0),
deadline,
nil,
)
require.NoError(err)
}
// List all (0 means all states since SwapStatePending is 0)
all := bridge.ListSwaps(0)
require.Len(all, 3)
// List by state - all swaps are locked after InitiateSwap
locked := bridge.ListSwaps(SwapStateLocked)
require.Len(locked, 3)
// No settled swaps
settled := bridge.ListSwaps(SwapStateSettled)
require.Len(settled, 0)
}
func TestSettleSwap(t *testing.T) {
require := require.New(t)
bridge := NewAtomicBridge(&BridgeConfig{
NetworkID: 1,
SourceChainID: generateTestID(),
DestChainID: generateTestID(),
Backend: newMockBackend(),
})
settleCalled := false
bridge.SetCallbacks(nil, nil, nil, func(ctx context.Context, swap *SwapRecord) error {
settleCalled = true
return nil
})
sender := make([]byte, 20)
recipient := make([]byte, 20)
var asset [32]byte
amount := big.NewInt(1000000)
deadline := uint64(time.Now().Add(1 * time.Hour).Unix())
record, err := bridge.InitiateSwap(
context.Background(),
sender,
recipient,
asset,
amount,
big.NewInt(0),
deadline,
nil,
)
require.NoError(err)
// Settle with correct preimage
err = bridge.SettleSwap(context.Background(), record.SwapID, record.Preimage)
require.NoError(err)
require.True(settleCalled)
// Verify state
found, err := bridge.GetSwap(record.SwapID)
require.NoError(err)
require.Equal(SwapStateSettled, found.State)
}
func TestSettleSwapInvalidPreimage(t *testing.T) {
require := require.New(t)
bridge := NewAtomicBridge(&BridgeConfig{
NetworkID: 1,
SourceChainID: generateTestID(),
DestChainID: generateTestID(),
Backend: newMockBackend(),
})
sender := make([]byte, 20)
recipient := make([]byte, 20)
var asset [32]byte
amount := big.NewInt(1000000)
deadline := uint64(time.Now().Add(1 * time.Hour).Unix())
record, err := bridge.InitiateSwap(
context.Background(),
sender,
recipient,
asset,
amount,
big.NewInt(0),
deadline,
nil,
)
require.NoError(err)
// Settle with wrong preimage
var wrongPreimage [32]byte
rand.Read(wrongPreimage[:])
err = bridge.SettleSwap(context.Background(), record.SwapID, wrongPreimage)
require.Error(err)
require.Contains(err.Error(), "invalid preimage")
}
func TestSettleSwapNotFound(t *testing.T) {
require := require.New(t)
bridge := NewAtomicBridge(&BridgeConfig{
NetworkID: 1,
SourceChainID: generateTestID(),
DestChainID: generateTestID(),
Backend: newMockBackend(),
})
var fakeID [32]byte
var preimage [32]byte
rand.Read(fakeID[:])
rand.Read(preimage[:])
err := bridge.SettleSwap(context.Background(), fakeID, preimage)
require.Error(err)
require.Contains(err.Error(), "swap not found")
}
func TestCancelSwap(t *testing.T) {
require := require.New(t)
bridge := NewAtomicBridge(&BridgeConfig{
NetworkID: 1,
SourceChainID: generateTestID(),
DestChainID: generateTestID(),
Backend: newMockBackend(),
})
sender := make([]byte, 20)
recipient := make([]byte, 20)
var asset [32]byte
amount := big.NewInt(1000000)
// Deadline in the past
deadline := uint64(time.Now().Add(-1 * time.Hour).Unix())
record, err := bridge.InitiateSwap(
context.Background(),
sender,
recipient,
asset,
amount,
big.NewInt(0),
deadline,
nil,
)
require.NoError(err)
// Cancel after deadline
err = bridge.CancelSwap(context.Background(), record.SwapID)
require.NoError(err)
found, err := bridge.GetSwap(record.SwapID)
require.NoError(err)
require.Equal(SwapStateCancelled, found.State)
}
func TestCancelSwapBeforeDeadline(t *testing.T) {
require := require.New(t)
bridge := NewAtomicBridge(&BridgeConfig{
NetworkID: 1,
SourceChainID: generateTestID(),
DestChainID: generateTestID(),
Backend: newMockBackend(),
})
sender := make([]byte, 20)
recipient := make([]byte, 20)
var asset [32]byte
amount := big.NewInt(1000000)
// Deadline in the future
deadline := uint64(time.Now().Add(1 * time.Hour).Unix())
record, err := bridge.InitiateSwap(
context.Background(),
sender,
recipient,
asset,
amount,
big.NewInt(0),
deadline,
nil,
)
require.NoError(err)
// Try to cancel before deadline
err = bridge.CancelSwap(context.Background(), record.SwapID)
require.Error(err)
require.Contains(err.Error(), "deadline not reached")
}
func TestCancelSwapNotFound(t *testing.T) {
require := require.New(t)
bridge := NewAtomicBridge(&BridgeConfig{
NetworkID: 1,
SourceChainID: generateTestID(),
DestChainID: generateTestID(),
Backend: newMockBackend(),
})
var fakeID [32]byte
rand.Read(fakeID[:])
err := bridge.CancelSwap(context.Background(), fakeID)
require.Error(err)
require.Contains(err.Error(), "swap not found")
}
func TestCleanupExpired(t *testing.T) {
require := require.New(t)
bridge := NewAtomicBridge(&BridgeConfig{
NetworkID: 1,
SourceChainID: generateTestID(),
DestChainID: generateTestID(),
Backend: newMockBackend(),
})
sender := make([]byte, 20)
recipient := make([]byte, 20)
var asset [32]byte
amount := big.NewInt(1000000)
// Create expired swap
expiredDeadline := uint64(time.Now().Add(-1 * time.Hour).Unix())
_, err := bridge.InitiateSwap(
context.Background(),
sender,
recipient,
asset,
amount,
big.NewInt(0),
expiredDeadline,
nil,
)
require.NoError(err)
// Create valid swap
validDeadline := uint64(time.Now().Add(1 * time.Hour).Unix())
_, err = bridge.InitiateSwap(
context.Background(),
sender,
recipient,
asset,
amount,
big.NewInt(0),
validDeadline,
nil,
)
require.NoError(err)
// Cleanup
cleaned := bridge.CleanupExpired()
require.Equal(1, cleaned)
// Only 1 swap remaining
all := bridge.ListSwaps(0)
require.Len(all, 1)
}
func TestConcurrentSwaps(t *testing.T) {
require := require.New(t)
bridge := NewAtomicBridge(&BridgeConfig{
NetworkID: 1,
SourceChainID: generateTestID(),
DestChainID: generateTestID(),
Backend: newMockBackend(),
})
var wg sync.WaitGroup
numSwaps := 10
for i := 0; i < numSwaps; i++ {
wg.Add(1)
go func() {
defer wg.Done()
sender := make([]byte, 20)
recipient := make([]byte, 20)
rand.Read(sender)
rand.Read(recipient)
var asset [32]byte
rand.Read(asset[:])
amount := big.NewInt(1000000)
deadline := uint64(time.Now().Add(1 * time.Hour).Unix())
_, err := bridge.InitiateSwap(
context.Background(),
sender,
recipient,
asset,
amount,
big.NewInt(0),
deadline,
nil,
)
require.NoError(err)
}()
}
wg.Wait()
// Verify all swaps created
all := bridge.ListSwaps(0)
require.Len(all, numSwaps)
// Verify unique nonces
nonces := make(map[uint64]bool)
for _, swap := range all {
require.False(nonces[swap.Nonce], "duplicate nonce found")
nonces[swap.Nonce] = true
}
}
func TestGenerateHashLock(t *testing.T) {
require := require.New(t)
hashLock, preimage, err := generateHashLock()
require.NoError(err)
require.NotEqual([32]byte{}, hashLock)
require.NotEqual([32]byte{}, preimage)
// Verify hashlock is sha256 of preimage
expected := sha256.Sum256(preimage[:])
require.Equal(expected, hashLock)
}
func TestNonceIncrement(t *testing.T) {
require := require.New(t)
bridge := NewAtomicBridge(&BridgeConfig{
NetworkID: 1,
SourceChainID: generateTestID(),
DestChainID: generateTestID(),
Backend: newMockBackend(),
})
sender := make([]byte, 20)
recipient := make([]byte, 20)
var asset [32]byte
amount := big.NewInt(1000000)
deadline := uint64(time.Now().Add(1 * time.Hour).Unix())
// Create multiple swaps
for i := 0; i < 5; i++ {
record, err := bridge.InitiateSwap(
context.Background(),
sender,
recipient,
asset,
amount,
big.NewInt(0),
deadline,
nil,
)
require.NoError(err)
require.Equal(uint64(i), record.Nonce)
}
}
func TestSwapWithRoute(t *testing.T) {
require := require.New(t)
bridge := NewAtomicBridge(&BridgeConfig{
NetworkID: 1,
SourceChainID: generateTestID(),
DestChainID: generateTestID(),
Backend: newMockBackend(),
})
sender := make([]byte, 20)
recipient := make([]byte, 20)
var asset [32]byte
amount := big.NewInt(1000000)
deadline := uint64(time.Now().Add(1 * time.Hour).Unix())
route := []payload.SwapRoute{
{
TokenIn: [20]byte{0x01},
TokenOut: [20]byte{0x02},
PoolFee: 500, // 0.05%
TickSpacing: 10,
},
{
TokenIn: [20]byte{0x02},
TokenOut: [20]byte{0x03},
PoolFee: 3000, // 0.3%
TickSpacing: 60,
},
}
record, err := bridge.InitiateSwap(
context.Background(),
sender,
recipient,
asset,
amount,
big.NewInt(0),
deadline,
route,
)
require.NoError(err)
require.Len(record.SwapRoute, 2)
require.Equal(uint32(500), record.SwapRoute[0].PoolFee)
require.Equal(uint32(3000), record.SwapRoute[1].PoolFee)
}