Files
maker/dexauth_test.go
zeekayandHanzo Dev 1ac4c806b2 Go 1.27.1, and the code it can say more simply
Modules declare go 1.27.1 and the build images follow. go fix applied the
modernizers; omitzero is not applied, since it changes what the JSON says.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 19:23:15 -07:00

216 lines
7.7 KiB
Go

package maker
import (
"context"
"crypto/sha256"
"encoding/binary"
"sync"
"testing"
"github.com/luxfi/crypto"
"github.com/luxfi/crypto/mldsa"
)
// dexauth_test.go proves the maker's auth signing is byte-compatible with the live
// venue gate (dex/pkg/dchain auth.go): a signature the maker produces recovers to
// the account it asserts, and the signed client appends the envelope to every
// money-moving frame with a strictly-monotonic nonce.
// TestEnvelopeRecoversToWireUser independently recomputes the digest the server
// signs over, recovers the signer with the SAME math RecoverAuthAddress uses
// (secp ecrecover -> keccak address), folds it to the 16-byte account, and asserts
// it equals Signer.WireUser(). That is exactly verifyTxSignature's accept check,
// so a pass means the real D-Chain VM binds this signature to the asserted user.
func TestEnvelopeRecoversToWireUser(t *testing.T) {
cases := []struct {
name string
mk func() (*Signer, error)
scheme byte
sigSize int
pubSize int
}{
{"secp", func() (*Signer, error) { return NewSignerFromSeed([]byte("treasury-seed-reboot")) }, schemeSecp, secpSigSize, 0},
{"mldsa", func() (*Signer, error) { return NewMLDSASignerFromSeed([]byte("treasury-seed-pq")) }, schemeMLDSA, mldsa.MLDSA65SignatureSize, mldsa.MLDSA65PublicKeySize},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
signer, err := tc.mk()
if err != nil {
t.Fatalf("new signer: %v", err)
}
pool := SymbolPoolID("LUX/LUSD")
body := encodePlace(pool, sideSell, 12.5125, 100, signer.WireUser())
const nonce = uint64(7)
env := signer.envelope(txTypePlace, nonce, body)
// Envelope layout: scheme[1] + nonce[8] + sigLen[2] + pubLen[2] + sig + pub.
if got, want := len(env), authHeaderSize+tc.sigSize+tc.pubSize; got != want {
t.Fatalf("envelope len = %d, want %d", got, want)
}
if env[0] != tc.scheme {
t.Fatalf("scheme = %d, want %d", env[0], tc.scheme)
}
if got := binary.BigEndian.Uint64(env[1:9]); got != nonce {
t.Fatalf("envelope nonce = %d, want %d", got, nonce)
}
sigLen := int(binary.BigEndian.Uint16(env[9:11]))
pubLen := int(binary.BigEndian.Uint16(env[11:13]))
if sigLen != tc.sigSize || pubLen != tc.pubSize {
t.Fatalf("sigLen=%d pubLen=%d, want %d/%d", sigLen, pubLen, tc.sigSize, tc.pubSize)
}
sig := env[authHeaderSize : authHeaderSize+sigLen]
pub := env[authHeaderSize+sigLen:]
// Recompute the auth digest exactly as the server's txAuthDigest, then
// recover the signer with the SAME math as lx.RecoverAuthAddress and fold
// to the 16-byte account — verifyTxSignature's accept check, both schemes.
var n8 [8]byte
binary.BigEndian.PutUint64(n8[:], nonce)
digest := crypto.Keccak256([]byte(txAuthDomain), []byte{tc.scheme}, []byte{txTypePlace}, n8[:], body)
var addr []byte
switch tc.scheme {
case schemeSecp:
pk, err := crypto.SigToPub(digest, sig)
if err != nil {
t.Fatalf("recover pubkey: %v", err)
}
addr = crypto.PubkeyToAddress(*pk).Bytes()
case schemeMLDSA:
pk, err := mldsa.PublicKeyFromBytes(pub, mldsa.MLDSA65)
if err != nil {
t.Fatalf("parse mldsa pub: %v", err)
}
if !pk.VerifySignature(digest, sig) {
t.Fatal("mldsa VerifySignature returned false for our signature")
}
h := sha256.Sum256(pub)
addr = h[:20]
}
account := crypto.Keccak256([]byte(accountDomain), []byte{tc.scheme}, addr)[:userSize]
if string(account) != signer.WireUser() {
t.Fatalf("recovered account %x != wire user %x", account, []byte(signer.WireUser()))
}
})
}
}
// capturedCall records one CallRaw the DEXClient issued.
type capturedCall struct {
method string
payload []byte
}
// captureClient is a tiny in-test rpc.Client that records every CallRaw payload
// and replies with the frozen ack/fills/balance frames (reusing seeder_test.go's
// builders) so the signed methods succeed. It lets the test inspect the EXACT
// bytes the maker put on the wire.
type captureClient struct {
mu sync.Mutex
calls []capturedCall
}
func (c *captureClient) CallRaw(_ context.Context, method string, payload []byte) ([]byte, error) {
c.mu.Lock()
c.calls = append(c.calls, capturedCall{method: method, payload: append([]byte(nil), payload...)})
c.mu.Unlock()
switch method {
case methodDeposit:
// Echo the credited amount so DEXClient.Deposit's credited==amount check passes.
amount := binary.BigEndian.Uint64(payload[userSize+assetIDSize : userSize+assetIDSize+8])
return balanceResp(statusPlaced, amount), nil
case methodSubmit:
return encodeFillsResp(nil), nil
default: // ensure_market, place, cancel
return ackBytes(1, statusPlaced), nil
}
}
func (c *captureClient) Call(context.Context, string, any, any) error { return nil }
func (c *captureClient) Notify(context.Context, string, any) error { return nil }
func (c *captureClient) Close() error { return nil }
// signedFor returns the captured calls whose method moves money, in order.
func (c *captureClient) signedFor() []capturedCall {
c.mu.Lock()
defer c.mu.Unlock()
var out []capturedCall
for _, call := range c.calls {
switch call.method {
case methodDeposit, methodPlace, methodCancel, methodSubmit:
out = append(out, call)
}
}
return out
}
// TestSignedClientAppendsEnvelope drives Deposit/PlaceLimit/Cancel/Submit through a
// signed DEXClient and asserts each wire payload is exactly body‖envelope for the
// matching tx type, and that the nonce increments monotonically (0,1,2,3) across
// the money-moving calls. ensure_market rides between deposit and place WITHOUT a
// nonce (admin/unsigned), proving only money-moving ops consume the sequence.
func TestSignedClientAppendsEnvelope(t *testing.T) {
signer, err := NewSignerFromSeed([]byte("maker-signed-client"))
if err != nil {
t.Fatalf("new signer: %v", err)
}
cc := &captureClient{}
dex := NewDEXClient(cc, "placeholder")
dex.UseSigner(signer)
if dex.user != signer.WireUser() {
t.Fatalf("UseSigner did not rebind user: %x != %x", []byte(dex.user), []byte(signer.WireUser()))
}
ctx := context.Background()
if err := dex.Deposit(ctx, AssetLUX, 100); err != nil {
t.Fatalf("deposit: %v", err)
}
orderID, err := dex.PlaceLimit(ctx, "LUX/LUSD", "sell", 12.5, 10)
if err != nil {
t.Fatalf("place: %v", err)
}
if err := dex.Cancel(ctx, orderID); err != nil {
t.Fatalf("cancel: %v", err)
}
if _, err := dex.Submit(ctx, "LUX/LUSD", "buy", false, 13.0, 5); err != nil {
t.Fatalf("submit: %v", err)
}
pool := SymbolPoolID("LUX/LUSD")
want := []struct {
typ byte
body []byte
}{
{txTypeDeposit, encodeDeposit(dex.user, AssetLUX, 100, depositRef(dex.user, AssetLUX, 100))},
{txTypePlace, encodePlace(pool, sideSell, 12.5, 10, dex.user)},
{txTypeCancel, encodeCancel(pool, orderID)},
{txTypeSubmit, encodeSubmit(pool, sideBuy, false, 13.0, 5, dex.user)},
}
signed := cc.signedFor()
if len(signed) != len(want) {
t.Fatalf("signed calls = %d, want %d", len(signed), len(want))
}
for i, w := range want {
got := signed[i].payload
// (1) payload == body ‖ envelope(typ, nonce=i, body).
expectedEnv := signer.envelope(w.typ, uint64(i), w.body)
expected := append(append([]byte(nil), w.body...), expectedEnv...)
if string(got) != string(expected) {
t.Fatalf("call %d (%s): payload mismatch\n got=%x\nwant=%x", i, signed[i].method, got, expected)
}
// (2) the appended envelope carries nonce i, strictly monotonic.
if got, want := binary.BigEndian.Uint64(signed[i].payload[len(w.body)+1:len(w.body)+9]), uint64(i); got != want {
t.Fatalf("call %d (%s): nonce = %d, want %d", i, signed[i].method, got, want)
}
// (3) the body prefix is the unmodified frozen frame.
if string(got[:len(w.body)]) != string(w.body) {
t.Fatalf("call %d (%s): body prefix altered", i, signed[i].method)
}
}
}