Files
zeekay 9c9106017e feat(htlc): cross-chain HTLC leg builders (pkg/htlc)
The single place a Lux cross-chain atomic swap is constructed across all
three legs, composed from one shared primitive:

- secret.go  — Secret [32]byte, Hashlock() = sha256(secret), constant-time
  VerifyPreimage. THE source of the SHA-256 hashlock every leg shares (one
  32-byte preimage unlocks BTC, the LP-90A0 swap precompile, and the EVM
  LXSwapHTLC.sol); keccak is explicitly excluded.
- bitcoin.go — the fixed P2WSH HTLC script (OP_IF SHA256/EQUALVERIFY/CHECKSIG
  // OP_ELSE CLTV/DROP/CHECKSIG), P2WSHAddress, and claim/refund witnesses
  signed with real BIP143 segwit-v0 sighash via btcd txscript (no hand-rolled
  sighash; low-S RFC6979 sigs).
- evm.go     — lock/claim/refund calldata; selectors DERIVED from the canonical
  ABI signatures and asserted equal to the precompile's 0x4da2c728 /
  0x84cc9dfb / 0x7249fbb6.

Tests (-race) include real txscript.Engine execution proofs: claim spends
with the correct preimage and fails with a wrong one; refund spends at/after
locktime and fails before / with CLTV disabled. btcd pinned v0.24.2 (v0.26.0
split chaincfg/wire/txscript into /v2 modules and breaks the build).
2026-06-26 04:26:54 +00:00

81 lines
2.3 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package htlc
import (
"encoding/binary"
"math/big"
"github.com/luxfi/crypto"
"github.com/luxfi/geth/common"
)
// EVM-leg ABI signatures. These canonical strings are the SINGLE source of truth
// for the selectors below; the documented hex (asserted in the tests) is derived
// from them, never hand-entered, so it cannot drift from swap.go / LXSwapHTLC.sol.
const (
lockSig = "lock(bytes32,address,address,address,uint256,uint64)"
claimSig = "claim(bytes32,bytes32)"
refundSig = "refund(bytes32)"
)
// 4-byte selectors: keccak256(signature)[:4]. lock=0x4da2c728, claim=0x84cc9dfb,
// refund=0x7249fbb6.
var (
selLock = selector(lockSig)
selClaim = selector(claimSig)
selRefund = selector(refundSig)
)
func selector(sig string) [4]byte {
var s [4]byte
copy(s[:], crypto.Keccak256([]byte(sig)))
return s
}
// LockCalldata encodes lock(hashlock, recipient, refund, asset, amount, timeout)
// for the swap precompile / LXSwapHTLC.sol. hashlock is the SAME h = SHA256(s)
// the Bitcoin leg locks under, so one preimage settles both.
func LockCalldata(hashlock [32]byte, recipient, refund, asset common.Address, amount *big.Int, timeout uint64) []byte {
out := make([]byte, 0, 4+6*32)
out = append(out, selLock[:]...)
out = append(out, hashlock[:]...)
out = append(out, word(recipient.Bytes())...)
out = append(out, word(refund.Bytes())...)
out = append(out, word(asset.Bytes())...)
out = append(out, word(amount.Bytes())...)
out = append(out, word(u64(timeout))...)
return out
}
// ClaimCalldata encodes claim(swapId, preimage). preimage is the 32-byte secret s.
func ClaimCalldata(swapID [32]byte, preimage Secret) []byte {
out := make([]byte, 0, 4+2*32)
out = append(out, selClaim[:]...)
out = append(out, swapID[:]...)
out = append(out, preimage[:]...)
return out
}
// RefundCalldata encodes refund(swapId).
func RefundCalldata(swapID [32]byte) []byte {
out := make([]byte, 0, 4+32)
out = append(out, selRefund[:]...)
out = append(out, swapID[:]...)
return out
}
// word left-pads a big-endian byte string to a 32-byte ABI slot.
func word(b []byte) []byte {
w := make([]byte, 32)
copy(w[32-len(b):], b)
return w
}
func u64(v uint64) []byte {
var b [8]byte
binary.BigEndian.PutUint64(b[:], v)
return b[:]
}