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