Files
zooqueen 429a5d4d5e reveal: encrypt to a group, so t+1 can open it and no one can
The estate could hold a secret no single party could sign away, and had
nowhere to put a secret no single party could READ. tfhe's committee is
the shape of that but not the substance — its PartialDecrypt says so in
its own comment, returns an HMAC tag rather than a partial, and its
combine calls a single-party decrypt with the whole key. It panics
outside tests, deliberately, so nothing was ever misled by it. What was
missing was the primitive underneath.

This is hashed ElGamal over the group the DKG already produced. A party
answers a ciphertext with its share acting on the ephemeral point — one
point, not the share — and the requester interpolates the points. The
secret exists only as the thing the shares would sum to, which is the
property a threshold is bought for.

Every answer carries a Chaum-Pedersen proof that the point really is
that party's share acting on R, and combining verifies each one. Without
it a single wrong answer yields a key that is merely wrong, and a wrong
key is indistinguishable from a wrong ciphertext: the AEAD fails and
nothing says which party was at fault. The proof also binds the party's
name, so one valid answer cannot be replayed under another's — otherwise
t+1 answers could come from fewer than t+1 parties.

Answers are counted by party and not by arrival, because a set that
counted one party three times would open a 2-of-5 with one share.

The tests state the properties: any t+1 reach the same message, t reach
nothing, a wrong answer is named, a stolen answer is refused, another
group's shares do not open it, and a body moved onto another ephemeral
does not open. The last one runs the real FROST keygen and opens with
what it hands back — the algebra agreeing with itself proves nothing
about whether it agrees with the DKG's index convention.

The two go.sum lines are hashes for modules already required, without
which this package cannot build.
2026-08-20 13:06:33 -07:00

96 lines
2.6 KiB
Go

package reveal_test
// The primitive against a REAL share set.
//
// Everything in reveal_test.go deals its own shares, which proves the algebra
// but not that the algebra matches what the DKG actually produces. A share set
// is a polynomial evaluated at party indices, and the one thing that has to
// agree between the DKG, the Lagrange helper and this protocol is WHICH index —
// disagree and every open silently yields the wrong point.
//
// So this runs the keygen the fleet runs, and opens with what it hands back.
import (
"bytes"
"crypto/rand"
"testing"
"github.com/luxfi/threshold/internal/test"
"github.com/luxfi/threshold/pkg/math/curve"
"github.com/luxfi/threshold/pkg/party"
"github.com/luxfi/threshold/pkg/protocol"
"github.com/luxfi/threshold/protocols/frost"
"github.com/luxfi/threshold/protocols/reveal"
)
func TestARealKeygensSharesOpenIt(t *testing.T) {
const n, threshold = 3, 2
var configs map[party.ID]*frost.Config
pt := &test.ProtocolTest{
Name: "reveal-over-frost",
PartyCount: n,
Threshold: threshold,
SessionID: []byte("reveal-over-frost"),
CreateStart: func(id party.ID, ids []party.ID, threshold int) protocol.StartFunc {
return frost.Keygen(curve.Secp256k1{}, id, ids, threshold)
},
Validate: func(t *testing.T, results map[party.ID]interface{}) {
configs = make(map[party.ID]*frost.Config, len(results))
for id, r := range results {
c, ok := r.(*frost.Config)
if !ok {
t.Fatalf("party %s produced %T", id, r)
}
configs[id] = c
}
},
}
pt.Run(t)
if len(configs) != n {
t.Fatalf("keygen produced %d configs, want %d", len(configs), n)
}
var any *frost.Config
for _, c := range configs {
any = c
break
}
root := make([]byte, 32)
if _, err := rand.Read(root); err != nil {
t.Fatal(err)
}
ct, err := reveal.Encrypt(rand.Reader, any.PublicKey, root)
if err != nil {
t.Fatal(err)
}
// threshold+1 of the real parties answer with their real shares.
var answers []*reveal.Answer
for id, c := range configs {
if len(answers) == threshold+1 {
break
}
a, err := ct.Answer(rand.Reader, id, c.PrivateShare)
if err != nil {
t.Fatal(err)
}
answers = append(answers, a)
}
got, err := reveal.Open(ct, threshold, any.VerificationShares, answers)
if err != nil {
t.Fatalf("open with real shares: %v", err)
}
if !bytes.Equal(got, root) {
t.Fatal("the root did not survive a real share set")
}
// And threshold alone still does not.
if _, err := reveal.Open(ct, threshold, any.VerificationShares, answers[:threshold]); err == nil {
t.Fatal("threshold parties opened it")
}
}