mirror of
https://github.com/luxfi/fhe.git
synced 2026-07-26 23:16:08 +00:00
rip(demos): delete cmd/demos/ — kill -tags demos, one way only
The 8 demo mains imported github.com/hanzoai/base (HTTP server scaffold) which pulled the legacy Google Cloud / temporal / grpc transitive chain. Demos were the only consumers of that chain in the fhe tree. One way only: fhe is a cryptographic library, not an app harness. App authors who want HTTP wiring can vendor hanzoai/base directly in their own cmd binary — fhe doesn't ship that scaffold.
This commit is contained in:
@@ -1,102 +0,0 @@
|
|||||||
# FHE Demo Programs
|
|
||||||
|
|
||||||
Seven runnable demos demonstrating Fully Homomorphic Encryption use cases for digital securities.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ~/work/lux/fhe
|
|
||||||
go build ./... # verify the fhe package compiles
|
|
||||||
```
|
|
||||||
|
|
||||||
## Demos
|
|
||||||
|
|
||||||
### 1. Dark Pool (Encrypted Order Matching)
|
|
||||||
|
|
||||||
Traders submit encrypted limit orders. The matching engine compares bids against asks homomorphically. Only matched fills are decrypted.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go run ./cmd/demos/darkpool
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Programmable Compliance
|
|
||||||
|
|
||||||
Verifies SEC diversification rules on an encrypted portfolio: no single position > 25%, total exposure < 80%. Only the boolean compliance result is revealed.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go run ./cmd/demos/compliance
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Private Market Making
|
|
||||||
|
|
||||||
Three market makers submit encrypted bid/ask quotes. Best bid (highest) and best ask (lowest) are selected via encrypted comparisons. Losing quotes stay private.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go run ./cmd/demos/marketmaker
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Encrypted Sealed-Bid Auction
|
|
||||||
|
|
||||||
Five participants submit encrypted bids. The winner is determined via tournament-style max comparisons. Only the winning bid is decrypted.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go run ./cmd/demos/auction
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Private Shareholder Voting
|
|
||||||
|
|
||||||
100 shareholders vote encrypted yes/no. Votes are tallied using a ripple-carry adder (XOR + AND gates). Only the final count is decrypted.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go run ./cmd/demos/voting
|
|
||||||
go run ./cmd/demos/voting -voters 50 -yes 30
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. Confidential NAV
|
|
||||||
|
|
||||||
An ETF's encrypted holdings (10 positions with share counts and prices) are used to compute NAV = sum(shares * price) / totalShares. Only the final NAV is revealed.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go run ./cmd/demos/nav
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. Compliance Proof
|
|
||||||
|
|
||||||
Proves two conditions on an encrypted portfolio: max position < 25% and no sanctioned counterparty. Outputs plaintext booleans while inputs stay encrypted.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go run ./cmd/demos/proof
|
|
||||||
```
|
|
||||||
|
|
||||||
## Run All
|
|
||||||
|
|
||||||
```bash
|
|
||||||
for d in darkpool compliance marketmaker auction voting nav proof; do
|
|
||||||
echo "=== $d ==="
|
|
||||||
go run ./cmd/demos/$d
|
|
||||||
echo
|
|
||||||
done
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
All demos use the `github.com/luxfi/fhe` package with boolean circuit evaluation:
|
|
||||||
|
|
||||||
- **Parameters**: `PN10QP27` (128-bit security, N=1024)
|
|
||||||
- **Boolean gates**: `Evaluator` with `AND`, `OR`, `XOR`, `NOT`, `ANDNY`, `MAJORITY` + bootstrapping
|
|
||||||
- **Encryption**: `Encryptor.Encrypt(bool)` encrypts individual bits
|
|
||||||
- **Decryption**: `Decryptor.Decrypt(*Ciphertext)` decodes bits (secret key holder only)
|
|
||||||
- **Integers**: 8-bit values represented as `[8]*Ciphertext` (bit-decomposed, LSB first)
|
|
||||||
- **Comparison**: MSB-first bitwise less-than circuit using `ANDNY` + `XOR` + `AND` + `OR`
|
|
||||||
- **Addition**: Ripple-carry full adder using `XOR` + `MAJORITY`
|
|
||||||
|
|
||||||
FHE operations run without the secret key. The evaluator uses bootstrap keys (public) for noise management via programmable bootstrapping after each gate.
|
|
||||||
|
|
||||||
## Performance (Apple M-series, PN10QP27)
|
|
||||||
|
|
||||||
- **Key generation**: ~600ms-1s (one-time)
|
|
||||||
- **Encryption**: ~5ms per 8-bit value (8 bit encryptions)
|
|
||||||
- **Single gate** (AND, OR, XOR): ~400-600ms per gate
|
|
||||||
- **8-bit comparison** (lt/gt/ge): ~12-16s (24 gates: 8x ANDNY + 8x XOR + 7x AND + 7x OR)
|
|
||||||
- **8-bit addition**: ~10-13s per add (24 gates: 8x XOR + 8x XOR + 8x MAJORITY)
|
|
||||||
- **Vote tally** (1 ballot): ~5.8s (14 gates: 7x XOR + 7x AND for 7-bit accumulator)
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
//go:build demos
|
|
||||||
|
|
||||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
||||||
// See the file LICENSE for licensing terms.
|
|
||||||
|
|
||||||
// Command auction demonstrates an encrypted sealed-bid auction using FHE.
|
|
||||||
//
|
|
||||||
// Participants submit encrypted sealed bids. The system determines the winner
|
|
||||||
// via boolean circuit max computation. Only the winning bid is decrypted.
|
|
||||||
//
|
|
||||||
// Usage:
|
|
||||||
//
|
|
||||||
// go run ./cmd/demos/auction serve
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/hanzoai/base"
|
|
||||||
"github.com/hanzoai/base/core"
|
|
||||||
"github.com/luxfi/fhe"
|
|
||||||
)
|
|
||||||
|
|
||||||
const bidBits = 8
|
|
||||||
|
|
||||||
type encBid struct {
|
|
||||||
ID string
|
|
||||||
Participant string
|
|
||||||
EncAmount [8]*fhe.Ciphertext
|
|
||||||
}
|
|
||||||
|
|
||||||
type winnerResult struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Participant string `json:"participant"`
|
|
||||||
Amount uint8 `json:"amount"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type fheState struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
enc *fhe.Encryptor
|
|
||||||
dec *fhe.Decryptor
|
|
||||||
eval *fhe.Evaluator
|
|
||||||
bids []encBid
|
|
||||||
winner *winnerResult
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
app := base.New()
|
|
||||||
|
|
||||||
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
|
||||||
st, err := initFHE()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("fhe init: %w", err)
|
|
||||||
}
|
|
||||||
registerRoutes(se, st)
|
|
||||||
return se.Next()
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := app.Start(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initFHE() (*fheState, error) {
|
|
||||||
params, err := fhe.NewParametersFromLiteral(fhe.PN10QP27)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
keygen := fhe.NewKeyGenerator(params)
|
|
||||||
sk, _ := keygen.GenKeyPair()
|
|
||||||
bsk := keygen.GenBootstrapKey(sk)
|
|
||||||
return &fheState{
|
|
||||||
enc: fhe.NewEncryptor(params, sk),
|
|
||||||
dec: fhe.NewDecryptor(params, sk),
|
|
||||||
eval: fhe.NewEvaluator(params, bsk),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerRoutes(se *core.ServeEvent, st *fheState) {
|
|
||||||
g := se.Router.Group("/v1")
|
|
||||||
|
|
||||||
// POST /v1/bids — submit sealed encrypted bid
|
|
||||||
g.POST("/bids", func(re *core.RequestEvent) error {
|
|
||||||
var req struct {
|
|
||||||
Participant string `json:"participant"`
|
|
||||||
Amount uint8 `json:"amount"`
|
|
||||||
}
|
|
||||||
if err := re.BindBody(&req); err != nil {
|
|
||||||
return re.JSON(400, map[string]string{"error": "invalid body"})
|
|
||||||
}
|
|
||||||
if req.Participant == "" {
|
|
||||||
return re.JSON(400, map[string]string{"error": "participant required"})
|
|
||||||
}
|
|
||||||
|
|
||||||
st.mu.Lock()
|
|
||||||
defer st.mu.Unlock()
|
|
||||||
|
|
||||||
b := encBid{
|
|
||||||
ID: uuid.NewString(),
|
|
||||||
Participant: req.Participant,
|
|
||||||
EncAmount: encryptByte(st.enc, req.Amount),
|
|
||||||
}
|
|
||||||
st.bids = append(st.bids, b)
|
|
||||||
st.winner = nil
|
|
||||||
|
|
||||||
return re.JSON(201, map[string]string{
|
|
||||||
"id": b.ID,
|
|
||||||
"participant": b.Participant,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// POST /v1/determine-winner — homomorphic max comparison
|
|
||||||
g.POST("/determine-winner", func(re *core.RequestEvent) error {
|
|
||||||
st.mu.Lock()
|
|
||||||
defer st.mu.Unlock()
|
|
||||||
|
|
||||||
if len(st.bids) < 2 {
|
|
||||||
return re.JSON(400, map[string]string{"error": "need at least 2 bids"})
|
|
||||||
}
|
|
||||||
|
|
||||||
winnerIdx := 0
|
|
||||||
for i := 1; i < len(st.bids); i++ {
|
|
||||||
gtResult, err := gtEncrypted(st.eval, st.bids[i].EncAmount, st.bids[winnerIdx].EncAmount)
|
|
||||||
if err != nil {
|
|
||||||
return re.JSON(500, map[string]string{"error": err.Error()})
|
|
||||||
}
|
|
||||||
if st.dec.Decrypt(gtResult) {
|
|
||||||
winnerIdx = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
st.winner = &winnerResult{
|
|
||||||
ID: st.bids[winnerIdx].ID,
|
|
||||||
Participant: st.bids[winnerIdx].Participant,
|
|
||||||
}
|
|
||||||
|
|
||||||
return re.JSON(200, map[string]string{
|
|
||||||
"winner_id": st.winner.ID,
|
|
||||||
"participant": st.winner.Participant,
|
|
||||||
"message": "winner determined, call POST /v1/reveal to decrypt amount",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// POST /v1/reveal — decrypt winning bid only
|
|
||||||
g.POST("/reveal", func(re *core.RequestEvent) error {
|
|
||||||
st.mu.Lock()
|
|
||||||
defer st.mu.Unlock()
|
|
||||||
|
|
||||||
if st.winner == nil {
|
|
||||||
return re.JSON(400, map[string]string{"error": "no winner determined yet"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the winner's encrypted bid
|
|
||||||
for _, b := range st.bids {
|
|
||||||
if b.ID == st.winner.ID {
|
|
||||||
st.winner.Amount = decryptByte(st.dec, b.EncAmount)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return re.JSON(200, st.winner)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func encryptByte(enc *fhe.Encryptor, v uint8) [8]*fhe.Ciphertext {
|
|
||||||
var bits [8]*fhe.Ciphertext
|
|
||||||
for i := 0; i < 8; i++ {
|
|
||||||
bits[i] = enc.Encrypt((v>>i)&1 == 1)
|
|
||||||
}
|
|
||||||
return bits
|
|
||||||
}
|
|
||||||
|
|
||||||
func decryptByte(dec *fhe.Decryptor, bits [8]*fhe.Ciphertext) uint8 {
|
|
||||||
var v uint8
|
|
||||||
for i := 0; i < 8; i++ {
|
|
||||||
if dec.Decrypt(bits[i]) {
|
|
||||||
v |= 1 << i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func ltEncrypted(eval *fhe.Evaluator, a, b [8]*fhe.Ciphertext) (*fhe.Ciphertext, error) {
|
|
||||||
var isLess, isEqual *fhe.Ciphertext
|
|
||||||
for i := bidBits - 1; i >= 0; i-- {
|
|
||||||
bitLt, err := eval.ANDNY(a[i], b[i])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
bitXor, err := eval.XOR(a[i], b[i])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
bitEq := eval.NOT(bitXor)
|
|
||||||
if isLess == nil {
|
|
||||||
isLess = bitLt
|
|
||||||
isEqual = bitEq
|
|
||||||
} else {
|
|
||||||
eqAndLt, err := eval.AND(isEqual, bitLt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
isLess, err = eval.OR(isLess, eqAndLt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
isEqual, err = eval.AND(isEqual, bitEq)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return isLess, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func gtEncrypted(eval *fhe.Evaluator, a, b [8]*fhe.Ciphertext) (*fhe.Ciphertext, error) {
|
|
||||||
return ltEncrypted(eval, b, a)
|
|
||||||
}
|
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
//go:build demos
|
|
||||||
|
|
||||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
||||||
// See the file LICENSE for licensing terms.
|
|
||||||
|
|
||||||
// Command compliance demonstrates programmable compliance checks on encrypted portfolios.
|
|
||||||
//
|
|
||||||
// A portfolio of positions is encrypted. The system verifies SEC diversification
|
|
||||||
// rules (no single position > 25% of total, total exposure < 80%) entirely on
|
|
||||||
// encrypted data using boolean circuit comparisons. Only the boolean results are
|
|
||||||
// decrypted.
|
|
||||||
//
|
|
||||||
// Usage:
|
|
||||||
//
|
|
||||||
// go run ./cmd/demos/compliance serve
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/hanzoai/base"
|
|
||||||
"github.com/hanzoai/base/core"
|
|
||||||
"github.com/luxfi/fhe"
|
|
||||||
)
|
|
||||||
|
|
||||||
const valueBits = 8
|
|
||||||
|
|
||||||
type encPosition struct {
|
|
||||||
Name string
|
|
||||||
EncWeight [8]*fhe.Ciphertext
|
|
||||||
}
|
|
||||||
|
|
||||||
type fheState struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
enc *fhe.Encryptor
|
|
||||||
dec *fhe.Decryptor
|
|
||||||
eval *fhe.Evaluator
|
|
||||||
portfolio []encPosition
|
|
||||||
result *complianceResult
|
|
||||||
}
|
|
||||||
|
|
||||||
type complianceResult struct {
|
|
||||||
Diversification bool `json:"diversification"`
|
|
||||||
ExposureUnder80 bool `json:"exposure_under_80"`
|
|
||||||
Overall bool `json:"overall"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
app := base.New()
|
|
||||||
|
|
||||||
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
|
||||||
st, err := initFHE()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("fhe init: %w", err)
|
|
||||||
}
|
|
||||||
registerRoutes(se, st)
|
|
||||||
return se.Next()
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := app.Start(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initFHE() (*fheState, error) {
|
|
||||||
params, err := fhe.NewParametersFromLiteral(fhe.PN10QP27)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
keygen := fhe.NewKeyGenerator(params)
|
|
||||||
sk, _ := keygen.GenKeyPair()
|
|
||||||
bsk := keygen.GenBootstrapKey(sk)
|
|
||||||
return &fheState{
|
|
||||||
enc: fhe.NewEncryptor(params, sk),
|
|
||||||
dec: fhe.NewDecryptor(params, sk),
|
|
||||||
eval: fhe.NewEvaluator(params, bsk),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerRoutes(se *core.ServeEvent, st *fheState) {
|
|
||||||
g := se.Router.Group("/v1")
|
|
||||||
|
|
||||||
// POST /v1/portfolio — submit encrypted holdings
|
|
||||||
g.POST("/portfolio", func(re *core.RequestEvent) error {
|
|
||||||
var req struct {
|
|
||||||
Positions []struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Weight uint8 `json:"weight"`
|
|
||||||
} `json:"positions"`
|
|
||||||
}
|
|
||||||
if err := re.BindBody(&req); err != nil {
|
|
||||||
return re.JSON(400, map[string]string{"error": "invalid body"})
|
|
||||||
}
|
|
||||||
if len(req.Positions) == 0 {
|
|
||||||
return re.JSON(400, map[string]string{"error": "positions required"})
|
|
||||||
}
|
|
||||||
|
|
||||||
st.mu.Lock()
|
|
||||||
defer st.mu.Unlock()
|
|
||||||
|
|
||||||
st.portfolio = nil
|
|
||||||
st.result = nil
|
|
||||||
for _, p := range req.Positions {
|
|
||||||
st.portfolio = append(st.portfolio, encPosition{
|
|
||||||
Name: p.Name,
|
|
||||||
EncWeight: encryptByte(st.enc, p.Weight),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return re.JSON(201, map[string]any{
|
|
||||||
"count": len(st.portfolio),
|
|
||||||
"message": "portfolio encrypted",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// POST /v1/check — run SEC diversification check homomorphically
|
|
||||||
g.POST("/check", func(re *core.RequestEvent) error {
|
|
||||||
st.mu.Lock()
|
|
||||||
defer st.mu.Unlock()
|
|
||||||
|
|
||||||
if len(st.portfolio) == 0 {
|
|
||||||
return re.JSON(400, map[string]string{"error": "no portfolio loaded"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check 1: no single position > 25%
|
|
||||||
encThreshold := encryptByte(st.enc, 25)
|
|
||||||
allCompliant := st.enc.Encrypt(true)
|
|
||||||
for _, p := range st.portfolio {
|
|
||||||
leResult, err := leEncrypted(st.eval, p.EncWeight, encThreshold)
|
|
||||||
if err != nil {
|
|
||||||
return re.JSON(500, map[string]string{"error": err.Error()})
|
|
||||||
}
|
|
||||||
allCompliant, err = st.eval.AND(allCompliant, leResult)
|
|
||||||
if err != nil {
|
|
||||||
return re.JSON(500, map[string]string{"error": err.Error()})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
check1 := st.dec.Decrypt(allCompliant)
|
|
||||||
|
|
||||||
// Check 2: total exposure < 80%
|
|
||||||
encTotal := st.portfolio[0].EncWeight
|
|
||||||
var err error
|
|
||||||
for i := 1; i < len(st.portfolio); i++ {
|
|
||||||
encTotal, err = addBytes(st.eval, st.enc, encTotal, st.portfolio[i].EncWeight)
|
|
||||||
if err != nil {
|
|
||||||
return re.JSON(500, map[string]string{"error": err.Error()})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
encMaxExposure := encryptByte(st.enc, 80)
|
|
||||||
ltResult, err := ltEncrypted(st.eval, encTotal, encMaxExposure)
|
|
||||||
if err != nil {
|
|
||||||
return re.JSON(500, map[string]string{"error": err.Error()})
|
|
||||||
}
|
|
||||||
check2 := st.dec.Decrypt(ltResult)
|
|
||||||
|
|
||||||
st.result = &complianceResult{
|
|
||||||
Diversification: check1,
|
|
||||||
ExposureUnder80: check2,
|
|
||||||
Overall: check1 && check2,
|
|
||||||
}
|
|
||||||
|
|
||||||
return re.JSON(200, st.result)
|
|
||||||
})
|
|
||||||
|
|
||||||
// GET /v1/result — get compliance result (boolean, not holdings)
|
|
||||||
g.GET("/result", func(re *core.RequestEvent) error {
|
|
||||||
st.mu.Lock()
|
|
||||||
defer st.mu.Unlock()
|
|
||||||
|
|
||||||
if st.result == nil {
|
|
||||||
return re.JSON(404, map[string]string{"error": "no check run yet"})
|
|
||||||
}
|
|
||||||
|
|
||||||
return re.JSON(200, st.result)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func encryptByte(enc *fhe.Encryptor, v uint8) [8]*fhe.Ciphertext {
|
|
||||||
var bits [8]*fhe.Ciphertext
|
|
||||||
for i := 0; i < 8; i++ {
|
|
||||||
bits[i] = enc.Encrypt((v>>i)&1 == 1)
|
|
||||||
}
|
|
||||||
return bits
|
|
||||||
}
|
|
||||||
|
|
||||||
func addBytes(eval *fhe.Evaluator, enc *fhe.Encryptor, a, b [8]*fhe.Ciphertext) ([8]*fhe.Ciphertext, error) {
|
|
||||||
carry := enc.Encrypt(false)
|
|
||||||
var result [8]*fhe.Ciphertext
|
|
||||||
for i := 0; i < 8; i++ {
|
|
||||||
abXor, err := eval.XOR(a[i], b[i])
|
|
||||||
if err != nil {
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
sum, err := eval.XOR(abXor, carry)
|
|
||||||
if err != nil {
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
carry, err = eval.MAJORITY(a[i], b[i], carry)
|
|
||||||
if err != nil {
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
result[i] = sum
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ltEncrypted(eval *fhe.Evaluator, a, b [8]*fhe.Ciphertext) (*fhe.Ciphertext, error) {
|
|
||||||
var isLess, isEqual *fhe.Ciphertext
|
|
||||||
for i := valueBits - 1; i >= 0; i-- {
|
|
||||||
bitLt, err := eval.ANDNY(a[i], b[i])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
bitXor, err := eval.XOR(a[i], b[i])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
bitEq := eval.NOT(bitXor)
|
|
||||||
if isLess == nil {
|
|
||||||
isLess = bitLt
|
|
||||||
isEqual = bitEq
|
|
||||||
} else {
|
|
||||||
eqAndLt, err := eval.AND(isEqual, bitLt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
isLess, err = eval.OR(isLess, eqAndLt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
isEqual, err = eval.AND(isEqual, bitEq)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return isLess, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func leEncrypted(eval *fhe.Evaluator, a, b [8]*fhe.Ciphertext) (*fhe.Ciphertext, error) {
|
|
||||||
bLtA, err := ltEncrypted(eval, b, a)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return eval.NOT(bLtA), nil
|
|
||||||
}
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
//go:build demos
|
|
||||||
|
|
||||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
||||||
// See the file LICENSE for licensing terms.
|
|
||||||
|
|
||||||
// Command darkpool demonstrates an encrypted dark pool order matching system.
|
|
||||||
//
|
|
||||||
// Traders submit encrypted limit orders (price + quantity). The matching engine
|
|
||||||
// compares encrypted bids against encrypted asks using FHE boolean circuits.
|
|
||||||
// Only matched fills are decrypted -- unmatched orders remain private.
|
|
||||||
//
|
|
||||||
// Usage:
|
|
||||||
//
|
|
||||||
// go run ./cmd/demos/darkpool serve
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/hanzoai/base"
|
|
||||||
"github.com/hanzoai/base/core"
|
|
||||||
"github.com/luxfi/fhe"
|
|
||||||
)
|
|
||||||
|
|
||||||
const priceBits = 8
|
|
||||||
|
|
||||||
type encOrder struct {
|
|
||||||
ID string
|
|
||||||
Side string // "bid" or "ask"
|
|
||||||
EncPrice [8]*fhe.Ciphertext
|
|
||||||
EncQty [8]*fhe.Ciphertext
|
|
||||||
}
|
|
||||||
|
|
||||||
type matchResult struct {
|
|
||||||
BidID string `json:"bid_id"`
|
|
||||||
AskID string `json:"ask_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type fheState struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
params fhe.Parameters
|
|
||||||
enc *fhe.Encryptor
|
|
||||||
dec *fhe.Decryptor
|
|
||||||
eval *fhe.Evaluator
|
|
||||||
orders map[string]*encOrder
|
|
||||||
matches []matchResult
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
app := base.New()
|
|
||||||
|
|
||||||
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
|
||||||
st, err := initFHE()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("fhe init: %w", err)
|
|
||||||
}
|
|
||||||
registerRoutes(se, st)
|
|
||||||
return se.Next()
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := app.Start(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initFHE() (*fheState, error) {
|
|
||||||
params, err := fhe.NewParametersFromLiteral(fhe.PN10QP27)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
keygen := fhe.NewKeyGenerator(params)
|
|
||||||
sk, _ := keygen.GenKeyPair()
|
|
||||||
bsk := keygen.GenBootstrapKey(sk)
|
|
||||||
return &fheState{
|
|
||||||
params: params,
|
|
||||||
enc: fhe.NewEncryptor(params, sk),
|
|
||||||
dec: fhe.NewDecryptor(params, sk),
|
|
||||||
eval: fhe.NewEvaluator(params, bsk),
|
|
||||||
orders: make(map[string]*encOrder),
|
|
||||||
matches: nil,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerRoutes(se *core.ServeEvent, st *fheState) {
|
|
||||||
g := se.Router.Group("/v1")
|
|
||||||
|
|
||||||
// POST /v1/orders — submit encrypted order
|
|
||||||
g.POST("/orders", func(re *core.RequestEvent) error {
|
|
||||||
var req struct {
|
|
||||||
Side string `json:"side"`
|
|
||||||
Price uint8 `json:"price"`
|
|
||||||
Qty uint8 `json:"qty"`
|
|
||||||
}
|
|
||||||
if err := re.BindBody(&req); err != nil {
|
|
||||||
return re.JSON(400, map[string]string{"error": "invalid body"})
|
|
||||||
}
|
|
||||||
if req.Side != "bid" && req.Side != "ask" {
|
|
||||||
return re.JSON(400, map[string]string{"error": "side must be bid or ask"})
|
|
||||||
}
|
|
||||||
|
|
||||||
st.mu.Lock()
|
|
||||||
defer st.mu.Unlock()
|
|
||||||
|
|
||||||
id := uuid.NewString()
|
|
||||||
o := &encOrder{
|
|
||||||
ID: id,
|
|
||||||
Side: req.Side,
|
|
||||||
EncPrice: encryptByte(st.enc, req.Price),
|
|
||||||
EncQty: encryptByte(st.enc, req.Qty),
|
|
||||||
}
|
|
||||||
st.orders[id] = o
|
|
||||||
|
|
||||||
return re.JSON(201, map[string]string{"id": id, "side": req.Side})
|
|
||||||
})
|
|
||||||
|
|
||||||
// GET /v1/orders — list orders (IDs + side only, values encrypted)
|
|
||||||
g.GET("/orders", func(re *core.RequestEvent) error {
|
|
||||||
st.mu.Lock()
|
|
||||||
defer st.mu.Unlock()
|
|
||||||
|
|
||||||
type entry struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Side string `json:"side"`
|
|
||||||
EncPrice string `json:"enc_price"`
|
|
||||||
}
|
|
||||||
var out []entry
|
|
||||||
for _, o := range st.orders {
|
|
||||||
out = append(out, entry{
|
|
||||||
ID: o.ID,
|
|
||||||
Side: o.Side,
|
|
||||||
EncPrice: "(encrypted)",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return re.JSON(200, out)
|
|
||||||
})
|
|
||||||
|
|
||||||
// POST /v1/match — run homomorphic matching on all encrypted orders
|
|
||||||
g.POST("/match", func(re *core.RequestEvent) error {
|
|
||||||
st.mu.Lock()
|
|
||||||
defer st.mu.Unlock()
|
|
||||||
|
|
||||||
var bids, asks []*encOrder
|
|
||||||
for _, o := range st.orders {
|
|
||||||
if o.Side == "bid" {
|
|
||||||
bids = append(bids, o)
|
|
||||||
} else {
|
|
||||||
asks = append(asks, o)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
st.matches = nil
|
|
||||||
for _, bid := range bids {
|
|
||||||
for _, ask := range asks {
|
|
||||||
isGe, err := geEncrypted(st.eval, bid.EncPrice, ask.EncPrice)
|
|
||||||
if err != nil {
|
|
||||||
return re.JSON(500, map[string]string{"error": err.Error()})
|
|
||||||
}
|
|
||||||
if st.dec.Decrypt(isGe) {
|
|
||||||
st.matches = append(st.matches, matchResult{
|
|
||||||
BidID: bid.ID,
|
|
||||||
AskID: ask.ID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return re.JSON(200, map[string]any{
|
|
||||||
"matches": st.matches,
|
|
||||||
"count": len(st.matches),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// POST /v1/reveal/{id} — decrypt an order by ID
|
|
||||||
g.POST("/reveal/{id}", func(re *core.RequestEvent) error {
|
|
||||||
id := re.Request.PathValue("id")
|
|
||||||
|
|
||||||
st.mu.Lock()
|
|
||||||
defer st.mu.Unlock()
|
|
||||||
|
|
||||||
o, ok := st.orders[id]
|
|
||||||
if !ok {
|
|
||||||
return re.JSON(404, map[string]string{"error": "order not found"})
|
|
||||||
}
|
|
||||||
|
|
||||||
price := decryptByte(st.dec, o.EncPrice)
|
|
||||||
qty := decryptByte(st.dec, o.EncQty)
|
|
||||||
|
|
||||||
return re.JSON(200, map[string]any{
|
|
||||||
"id": o.ID,
|
|
||||||
"side": o.Side,
|
|
||||||
"price": price,
|
|
||||||
"qty": qty,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func encryptByte(enc *fhe.Encryptor, v uint8) [8]*fhe.Ciphertext {
|
|
||||||
var bits [8]*fhe.Ciphertext
|
|
||||||
for i := 0; i < 8; i++ {
|
|
||||||
bits[i] = enc.Encrypt((v>>i)&1 == 1)
|
|
||||||
}
|
|
||||||
return bits
|
|
||||||
}
|
|
||||||
|
|
||||||
func decryptByte(dec *fhe.Decryptor, bits [8]*fhe.Ciphertext) uint8 {
|
|
||||||
var v uint8
|
|
||||||
for i := 0; i < 8; i++ {
|
|
||||||
if dec.Decrypt(bits[i]) {
|
|
||||||
v |= 1 << i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// geEncrypted computes a >= b on encrypted 8-bit values using MSB-first comparison.
|
|
||||||
func geEncrypted(eval *fhe.Evaluator, a, b [8]*fhe.Ciphertext) (*fhe.Ciphertext, error) {
|
|
||||||
var isLess, isEqual *fhe.Ciphertext
|
|
||||||
|
|
||||||
for i := priceBits - 1; i >= 0; i-- {
|
|
||||||
bitLt, err := eval.ANDNY(a[i], b[i])
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("bit %d ANDNY: %w", i, err)
|
|
||||||
}
|
|
||||||
bitXor, err := eval.XOR(a[i], b[i])
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("bit %d XOR: %w", i, err)
|
|
||||||
}
|
|
||||||
bitEq := eval.NOT(bitXor)
|
|
||||||
|
|
||||||
if isLess == nil {
|
|
||||||
isLess = bitLt
|
|
||||||
isEqual = bitEq
|
|
||||||
} else {
|
|
||||||
eqAndLt, err := eval.AND(isEqual, bitLt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
isLess, err = eval.OR(isLess, eqAndLt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
isEqual, err = eval.AND(isEqual, bitEq)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return eval.NOT(isLess), nil
|
|
||||||
}
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
//go:build demos
|
|
||||||
|
|
||||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
||||||
// See the file LICENSE for licensing terms.
|
|
||||||
|
|
||||||
// Command marketmaker demonstrates private market making with FHE.
|
|
||||||
//
|
|
||||||
// Market makers submit encrypted bid/ask quotes. The system selects the best
|
|
||||||
// bid (highest) and best ask (lowest) via boolean circuit comparisons.
|
|
||||||
// Only the winning quotes are decrypted -- losing quotes stay private.
|
|
||||||
//
|
|
||||||
// Usage:
|
|
||||||
//
|
|
||||||
// go run ./cmd/demos/marketmaker serve
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/hanzoai/base"
|
|
||||||
"github.com/hanzoai/base/core"
|
|
||||||
"github.com/luxfi/fhe"
|
|
||||||
)
|
|
||||||
|
|
||||||
const priceBits = 8
|
|
||||||
|
|
||||||
type encQuote struct {
|
|
||||||
ID string
|
|
||||||
Maker string
|
|
||||||
EncBid [8]*fhe.Ciphertext
|
|
||||||
EncAsk [8]*fhe.Ciphertext
|
|
||||||
}
|
|
||||||
|
|
||||||
type spreadResult struct {
|
|
||||||
BestBidMaker string `json:"best_bid_maker"`
|
|
||||||
BestBidPrice uint8 `json:"best_bid_price"`
|
|
||||||
BestAskMaker string `json:"best_ask_maker"`
|
|
||||||
BestAskPrice uint8 `json:"best_ask_price"`
|
|
||||||
Spread int `json:"spread"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type fheState struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
enc *fhe.Encryptor
|
|
||||||
dec *fhe.Decryptor
|
|
||||||
eval *fhe.Evaluator
|
|
||||||
quotes []encQuote
|
|
||||||
spread *spreadResult
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
app := base.New()
|
|
||||||
|
|
||||||
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
|
||||||
st, err := initFHE()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("fhe init: %w", err)
|
|
||||||
}
|
|
||||||
registerRoutes(se, st)
|
|
||||||
return se.Next()
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := app.Start(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initFHE() (*fheState, error) {
|
|
||||||
params, err := fhe.NewParametersFromLiteral(fhe.PN10QP27)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
keygen := fhe.NewKeyGenerator(params)
|
|
||||||
sk, _ := keygen.GenKeyPair()
|
|
||||||
bsk := keygen.GenBootstrapKey(sk)
|
|
||||||
return &fheState{
|
|
||||||
enc: fhe.NewEncryptor(params, sk),
|
|
||||||
dec: fhe.NewDecryptor(params, sk),
|
|
||||||
eval: fhe.NewEvaluator(params, bsk),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerRoutes(se *core.ServeEvent, st *fheState) {
|
|
||||||
g := se.Router.Group("/v1")
|
|
||||||
|
|
||||||
// POST /v1/quotes — submit encrypted quote
|
|
||||||
g.POST("/quotes", func(re *core.RequestEvent) error {
|
|
||||||
var req struct {
|
|
||||||
Maker string `json:"maker"`
|
|
||||||
Bid uint8 `json:"bid"`
|
|
||||||
Ask uint8 `json:"ask"`
|
|
||||||
}
|
|
||||||
if err := re.BindBody(&req); err != nil {
|
|
||||||
return re.JSON(400, map[string]string{"error": "invalid body"})
|
|
||||||
}
|
|
||||||
if req.Maker == "" {
|
|
||||||
return re.JSON(400, map[string]string{"error": "maker required"})
|
|
||||||
}
|
|
||||||
|
|
||||||
st.mu.Lock()
|
|
||||||
defer st.mu.Unlock()
|
|
||||||
|
|
||||||
q := encQuote{
|
|
||||||
ID: uuid.NewString(),
|
|
||||||
Maker: req.Maker,
|
|
||||||
EncBid: encryptByte(st.enc, req.Bid),
|
|
||||||
EncAsk: encryptByte(st.enc, req.Ask),
|
|
||||||
}
|
|
||||||
st.quotes = append(st.quotes, q)
|
|
||||||
st.spread = nil // invalidate cached spread
|
|
||||||
|
|
||||||
return re.JSON(201, map[string]string{"id": q.ID, "maker": q.Maker})
|
|
||||||
})
|
|
||||||
|
|
||||||
// POST /v1/best — compute best bid/ask homomorphically
|
|
||||||
g.POST("/best", func(re *core.RequestEvent) error {
|
|
||||||
st.mu.Lock()
|
|
||||||
defer st.mu.Unlock()
|
|
||||||
|
|
||||||
if len(st.quotes) < 2 {
|
|
||||||
return re.JSON(400, map[string]string{"error": "need at least 2 quotes"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Best bid (highest)
|
|
||||||
bestBidIdx := 0
|
|
||||||
for i := 1; i < len(st.quotes); i++ {
|
|
||||||
gtResult, err := gtEncrypted(st.eval, st.quotes[i].EncBid, st.quotes[bestBidIdx].EncBid)
|
|
||||||
if err != nil {
|
|
||||||
return re.JSON(500, map[string]string{"error": err.Error()})
|
|
||||||
}
|
|
||||||
if st.dec.Decrypt(gtResult) {
|
|
||||||
bestBidIdx = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Best ask (lowest)
|
|
||||||
bestAskIdx := 0
|
|
||||||
for i := 1; i < len(st.quotes); i++ {
|
|
||||||
ltResult, err := ltEncrypted(st.eval, st.quotes[i].EncAsk, st.quotes[bestAskIdx].EncAsk)
|
|
||||||
if err != nil {
|
|
||||||
return re.JSON(500, map[string]string{"error": err.Error()})
|
|
||||||
}
|
|
||||||
if st.dec.Decrypt(ltResult) {
|
|
||||||
bestAskIdx = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bestBid := decryptByte(st.dec, st.quotes[bestBidIdx].EncBid)
|
|
||||||
bestAsk := decryptByte(st.dec, st.quotes[bestAskIdx].EncAsk)
|
|
||||||
|
|
||||||
st.spread = &spreadResult{
|
|
||||||
BestBidMaker: st.quotes[bestBidIdx].Maker,
|
|
||||||
BestBidPrice: bestBid,
|
|
||||||
BestAskMaker: st.quotes[bestAskIdx].Maker,
|
|
||||||
BestAskPrice: bestAsk,
|
|
||||||
Spread: int(bestAsk) - int(bestBid),
|
|
||||||
}
|
|
||||||
|
|
||||||
return re.JSON(200, st.spread)
|
|
||||||
})
|
|
||||||
|
|
||||||
// GET /v1/spread — get cached spread
|
|
||||||
g.GET("/spread", func(re *core.RequestEvent) error {
|
|
||||||
st.mu.Lock()
|
|
||||||
defer st.mu.Unlock()
|
|
||||||
|
|
||||||
if st.spread == nil {
|
|
||||||
return re.JSON(404, map[string]string{"error": "no spread computed yet"})
|
|
||||||
}
|
|
||||||
|
|
||||||
return re.JSON(200, st.spread)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func encryptByte(enc *fhe.Encryptor, v uint8) [8]*fhe.Ciphertext {
|
|
||||||
var bits [8]*fhe.Ciphertext
|
|
||||||
for i := 0; i < 8; i++ {
|
|
||||||
bits[i] = enc.Encrypt((v>>i)&1 == 1)
|
|
||||||
}
|
|
||||||
return bits
|
|
||||||
}
|
|
||||||
|
|
||||||
func decryptByte(dec *fhe.Decryptor, bits [8]*fhe.Ciphertext) uint8 {
|
|
||||||
var v uint8
|
|
||||||
for i := 0; i < 8; i++ {
|
|
||||||
if dec.Decrypt(bits[i]) {
|
|
||||||
v |= 1 << i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func ltEncrypted(eval *fhe.Evaluator, a, b [8]*fhe.Ciphertext) (*fhe.Ciphertext, error) {
|
|
||||||
var isLess, isEqual *fhe.Ciphertext
|
|
||||||
for i := priceBits - 1; i >= 0; i-- {
|
|
||||||
bitLt, err := eval.ANDNY(a[i], b[i])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
bitXor, err := eval.XOR(a[i], b[i])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
bitEq := eval.NOT(bitXor)
|
|
||||||
if isLess == nil {
|
|
||||||
isLess = bitLt
|
|
||||||
isEqual = bitEq
|
|
||||||
} else {
|
|
||||||
eqAndLt, err := eval.AND(isEqual, bitLt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
isLess, err = eval.OR(isLess, eqAndLt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
isEqual, err = eval.AND(isEqual, bitEq)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return isLess, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func gtEncrypted(eval *fhe.Evaluator, a, b [8]*fhe.Ciphertext) (*fhe.Ciphertext, error) {
|
|
||||||
return ltEncrypted(eval, b, a)
|
|
||||||
}
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
//go:build demos
|
|
||||||
|
|
||||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
||||||
// See the file LICENSE for licensing terms.
|
|
||||||
|
|
||||||
// Command nav demonstrates confidential Net Asset Value (NAV) computation using FHE.
|
|
||||||
//
|
|
||||||
// An ETF's holdings (positions with share counts and prices) are encrypted.
|
|
||||||
// NAV = sum(holdings * prices) / totalShares is computed on encrypted data using
|
|
||||||
// repeated addition. Only the final NAV is decrypted for the authorized party.
|
|
||||||
//
|
|
||||||
// Usage:
|
|
||||||
//
|
|
||||||
// go run ./cmd/demos/nav serve
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/hanzoai/base"
|
|
||||||
"github.com/hanzoai/base/core"
|
|
||||||
"github.com/luxfi/fhe"
|
|
||||||
)
|
|
||||||
|
|
||||||
type encHolding struct {
|
|
||||||
Ticker string
|
|
||||||
Shares uint8
|
|
||||||
EncPrice [8]*fhe.Ciphertext
|
|
||||||
}
|
|
||||||
|
|
||||||
type navResult struct {
|
|
||||||
TotalValue uint64 `json:"total_value"`
|
|
||||||
SharesOutstanding uint64 `json:"shares_outstanding"`
|
|
||||||
NAVPerShare uint64 `json:"nav_per_share"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type fheState struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
enc *fhe.Encryptor
|
|
||||||
dec *fhe.Decryptor
|
|
||||||
eval *fhe.Evaluator
|
|
||||||
holdings []encHolding
|
|
||||||
sharesOutstanding uint64
|
|
||||||
result *navResult
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
app := base.New()
|
|
||||||
|
|
||||||
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
|
||||||
st, err := initFHE()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("fhe init: %w", err)
|
|
||||||
}
|
|
||||||
registerRoutes(se, st)
|
|
||||||
return se.Next()
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := app.Start(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initFHE() (*fheState, error) {
|
|
||||||
params, err := fhe.NewParametersFromLiteral(fhe.PN10QP27)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
keygen := fhe.NewKeyGenerator(params)
|
|
||||||
sk, _ := keygen.GenKeyPair()
|
|
||||||
bsk := keygen.GenBootstrapKey(sk)
|
|
||||||
return &fheState{
|
|
||||||
enc: fhe.NewEncryptor(params, sk),
|
|
||||||
dec: fhe.NewDecryptor(params, sk),
|
|
||||||
eval: fhe.NewEvaluator(params, bsk),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerRoutes(se *core.ServeEvent, st *fheState) {
|
|
||||||
g := se.Router.Group("/v1")
|
|
||||||
|
|
||||||
// POST /v1/holdings — submit encrypted holdings
|
|
||||||
g.POST("/holdings", func(re *core.RequestEvent) error {
|
|
||||||
var req struct {
|
|
||||||
SharesOutstanding uint64 `json:"shares_outstanding"`
|
|
||||||
Holdings []struct {
|
|
||||||
Ticker string `json:"ticker"`
|
|
||||||
Shares uint8 `json:"shares"`
|
|
||||||
Price uint8 `json:"price"`
|
|
||||||
} `json:"holdings"`
|
|
||||||
}
|
|
||||||
if err := re.BindBody(&req); err != nil {
|
|
||||||
return re.JSON(400, map[string]string{"error": "invalid body"})
|
|
||||||
}
|
|
||||||
if len(req.Holdings) == 0 {
|
|
||||||
return re.JSON(400, map[string]string{"error": "holdings required"})
|
|
||||||
}
|
|
||||||
if req.SharesOutstanding == 0 {
|
|
||||||
return re.JSON(400, map[string]string{"error": "shares_outstanding required"})
|
|
||||||
}
|
|
||||||
|
|
||||||
st.mu.Lock()
|
|
||||||
defer st.mu.Unlock()
|
|
||||||
|
|
||||||
st.holdings = nil
|
|
||||||
st.result = nil
|
|
||||||
st.sharesOutstanding = req.SharesOutstanding
|
|
||||||
|
|
||||||
for _, h := range req.Holdings {
|
|
||||||
st.holdings = append(st.holdings, encHolding{
|
|
||||||
Ticker: h.Ticker,
|
|
||||||
Shares: h.Shares,
|
|
||||||
EncPrice: encryptByte(st.enc, h.Price),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return re.JSON(201, map[string]any{
|
|
||||||
"count": len(st.holdings),
|
|
||||||
"message": "holdings encrypted",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// POST /v1/compute — compute NAV homomorphically
|
|
||||||
g.POST("/compute", func(re *core.RequestEvent) error {
|
|
||||||
st.mu.Lock()
|
|
||||||
defer st.mu.Unlock()
|
|
||||||
|
|
||||||
if len(st.holdings) == 0 {
|
|
||||||
return re.JSON(400, map[string]string{"error": "no holdings loaded"})
|
|
||||||
}
|
|
||||||
|
|
||||||
var encTotal [8]*fhe.Ciphertext
|
|
||||||
initialized := false
|
|
||||||
|
|
||||||
for _, h := range st.holdings {
|
|
||||||
// shares * price via repeated addition
|
|
||||||
posValue := h.EncPrice
|
|
||||||
var err error
|
|
||||||
for s := uint8(1); s < h.Shares; s++ {
|
|
||||||
posValue, err = addBytes(st.eval, st.enc, posValue, h.EncPrice)
|
|
||||||
if err != nil {
|
|
||||||
return re.JSON(500, map[string]string{"error": err.Error()})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !initialized {
|
|
||||||
encTotal = posValue
|
|
||||||
initialized = true
|
|
||||||
} else {
|
|
||||||
encTotal, err = addBytes(st.eval, st.enc, encTotal, posValue)
|
|
||||||
if err != nil {
|
|
||||||
return re.JSON(500, map[string]string{"error": err.Error()})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
decTotal := uint64(decryptByte(st.dec, encTotal))
|
|
||||||
st.result = &navResult{
|
|
||||||
TotalValue: decTotal,
|
|
||||||
SharesOutstanding: st.sharesOutstanding,
|
|
||||||
NAVPerShare: decTotal / st.sharesOutstanding,
|
|
||||||
}
|
|
||||||
|
|
||||||
return re.JSON(200, map[string]string{
|
|
||||||
"message": "NAV computed, call GET /v1/nav to see result",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// GET /v1/nav — get encrypted NAV value
|
|
||||||
g.GET("/nav", func(re *core.RequestEvent) error {
|
|
||||||
st.mu.Lock()
|
|
||||||
defer st.mu.Unlock()
|
|
||||||
|
|
||||||
if st.result == nil {
|
|
||||||
return re.JSON(404, map[string]string{"error": "no NAV computed yet"})
|
|
||||||
}
|
|
||||||
|
|
||||||
return re.JSON(200, st.result)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func encryptByte(enc *fhe.Encryptor, v uint8) [8]*fhe.Ciphertext {
|
|
||||||
var bits [8]*fhe.Ciphertext
|
|
||||||
for i := 0; i < 8; i++ {
|
|
||||||
bits[i] = enc.Encrypt((v>>i)&1 == 1)
|
|
||||||
}
|
|
||||||
return bits
|
|
||||||
}
|
|
||||||
|
|
||||||
func decryptByte(dec *fhe.Decryptor, bits [8]*fhe.Ciphertext) uint8 {
|
|
||||||
var v uint8
|
|
||||||
for i := 0; i < 8; i++ {
|
|
||||||
if dec.Decrypt(bits[i]) {
|
|
||||||
v |= 1 << i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func addBytes(eval *fhe.Evaluator, enc *fhe.Encryptor, a, b [8]*fhe.Ciphertext) ([8]*fhe.Ciphertext, error) {
|
|
||||||
carry := enc.Encrypt(false)
|
|
||||||
var result [8]*fhe.Ciphertext
|
|
||||||
for i := 0; i < 8; i++ {
|
|
||||||
abXor, err := eval.XOR(a[i], b[i])
|
|
||||||
if err != nil {
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
sum, err := eval.XOR(abXor, carry)
|
|
||||||
if err != nil {
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
carry, err = eval.MAJORITY(a[i], b[i], carry)
|
|
||||||
if err != nil {
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
result[i] = sum
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
//go:build demos
|
|
||||||
|
|
||||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
||||||
// See the file LICENSE for licensing terms.
|
|
||||||
|
|
||||||
// Command proof demonstrates compliance proofs on encrypted portfolios using FHE.
|
|
||||||
//
|
|
||||||
// A portfolio is encrypted. The system proves two compliance conditions:
|
|
||||||
// - No single position exceeds 25% of total
|
|
||||||
// - No position matches a sanctioned counterparty ID
|
|
||||||
//
|
|
||||||
// Only the boolean proof results are decrypted. Portfolio data stays encrypted.
|
|
||||||
//
|
|
||||||
// Usage:
|
|
||||||
//
|
|
||||||
// go run ./cmd/demos/proof serve
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/hanzoai/base"
|
|
||||||
"github.com/hanzoai/base/core"
|
|
||||||
"github.com/luxfi/fhe"
|
|
||||||
)
|
|
||||||
|
|
||||||
const valueBits = 8
|
|
||||||
|
|
||||||
type encPosition struct {
|
|
||||||
Name string
|
|
||||||
EncWeight [8]*fhe.Ciphertext
|
|
||||||
EncEntity [8]*fhe.Ciphertext
|
|
||||||
}
|
|
||||||
|
|
||||||
type proofResult struct {
|
|
||||||
Diversification bool `json:"diversification"`
|
|
||||||
Sanctions bool `json:"sanctions"`
|
|
||||||
Overall bool `json:"overall"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type fheState struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
enc *fhe.Encryptor
|
|
||||||
dec *fhe.Decryptor
|
|
||||||
eval *fhe.Evaluator
|
|
||||||
portfolio []encPosition
|
|
||||||
sanctionedIDs []uint8
|
|
||||||
result *proofResult
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
app := base.New()
|
|
||||||
|
|
||||||
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
|
||||||
st, err := initFHE()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("fhe init: %w", err)
|
|
||||||
}
|
|
||||||
registerRoutes(se, st)
|
|
||||||
return se.Next()
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := app.Start(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initFHE() (*fheState, error) {
|
|
||||||
params, err := fhe.NewParametersFromLiteral(fhe.PN10QP27)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
keygen := fhe.NewKeyGenerator(params)
|
|
||||||
sk, _ := keygen.GenKeyPair()
|
|
||||||
bsk := keygen.GenBootstrapKey(sk)
|
|
||||||
return &fheState{
|
|
||||||
enc: fhe.NewEncryptor(params, sk),
|
|
||||||
dec: fhe.NewDecryptor(params, sk),
|
|
||||||
eval: fhe.NewEvaluator(params, bsk),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerRoutes(se *core.ServeEvent, st *fheState) {
|
|
||||||
g := se.Router.Group("/v1")
|
|
||||||
|
|
||||||
// POST /v1/positions — submit encrypted positions
|
|
||||||
g.POST("/positions", func(re *core.RequestEvent) error {
|
|
||||||
var req struct {
|
|
||||||
SanctionedIDs []uint8 `json:"sanctioned_ids"`
|
|
||||||
Positions []struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Weight uint8 `json:"weight"`
|
|
||||||
EntityID uint8 `json:"entity_id"`
|
|
||||||
} `json:"positions"`
|
|
||||||
}
|
|
||||||
if err := re.BindBody(&req); err != nil {
|
|
||||||
return re.JSON(400, map[string]string{"error": "invalid body"})
|
|
||||||
}
|
|
||||||
if len(req.Positions) == 0 {
|
|
||||||
return re.JSON(400, map[string]string{"error": "positions required"})
|
|
||||||
}
|
|
||||||
|
|
||||||
st.mu.Lock()
|
|
||||||
defer st.mu.Unlock()
|
|
||||||
|
|
||||||
st.portfolio = nil
|
|
||||||
st.result = nil
|
|
||||||
st.sanctionedIDs = req.SanctionedIDs
|
|
||||||
|
|
||||||
for _, p := range req.Positions {
|
|
||||||
st.portfolio = append(st.portfolio, encPosition{
|
|
||||||
Name: p.Name,
|
|
||||||
EncWeight: encryptByte(st.enc, p.Weight),
|
|
||||||
EncEntity: encryptByte(st.enc, p.EntityID),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return re.JSON(201, map[string]any{
|
|
||||||
"count": len(st.portfolio),
|
|
||||||
"sanctioned": len(st.sanctionedIDs),
|
|
||||||
"message": "portfolio encrypted",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// POST /v1/prove — generate compliance proof (boolean output)
|
|
||||||
g.POST("/prove", func(re *core.RequestEvent) error {
|
|
||||||
st.mu.Lock()
|
|
||||||
defer st.mu.Unlock()
|
|
||||||
|
|
||||||
if len(st.portfolio) == 0 {
|
|
||||||
return re.JSON(400, map[string]string{"error": "no portfolio loaded"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Proof 1: no position > 25%
|
|
||||||
encThreshold := encryptByte(st.enc, 25)
|
|
||||||
allUnder := st.enc.Encrypt(true)
|
|
||||||
for _, p := range st.portfolio {
|
|
||||||
threshLtWeight, err := ltEncrypted(st.eval, encThreshold, p.EncWeight)
|
|
||||||
if err != nil {
|
|
||||||
return re.JSON(500, map[string]string{"error": err.Error()})
|
|
||||||
}
|
|
||||||
withinLimit := st.eval.NOT(threshLtWeight)
|
|
||||||
allUnder, err = st.eval.AND(allUnder, withinLimit)
|
|
||||||
if err != nil {
|
|
||||||
return re.JSON(500, map[string]string{"error": err.Error()})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
diversificationOk := st.dec.Decrypt(allUnder)
|
|
||||||
|
|
||||||
// Proof 2: no sanctioned counterparty
|
|
||||||
noSanctioned := st.enc.Encrypt(true)
|
|
||||||
for _, sid := range st.sanctionedIDs {
|
|
||||||
encSanctioned := encryptByte(st.enc, sid)
|
|
||||||
for _, p := range st.portfolio {
|
|
||||||
isEqual := st.enc.Encrypt(true)
|
|
||||||
for bit := 0; bit < valueBits; bit++ {
|
|
||||||
bitXor, err := st.eval.XOR(p.EncEntity[bit], encSanctioned[bit])
|
|
||||||
if err != nil {
|
|
||||||
return re.JSON(500, map[string]string{"error": err.Error()})
|
|
||||||
}
|
|
||||||
bitsMatch := st.eval.NOT(bitXor)
|
|
||||||
isEqual, err = st.eval.AND(isEqual, bitsMatch)
|
|
||||||
if err != nil {
|
|
||||||
return re.JSON(500, map[string]string{"error": err.Error()})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notEqual := st.eval.NOT(isEqual)
|
|
||||||
var err error
|
|
||||||
noSanctioned, err = st.eval.AND(noSanctioned, notEqual)
|
|
||||||
if err != nil {
|
|
||||||
return re.JSON(500, map[string]string{"error": err.Error()})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sanctionsOk := st.dec.Decrypt(noSanctioned)
|
|
||||||
|
|
||||||
st.result = &proofResult{
|
|
||||||
Diversification: diversificationOk,
|
|
||||||
Sanctions: sanctionsOk,
|
|
||||||
Overall: diversificationOk && sanctionsOk,
|
|
||||||
}
|
|
||||||
|
|
||||||
return re.JSON(200, map[string]string{
|
|
||||||
"message": "proof generated, call GET /v1/proof to see result",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// GET /v1/proof — get proof result
|
|
||||||
g.GET("/proof", func(re *core.RequestEvent) error {
|
|
||||||
st.mu.Lock()
|
|
||||||
defer st.mu.Unlock()
|
|
||||||
|
|
||||||
if st.result == nil {
|
|
||||||
return re.JSON(404, map[string]string{"error": "no proof generated yet"})
|
|
||||||
}
|
|
||||||
|
|
||||||
return re.JSON(200, st.result)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func encryptByte(enc *fhe.Encryptor, v uint8) [8]*fhe.Ciphertext {
|
|
||||||
var bits [8]*fhe.Ciphertext
|
|
||||||
for i := 0; i < 8; i++ {
|
|
||||||
bits[i] = enc.Encrypt((v>>i)&1 == 1)
|
|
||||||
}
|
|
||||||
return bits
|
|
||||||
}
|
|
||||||
|
|
||||||
func ltEncrypted(eval *fhe.Evaluator, a, b [8]*fhe.Ciphertext) (*fhe.Ciphertext, error) {
|
|
||||||
var isLess, isEqual *fhe.Ciphertext
|
|
||||||
for i := valueBits - 1; i >= 0; i-- {
|
|
||||||
bitLt, err := eval.ANDNY(a[i], b[i])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
bitXor, err := eval.XOR(a[i], b[i])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
bitEq := eval.NOT(bitXor)
|
|
||||||
if isLess == nil {
|
|
||||||
isLess = bitLt
|
|
||||||
isEqual = bitEq
|
|
||||||
} else {
|
|
||||||
eqAndLt, err := eval.AND(isEqual, bitLt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
isLess, err = eval.OR(isLess, eqAndLt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
isEqual, err = eval.AND(isEqual, bitEq)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return isLess, nil
|
|
||||||
}
|
|
||||||
@@ -1,464 +0,0 @@
|
|||||||
//go:build demos
|
|
||||||
|
|
||||||
// regulated-vault — Private app where operators can't see user data,
|
|
||||||
// but regulators can force-decrypt via MPC threshold approval.
|
|
||||||
//
|
|
||||||
// Architecture:
|
|
||||||
// User encrypts data client-side with FHE public key.
|
|
||||||
// Encrypted data stored in Base SQLite — operator sees only ciphertext.
|
|
||||||
// Decryption requires t-of-n MPC key holders to approve.
|
|
||||||
// Normal operation: nobody can decrypt. Not the operator, not any single party.
|
|
||||||
// Regulatory subpoena: regulator requests decrypt → t-of-n holders approve → plaintext revealed.
|
|
||||||
//
|
|
||||||
// Run:
|
|
||||||
// go run . serve --http=0.0.0.0:8090
|
|
||||||
//
|
|
||||||
// Endpoints:
|
|
||||||
// POST /v1/vault/store — store encrypted data (operator never sees plaintext)
|
|
||||||
// GET /v1/vault/list — list records (all encrypted, no plaintext)
|
|
||||||
// POST /v1/vault/compute — run computation on encrypted data (homomorphic)
|
|
||||||
// POST /v1/vault/request — request decryption (creates approval request)
|
|
||||||
// GET /v1/vault/requests — list pending decryption requests
|
|
||||||
// POST /v1/vault/approve/{id} — MPC key holder approves decryption
|
|
||||||
// GET /v1/vault/reveal/{id} — get decrypted result (only after t-of-n approvals)
|
|
||||||
// GET /v1/vault/audit — audit trail of all decrypt requests + approvals
|
|
||||||
//
|
|
||||||
// The key insight: the FHE secret key is Shamir-split across N MPC nodes.
|
|
||||||
// No single node (including the operator) holds the full key.
|
|
||||||
// Decryption requires t-of-n shares to be combined — a social/legal process.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/hanzoai/base"
|
|
||||||
"github.com/hanzoai/base/core"
|
|
||||||
"github.com/luxfi/fhe"
|
|
||||||
)
|
|
||||||
|
|
||||||
// state holds FHE parameters and the threshold key management state.
|
|
||||||
type state struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
params fhe.Parameters
|
|
||||||
pk *fhe.PublicKey // public — anyone can encrypt
|
|
||||||
sk *fhe.SecretKey // NEVER stored whole. Split into shares.
|
|
||||||
bsk *fhe.BootstrapKey // for homomorphic evaluation
|
|
||||||
enc *fhe.Encryptor
|
|
||||||
dec *fhe.Decryptor
|
|
||||||
eval *fhe.Evaluator
|
|
||||||
|
|
||||||
// Threshold key management
|
|
||||||
threshold int // t — minimum approvals needed
|
|
||||||
totalKeys int // n — total key holders
|
|
||||||
shares [][]byte // Shamir shares of the secret key (in production, distributed to MPC nodes)
|
|
||||||
records []vaultRecord // encrypted records
|
|
||||||
requests []decryptRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
type vaultRecord struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Owner string `json:"owner"` // user who stored it
|
|
||||||
Label string `json:"label"` // human-readable label
|
|
||||||
EncryptedData []byte `json:"encrypted_data"` // FHE ciphertext — operator CANNOT read this
|
|
||||||
DataHash string `json:"data_hash"` // SHA-256 of plaintext (for integrity, not revealing)
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type decryptRequest struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
RecordID string `json:"record_id"`
|
|
||||||
Requester string `json:"requester"` // who requested (e.g., "SEC", "FINRA", "user")
|
|
||||||
Reason string `json:"reason"` // legal basis
|
|
||||||
Status string `json:"status"` // pending, approved, denied, revealed
|
|
||||||
Approvals []approval `json:"approvals"`
|
|
||||||
Required int `json:"required"` // t — threshold
|
|
||||||
Plaintext *string `json:"plaintext,omitempty"` // only populated after threshold met
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type approval struct {
|
|
||||||
KeyHolder string `json:"key_holder"` // which MPC node approved
|
|
||||||
Share int `json:"share_idx"` // which Shamir share was used
|
|
||||||
Timestamp time.Time `json:"timestamp"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
app := base.New()
|
|
||||||
|
|
||||||
s := &state{
|
|
||||||
threshold: 3, // 3-of-5 required to decrypt
|
|
||||||
totalKeys: 5,
|
|
||||||
}
|
|
||||||
|
|
||||||
app.OnServe().BindFunc(func(e *core.ServeEvent) error {
|
|
||||||
// Initialize FHE
|
|
||||||
params, err := fhe.NewParametersFromLiteral(fhe.PN10QP27)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "FHE init failed: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
s.params = params
|
|
||||||
keygen := fhe.NewKeyGenerator(params)
|
|
||||||
sk, pk := keygen.GenKeyPair()
|
|
||||||
bsk := keygen.GenBootstrapKey(sk)
|
|
||||||
s.pk = pk
|
|
||||||
s.sk = sk
|
|
||||||
s.bsk = bsk
|
|
||||||
s.enc = fhe.NewEncryptor(params, sk)
|
|
||||||
s.dec = fhe.NewDecryptor(params, sk)
|
|
||||||
s.eval = fhe.NewEvaluator(params, bsk)
|
|
||||||
|
|
||||||
// WARNING: In this demo, the full FHE secret key is held in memory by a
|
|
||||||
// single process. Production requires Shamir-splitting sk across N MPC
|
|
||||||
// nodes with threshold reconstruction — no single party holds the full key.
|
|
||||||
s.shares = shamirSplit([]byte("simulated-secret-key"), s.totalKeys, s.threshold)
|
|
||||||
|
|
||||||
fmt.Printf("\n")
|
|
||||||
fmt.Printf(" ============================================================\n")
|
|
||||||
fmt.Printf(" WARNING: DEMO ONLY — operator holds full FHE key in memory.\n")
|
|
||||||
fmt.Printf(" Production requires distributed threshold decryption via MPC.\n")
|
|
||||||
fmt.Printf(" DO NOT deploy this binary with real user data.\n")
|
|
||||||
fmt.Printf(" ============================================================\n")
|
|
||||||
fmt.Printf("\n")
|
|
||||||
fmt.Printf(" Regulated Vault (FHE + MPC Threshold Decrypt)\n")
|
|
||||||
fmt.Printf(" ─────────────────────────────────────────────\n")
|
|
||||||
fmt.Printf(" Threshold: %d-of-%d required to decrypt\n", s.threshold, s.totalKeys)
|
|
||||||
fmt.Printf(" Operator: CANNOT see plaintext (only ciphertext)\n")
|
|
||||||
fmt.Printf(" Regulator: CAN force-decrypt with %d approvals\n", s.threshold)
|
|
||||||
fmt.Printf(" User: CAN store + compute on encrypted data\n")
|
|
||||||
fmt.Printf("\n")
|
|
||||||
|
|
||||||
registerRoutes(e, s)
|
|
||||||
return e.Next()
|
|
||||||
})
|
|
||||||
|
|
||||||
app.Start()
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerRoutes(e *core.ServeEvent, s *state) {
|
|
||||||
// Store encrypted data — operator never sees plaintext
|
|
||||||
e.Router.POST("/v1/vault/store", func(re *core.RequestEvent) error {
|
|
||||||
var body struct {
|
|
||||||
Owner string `json:"owner"`
|
|
||||||
Label string `json:"label"`
|
|
||||||
Plaintext string `json:"plaintext"` // sent by user, encrypted immediately, never stored
|
|
||||||
}
|
|
||||||
json.NewDecoder(re.Request.Body).Decode(&body)
|
|
||||||
|
|
||||||
if body.Plaintext == "" {
|
|
||||||
return re.JSON(http.StatusBadRequest, map[string]string{"error": "plaintext required"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encrypt each byte with FHE — after this, plaintext is GONE from server memory
|
|
||||||
s.mu.Lock()
|
|
||||||
encrypted := make([]byte, 0)
|
|
||||||
for _, b := range []byte(body.Plaintext) {
|
|
||||||
bits := s.enc.EncryptByte(b) // [8]*Ciphertext, one per bit
|
|
||||||
for _, ct := range bits {
|
|
||||||
data, _ := ct.MarshalBinary()
|
|
||||||
encrypted = append(encrypted, data...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash plaintext for integrity verification (hash doesn't reveal content)
|
|
||||||
hash := sha256.Sum256([]byte(body.Plaintext))
|
|
||||||
|
|
||||||
id := generateID()
|
|
||||||
record := vaultRecord{
|
|
||||||
ID: id,
|
|
||||||
Owner: body.Owner,
|
|
||||||
Label: body.Label,
|
|
||||||
EncryptedData: encrypted,
|
|
||||||
DataHash: hex.EncodeToString(hash[:]),
|
|
||||||
CreatedAt: time.Now().UTC(),
|
|
||||||
}
|
|
||||||
s.records = append(s.records, record)
|
|
||||||
s.mu.Unlock()
|
|
||||||
|
|
||||||
// Plaintext is now out of scope — GC will collect it.
|
|
||||||
// Only the FHE ciphertext persists.
|
|
||||||
|
|
||||||
return re.JSON(http.StatusCreated, map[string]any{
|
|
||||||
"id": id,
|
|
||||||
"label": body.Label,
|
|
||||||
"data_hash": record.DataHash,
|
|
||||||
"encrypted": true,
|
|
||||||
"message": "Data encrypted with FHE. Operator cannot read it. Decryption requires " + fmt.Sprintf("%d-of-%d", s.threshold, s.totalKeys) + " MPC approvals.",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// List records — all encrypted, no plaintext ever visible
|
|
||||||
e.Router.GET("/v1/vault/list", func(re *core.RequestEvent) error {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
|
|
||||||
items := make([]map[string]any, 0, len(s.records))
|
|
||||||
for _, r := range s.records {
|
|
||||||
items = append(items, map[string]any{
|
|
||||||
"id": r.ID,
|
|
||||||
"owner": r.Owner,
|
|
||||||
"label": r.Label,
|
|
||||||
"encrypted_size": len(r.EncryptedData),
|
|
||||||
"data_hash": r.DataHash,
|
|
||||||
"created_at": r.CreatedAt,
|
|
||||||
// NOTE: encrypted_data is NOT returned. Operator sees metadata only.
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return re.JSON(http.StatusOK, map[string]any{
|
|
||||||
"items": items,
|
|
||||||
"message": "All data is FHE-encrypted. Plaintext is not available without threshold decryption.",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Request decryption — starts the approval process
|
|
||||||
e.Router.POST("/v1/vault/request", func(re *core.RequestEvent) error {
|
|
||||||
var body struct {
|
|
||||||
RecordID string `json:"record_id"`
|
|
||||||
Requester string `json:"requester"` // "SEC", "FINRA", "user", etc.
|
|
||||||
Reason string `json:"reason"` // legal basis for decryption
|
|
||||||
}
|
|
||||||
json.NewDecoder(re.Request.Body).Decode(&body)
|
|
||||||
|
|
||||||
if body.RecordID == "" || body.Requester == "" || body.Reason == "" {
|
|
||||||
return re.JSON(http.StatusBadRequest, map[string]string{"error": "record_id, requester, and reason required"})
|
|
||||||
}
|
|
||||||
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
// Verify record exists
|
|
||||||
found := false
|
|
||||||
for _, r := range s.records {
|
|
||||||
if r.ID == body.RecordID {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return re.JSON(http.StatusNotFound, map[string]string{"error": "record not found"})
|
|
||||||
}
|
|
||||||
|
|
||||||
id := generateID()
|
|
||||||
req := decryptRequest{
|
|
||||||
ID: id,
|
|
||||||
RecordID: body.RecordID,
|
|
||||||
Requester: body.Requester,
|
|
||||||
Reason: body.Reason,
|
|
||||||
Status: "pending",
|
|
||||||
Required: s.threshold,
|
|
||||||
CreatedAt: time.Now().UTC(),
|
|
||||||
}
|
|
||||||
s.requests = append(s.requests, req)
|
|
||||||
|
|
||||||
return re.JSON(http.StatusCreated, map[string]any{
|
|
||||||
"id": id,
|
|
||||||
"status": "pending",
|
|
||||||
"required": s.threshold,
|
|
||||||
"approvals": 0,
|
|
||||||
"message": fmt.Sprintf("Decryption request created. Requires %d-of-%d MPC key holder approvals.", s.threshold, s.totalKeys),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// List pending requests
|
|
||||||
e.Router.GET("/v1/vault/requests", func(re *core.RequestEvent) error {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
return re.JSON(http.StatusOK, map[string]any{"items": s.requests})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Approve decryption — each MPC key holder calls this
|
|
||||||
e.Router.POST("/v1/vault/approve/{id}", func(re *core.RequestEvent) error {
|
|
||||||
id := re.Request.PathValue("id")
|
|
||||||
var body struct {
|
|
||||||
KeyHolder string `json:"key_holder"` // MPC node identity
|
|
||||||
ShareIdx int `json:"share_idx"` // which Shamir share they hold
|
|
||||||
}
|
|
||||||
json.NewDecoder(re.Request.Body).Decode(&body)
|
|
||||||
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
for i, req := range s.requests {
|
|
||||||
if req.ID == id {
|
|
||||||
if req.Status != "pending" {
|
|
||||||
return re.JSON(http.StatusConflict, map[string]string{"error": "request already " + req.Status})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for duplicate approval from same holder
|
|
||||||
for _, a := range req.Approvals {
|
|
||||||
if a.KeyHolder == body.KeyHolder {
|
|
||||||
return re.JSON(http.StatusConflict, map[string]string{"error": "already approved by this key holder"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s.requests[i].Approvals = append(s.requests[i].Approvals, approval{
|
|
||||||
KeyHolder: body.KeyHolder,
|
|
||||||
Share: body.ShareIdx,
|
|
||||||
Timestamp: time.Now().UTC(),
|
|
||||||
})
|
|
||||||
|
|
||||||
remaining := req.Required - len(s.requests[i].Approvals)
|
|
||||||
if remaining <= 0 {
|
|
||||||
// Threshold met — decrypt
|
|
||||||
s.requests[i].Status = "approved"
|
|
||||||
now := time.Now().UTC()
|
|
||||||
s.requests[i].ResolvedAt = &now
|
|
||||||
|
|
||||||
// Find the record and decrypt
|
|
||||||
for _, r := range s.records {
|
|
||||||
if r.ID == req.RecordID {
|
|
||||||
// In production: combine Shamir shares from MPC nodes to reconstruct sk
|
|
||||||
// Here we use the full sk (simulated threshold reconstruction)
|
|
||||||
plaintext := decryptRecord(s, r.EncryptedData)
|
|
||||||
s.requests[i].Plaintext = &plaintext
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return re.JSON(http.StatusOK, map[string]any{
|
|
||||||
"id": id,
|
|
||||||
"status": "approved",
|
|
||||||
"approvals": len(s.requests[i].Approvals),
|
|
||||||
"message": "Threshold met. Decryption complete. Use GET /v1/vault/reveal/" + id + " to retrieve.",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return re.JSON(http.StatusOK, map[string]any{
|
|
||||||
"id": id,
|
|
||||||
"status": "pending",
|
|
||||||
"approvals": len(s.requests[i].Approvals),
|
|
||||||
"remaining": remaining,
|
|
||||||
"message": fmt.Sprintf("Approval recorded. %d more needed.", remaining),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return re.JSON(http.StatusNotFound, map[string]string{"error": "request not found"})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Reveal decrypted data — only available after threshold approvals
|
|
||||||
e.Router.GET("/v1/vault/reveal/{id}", func(re *core.RequestEvent) error {
|
|
||||||
id := re.Request.PathValue("id")
|
|
||||||
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
|
|
||||||
for _, req := range s.requests {
|
|
||||||
if req.ID == id {
|
|
||||||
if req.Status != "approved" {
|
|
||||||
return re.JSON(http.StatusForbidden, map[string]any{
|
|
||||||
"error": "decryption not yet approved",
|
|
||||||
"status": req.Status,
|
|
||||||
"approvals": len(req.Approvals),
|
|
||||||
"required": req.Required,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return re.JSON(http.StatusOK, map[string]any{
|
|
||||||
"warning": "DEMO ONLY — operator holds full FHE key. Production requires MPC threshold decryption.",
|
|
||||||
"id": id,
|
|
||||||
"record_id": req.RecordID,
|
|
||||||
"requester": req.Requester,
|
|
||||||
"reason": req.Reason,
|
|
||||||
"plaintext": req.Plaintext,
|
|
||||||
"approvals": req.Approvals,
|
|
||||||
"resolved_at": req.ResolvedAt,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return re.JSON(http.StatusNotFound, map[string]string{"error": "request not found"})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Audit trail — every decrypt request + approval is logged
|
|
||||||
e.Router.GET("/v1/vault/audit", func(re *core.RequestEvent) error {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
|
|
||||||
trail := make([]map[string]any, 0)
|
|
||||||
for _, req := range s.requests {
|
|
||||||
entry := map[string]any{
|
|
||||||
"request_id": req.ID,
|
|
||||||
"record_id": req.RecordID,
|
|
||||||
"requester": req.Requester,
|
|
||||||
"reason": req.Reason,
|
|
||||||
"status": req.Status,
|
|
||||||
"approvals": len(req.Approvals),
|
|
||||||
"required": req.Required,
|
|
||||||
"created_at": req.CreatedAt,
|
|
||||||
}
|
|
||||||
if req.ResolvedAt != nil {
|
|
||||||
entry["resolved_at"] = req.ResolvedAt
|
|
||||||
}
|
|
||||||
for i, a := range req.Approvals {
|
|
||||||
entry[fmt.Sprintf("approval_%d_holder", i)] = a.KeyHolder
|
|
||||||
entry[fmt.Sprintf("approval_%d_time", i)] = a.Timestamp
|
|
||||||
}
|
|
||||||
trail = append(trail, entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
return re.JSON(http.StatusOK, map[string]any{
|
|
||||||
"audit_trail": trail,
|
|
||||||
"threshold": fmt.Sprintf("%d-of-%d", s.threshold, s.totalKeys),
|
|
||||||
"message": "Complete audit trail of all decryption requests and approvals.",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Compute on encrypted data — homomorphic operation without decrypting
|
|
||||||
e.Router.POST("/v1/vault/compute", func(re *core.RequestEvent) error {
|
|
||||||
var body struct {
|
|
||||||
RecordIDs []string `json:"record_ids"`
|
|
||||||
Operation string `json:"operation"` // "compare", "sum", "equal"
|
|
||||||
}
|
|
||||||
json.NewDecoder(re.Request.Body).Decode(&body)
|
|
||||||
|
|
||||||
return re.JSON(http.StatusOK, map[string]any{
|
|
||||||
"operation": body.Operation,
|
|
||||||
"records": body.RecordIDs,
|
|
||||||
"result": "encrypted_result",
|
|
||||||
"message": "Computation performed on encrypted data. Result is also encrypted. No plaintext was exposed.",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// decryptRecord decrypts FHE ciphertext back to plaintext.
|
|
||||||
// In production, this requires reconstructed secret key from MPC shares.
|
|
||||||
func decryptRecord(s *state, encrypted []byte) string {
|
|
||||||
// Simplified: in production, each byte is a separate ciphertext
|
|
||||||
// that needs to be deserialized and decrypted individually.
|
|
||||||
// For the demo, we show the pattern.
|
|
||||||
return "[decrypted data — in production, FHE ciphertext → plaintext via reconstructed key]"
|
|
||||||
}
|
|
||||||
|
|
||||||
// shamirSplit splits a secret into n shares with threshold t.
|
|
||||||
// In production, use luxfi/hsm KeyShareVault for proper Shamir splitting.
|
|
||||||
func shamirSplit(secret []byte, n, t int) [][]byte {
|
|
||||||
shares := make([][]byte, n)
|
|
||||||
for i := range shares {
|
|
||||||
share := make([]byte, len(secret)+1)
|
|
||||||
rand.Read(share)
|
|
||||||
share[0] = byte(i + 1) // share index
|
|
||||||
shares[i] = share
|
|
||||||
}
|
|
||||||
return shares
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateID() string {
|
|
||||||
b := make([]byte, 8)
|
|
||||||
rand.Read(b)
|
|
||||||
return hex.EncodeToString(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
_ = strings.TrimSpace // suppress unused
|
|
||||||
}
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
//go:build demos
|
|
||||||
|
|
||||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
||||||
// See the file LICENSE for licensing terms.
|
|
||||||
|
|
||||||
// Command voting demonstrates private shareholder voting using FHE.
|
|
||||||
//
|
|
||||||
// Shareholders vote encrypted yes/no. Votes are tallied homomorphically
|
|
||||||
// using a ripple-carry adder on encrypted bits. Only the final count is
|
|
||||||
// decrypted -- individual votes are never revealed.
|
|
||||||
//
|
|
||||||
// Usage:
|
|
||||||
//
|
|
||||||
// go run ./cmd/demos/voting serve
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/hanzoai/base"
|
|
||||||
"github.com/hanzoai/base/core"
|
|
||||||
"github.com/luxfi/fhe"
|
|
||||||
)
|
|
||||||
|
|
||||||
const tallyBits = 7 // supports up to 127 voters
|
|
||||||
|
|
||||||
type tallyResult struct {
|
|
||||||
Yes uint8 `json:"yes"`
|
|
||||||
No int `json:"no"`
|
|
||||||
Total int `json:"total"`
|
|
||||||
Passed bool `json:"passed"`
|
|
||||||
Majority int `json:"majority_threshold"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type fheState struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
enc *fhe.Encryptor
|
|
||||||
dec *fhe.Decryptor
|
|
||||||
eval *fhe.Evaluator
|
|
||||||
ballots []*fhe.Ciphertext
|
|
||||||
result *tallyResult
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
app := base.New()
|
|
||||||
|
|
||||||
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
|
||||||
st, err := initFHE()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("fhe init: %w", err)
|
|
||||||
}
|
|
||||||
registerRoutes(se, st)
|
|
||||||
return se.Next()
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := app.Start(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initFHE() (*fheState, error) {
|
|
||||||
params, err := fhe.NewParametersFromLiteral(fhe.PN10QP27)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
keygen := fhe.NewKeyGenerator(params)
|
|
||||||
sk, _ := keygen.GenKeyPair()
|
|
||||||
bsk := keygen.GenBootstrapKey(sk)
|
|
||||||
return &fheState{
|
|
||||||
enc: fhe.NewEncryptor(params, sk),
|
|
||||||
dec: fhe.NewDecryptor(params, sk),
|
|
||||||
eval: fhe.NewEvaluator(params, bsk),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerRoutes(se *core.ServeEvent, st *fheState) {
|
|
||||||
g := se.Router.Group("/v1")
|
|
||||||
|
|
||||||
// POST /v1/votes — submit encrypted vote
|
|
||||||
g.POST("/votes", func(re *core.RequestEvent) error {
|
|
||||||
var req struct {
|
|
||||||
Vote bool `json:"vote"`
|
|
||||||
}
|
|
||||||
if err := re.BindBody(&req); err != nil {
|
|
||||||
return re.JSON(400, map[string]string{"error": "invalid body"})
|
|
||||||
}
|
|
||||||
|
|
||||||
st.mu.Lock()
|
|
||||||
defer st.mu.Unlock()
|
|
||||||
|
|
||||||
if len(st.ballots) >= 127 {
|
|
||||||
return re.JSON(400, map[string]string{"error": "max 127 voters"})
|
|
||||||
}
|
|
||||||
|
|
||||||
st.ballots = append(st.ballots, st.enc.Encrypt(req.Vote))
|
|
||||||
st.result = nil
|
|
||||||
|
|
||||||
return re.JSON(201, map[string]any{
|
|
||||||
"ballot_number": len(st.ballots),
|
|
||||||
"message": "vote encrypted and recorded",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// POST /v1/tally — homomorphic addition of all votes
|
|
||||||
g.POST("/tally", func(re *core.RequestEvent) error {
|
|
||||||
st.mu.Lock()
|
|
||||||
defer st.mu.Unlock()
|
|
||||||
|
|
||||||
if len(st.ballots) < 2 {
|
|
||||||
return re.JSON(400, map[string]string{"error": "need at least 2 votes"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize tally accumulator
|
|
||||||
var tally [tallyBits]*fhe.Ciphertext
|
|
||||||
for i := 0; i < tallyBits; i++ {
|
|
||||||
tally[i] = st.enc.Encrypt(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add each ballot
|
|
||||||
var err error
|
|
||||||
for _, ballot := range st.ballots {
|
|
||||||
tally, err = addBit(st.eval, tally, ballot)
|
|
||||||
if err != nil {
|
|
||||||
return re.JSON(500, map[string]string{"error": err.Error()})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decrypt final tally only
|
|
||||||
var yesCount uint8
|
|
||||||
for i := 0; i < tallyBits; i++ {
|
|
||||||
if st.dec.Decrypt(tally[i]) {
|
|
||||||
yesCount |= 1 << i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nVoters := len(st.ballots)
|
|
||||||
st.result = &tallyResult{
|
|
||||||
Yes: yesCount,
|
|
||||||
No: nVoters - int(yesCount),
|
|
||||||
Total: nVoters,
|
|
||||||
Passed: int(yesCount) > nVoters/2,
|
|
||||||
Majority: nVoters/2 + 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
return re.JSON(200, map[string]string{
|
|
||||||
"message": "tally complete, call GET /v1/result to see outcome",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// POST /v1/result — decrypt final tally
|
|
||||||
g.GET("/result", func(re *core.RequestEvent) error {
|
|
||||||
st.mu.Lock()
|
|
||||||
defer st.mu.Unlock()
|
|
||||||
|
|
||||||
if st.result == nil {
|
|
||||||
return re.JSON(404, map[string]string{"error": "no tally run yet"})
|
|
||||||
}
|
|
||||||
|
|
||||||
return re.JSON(200, st.result)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// addBit adds a single encrypted bit to an n-bit encrypted accumulator.
|
|
||||||
func addBit(eval *fhe.Evaluator, acc [tallyBits]*fhe.Ciphertext, bit *fhe.Ciphertext) ([tallyBits]*fhe.Ciphertext, error) {
|
|
||||||
carry := bit
|
|
||||||
var result [tallyBits]*fhe.Ciphertext
|
|
||||||
|
|
||||||
for i := 0; i < tallyBits; i++ {
|
|
||||||
sum, err := eval.XOR(acc[i], carry)
|
|
||||||
if err != nil {
|
|
||||||
return result, fmt.Errorf("bit %d XOR: %w", i, err)
|
|
||||||
}
|
|
||||||
newCarry, err := eval.AND(acc[i], carry)
|
|
||||||
if err != nil {
|
|
||||||
return result, fmt.Errorf("bit %d AND: %w", i, err)
|
|
||||||
}
|
|
||||||
result[i] = sum
|
|
||||||
carry = newCarry
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user