mirror of
https://github.com/luxfi/mpc.git
synced 2026-09-24 00:26:24 +00:00
155 lines
4.2 KiB
Go
155 lines
4.2 KiB
Go
// Copyright © 2026 Lux Industries Inc. All rights reserved.
|
|
|
|
package main
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/luxfi/threshold/pkg/party"
|
|
"github.com/luxfi/threshold/pkg/protocol"
|
|
"github.com/luxfi/threshold/protocols/frost"
|
|
|
|
"github.com/luxfi/mpc/internal/ceremony"
|
|
"github.com/luxfi/mpc/pkg/mpc"
|
|
revealpkg "github.com/luxfi/mpc/pkg/reveal"
|
|
)
|
|
|
|
// desk is one node's local store, held in memory for the length of a test.
|
|
type desk struct {
|
|
mu sync.Mutex
|
|
held map[string][]byte
|
|
}
|
|
|
|
func newDesk() *desk { return &desk{held: map[string][]byte{}} }
|
|
|
|
func (d *desk) Put(k string, v []byte) error {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
d.held[k] = append([]byte(nil), v...)
|
|
return nil
|
|
}
|
|
|
|
func (d *desk) Get(k string) ([]byte, error) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
return d.held[k], nil
|
|
}
|
|
|
|
func (d *desk) Delete(k string) error { delete(d.held, k); return nil }
|
|
func (d *desk) Close() error { return nil }
|
|
func (d *desk) Backup() error { return nil }
|
|
|
|
const (
|
|
deskOrg = "hanzo"
|
|
deskWallet = "root"
|
|
)
|
|
|
|
// keyed runs a real Ed25519 keygen and files one party's share where a node
|
|
// keeps it, so the handler reads what production would read.
|
|
func keyed(t *testing.T) (*desk, *frost.Config) {
|
|
t.Helper()
|
|
ids := ceremony.Parties()
|
|
results, err := ceremony.Run(ids, []byte("group-handler"), func(id party.ID) protocol.StartFunc {
|
|
return frost.KeygenEd25519(id, ids, 1)
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("keygen: %v", err)
|
|
}
|
|
d := newDesk()
|
|
var cfg *frost.Config
|
|
for _, r := range results {
|
|
c, ok := r.(*frost.Config)
|
|
if !ok {
|
|
t.Fatalf("party produced %T", r)
|
|
}
|
|
raw, err := mpc.MarshalEd25519Config(c)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := d.Put(mpc.OrgScopedKey(deskOrg, mpc.Ed25519ShareKey(deskWallet)), raw); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg = c
|
|
break
|
|
}
|
|
return d, cfg
|
|
}
|
|
|
|
func ask(h http.HandlerFunc, query string) *httptest.ResponseRecorder {
|
|
w := httptest.NewRecorder()
|
|
h(w, httptest.NewRequest(http.MethodGet, "/group?"+query, nil))
|
|
return w
|
|
}
|
|
|
|
// The whole point: a caller who holds no share can still learn what to seal to.
|
|
func TestTheGroupKeyIsReadableWithoutHoldingAShare(t *testing.T) {
|
|
d, cfg := keyed(t)
|
|
w := ask(groupHandler(d), "org="+deskOrg+"&key="+deskWallet)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("code = %d, body %s", w.Code, w.Body.String())
|
|
}
|
|
var got struct {
|
|
GroupKey string `json:"group_key"`
|
|
Quorum int `json:"quorum"`
|
|
}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want, err := cfg.PublicKey.MarshalBinary()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.GroupKey != hex.EncodeToString(want) {
|
|
t.Fatalf("group key = %s, want %s", got.GroupKey, hex.EncodeToString(want))
|
|
}
|
|
// A key that seals but cannot be opened is worse than none, so the answer
|
|
// says how many nodes it takes to open it.
|
|
if got.Quorum != cfg.Threshold+1 {
|
|
t.Fatalf("quorum = %d, want %d", got.Quorum, cfg.Threshold+1)
|
|
}
|
|
}
|
|
|
|
// The key must be usable as-is: sealing to what this returns is the only reason
|
|
// to ask for it, so the bytes have to decode as a point on the curve.
|
|
func TestWhatItReturnsCanBeSealedTo(t *testing.T) {
|
|
d, _ := keyed(t)
|
|
w := ask(groupHandler(d), "org="+deskOrg+"&key="+deskWallet)
|
|
var got struct {
|
|
GroupKey string `json:"group_key"`
|
|
}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, err := hex.DecodeString(got.GroupKey)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := revealpkg.Seal(raw, []byte("a root that no one node may hold")); err != nil {
|
|
t.Fatalf("seal to the returned key: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAKeyWithNoShareIsNotFound(t *testing.T) {
|
|
d, _ := keyed(t)
|
|
w := ask(groupHandler(d), "org="+deskOrg+"&key=absent")
|
|
if w.Code != http.StatusNotFound {
|
|
t.Fatalf("code = %d, want 404", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestOrgAndKeyAreBothRequired(t *testing.T) {
|
|
d, _ := keyed(t)
|
|
// %20 is a key of pure whitespace: the handler trims before it decides,
|
|
// so a blank that arrived encoded is still a blank.
|
|
for _, q := range []string{"", "org=" + deskOrg, "key=" + deskWallet, "org=&key=%20"} {
|
|
if w := ask(groupHandler(d), q); w.Code != http.StatusBadRequest {
|
|
t.Fatalf("%q → %d, want 400", q, w.Code)
|
|
}
|
|
}
|
|
}
|