mirror of
https://github.com/luxfi/maker.git
synced 2026-10-30 16:20:04 +00:00
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]>
216 lines
7.7 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|