Files
mpc/pkg/backup/shamir.go
T
zeekay a42050c830 feat: tx lifecycle tracking, Shamir backup sharding, settlement engine
Transaction Lifecycle ("did the tx land?"):
- TxTracker polls eth_getTransactionReceipt for block inclusion
- Receipt fields: blockNumber, blockHash, receiptStatus, gasUsed, revertReason
- Confirmation counter with configurable target (default 12)
- StatusHistory records every state transition with timestamp and actor
- Stuck/dropped tx detection with webhook alerts

Backup Key Sharding (Secure Recovery Flow):
- GF(2^8) Shamir Secret Sharing (ShamirSplit/ShamirCombine)
- WalletBackup: AES-256-GCM encrypt key share, Shamir-split the key
- Labeled shard destinations: icloud, hsm, offline, custody, device
- Default 2-of-2 (iCloud + Platform HSM), institutional 3-of-5

Settlement & Intent Engine:
- Intent lifecycle: pending_sign → signed → co_signed → recorded → matched
- HSM co-signing as second signer in multisig flow
- Settlement with HSM multisig attestation collection
- TransferAgency hash registration and verification
- WalletBackup, Intent, Settlement DB models

API Routes:
- POST/GET /intents, /intents/:id/sign, /intents/:id/co-sign
- GET /settlements, /settlements/:id
- POST/GET /wallets/:id/backup
- New webhooks: tx.included, tx.finalized, tx.reverted, intent.*

40 tests passing across backup, settlement, txtracker, api packages.
2025-12-27 18:09:37 -08:00

201 lines
5.3 KiB
Go

package backup
// Shamir Secret Sharing over GF(2^8) for wallet backup key splitting.
//
// Default: 2-of-2 (iCloud Shard A + Platform HSM Shard B).
// Supports arbitrary T-of-N configurations for institutional custody.
//
// Uses the AES irreducible polynomial x^8 + x^4 + x^3 + x + 1 (0x11b).
import (
"crypto/rand"
"errors"
"fmt"
)
// Share is a single Shamir share: an evaluation of the secret polynomial at point Index.
type Share struct {
Index byte `json:"index"` // x-coordinate (1-based, never 0)
Data []byte `json:"data"` // y-coordinates (one per secret byte)
}
// ShamirSplit splits secret into n shares, requiring threshold to reconstruct.
// threshold must be >= 2 and <= n. n must be <= 255.
func ShamirSplit(secret []byte, n, threshold int) ([]Share, error) {
if len(secret) == 0 {
return nil, errors.New("shamir: secret must not be empty")
}
if threshold < 2 {
return nil, errors.New("shamir: threshold must be >= 2")
}
if n < threshold {
return nil, errors.New("shamir: n must be >= threshold")
}
if n > 255 {
return nil, errors.New("shamir: n must be <= 255")
}
shares := make([]Share, n)
for i := range shares {
shares[i] = Share{
Index: byte(i + 1),
Data: make([]byte, len(secret)),
}
}
// For each byte of the secret, create a random polynomial of degree (threshold-1)
// with the secret byte as the constant term, then evaluate at points 1..n.
coeffs := make([]byte, threshold)
for byteIdx := 0; byteIdx < len(secret); byteIdx++ {
// coeffs[0] = secret byte, rest are random
coeffs[0] = secret[byteIdx]
if _, err := rand.Read(coeffs[1:]); err != nil {
return nil, fmt.Errorf("shamir: random generation failed: %w", err)
}
for i := 0; i < n; i++ {
x := byte(i + 1)
shares[i].Data[byteIdx] = gf256Eval(coeffs, x)
}
}
return shares, nil
}
// ShamirCombine reconstructs the secret from threshold-or-more shares.
// Shares must have consistent Data lengths.
func ShamirCombine(shares []Share) ([]byte, error) {
if len(shares) < 2 {
return nil, errors.New("shamir: need at least 2 shares")
}
secretLen := len(shares[0].Data)
for _, s := range shares {
if len(s.Data) != secretLen {
return nil, errors.New("shamir: all shares must have the same data length")
}
}
// Check for duplicate indices
seen := make(map[byte]bool, len(shares))
for _, s := range shares {
if s.Index == 0 {
return nil, errors.New("shamir: share index must not be 0")
}
if seen[s.Index] {
return nil, fmt.Errorf("shamir: duplicate share index %d", s.Index)
}
seen[s.Index] = true
}
// Lagrange interpolation at x=0 for each byte position
secret := make([]byte, secretLen)
for byteIdx := 0; byteIdx < secretLen; byteIdx++ {
secret[byteIdx] = gf256Interpolate(shares, byteIdx)
}
return secret, nil
}
// gf256Eval evaluates polynomial coeffs[0] + coeffs[1]*x + coeffs[2]*x^2 + ...
// at the given point in GF(2^8).
func gf256Eval(coeffs []byte, x byte) byte {
// Horner's method
result := byte(0)
for i := len(coeffs) - 1; i >= 0; i-- {
result = gf256Add(gf256Mul(result, x), coeffs[i])
}
return result
}
// gf256Interpolate performs Lagrange interpolation at x=0 for a single byte position.
func gf256Interpolate(shares []Share, byteIdx int) byte {
result := byte(0)
n := len(shares)
for i := 0; i < n; i++ {
xi := shares[i].Index
yi := shares[i].Data[byteIdx]
// Compute Lagrange basis polynomial L_i(0)
// L_i(0) = product over j!=i of (0 - x_j) / (x_i - x_j)
// = product over j!=i of x_j / (x_i - x_j)
// (in GF(2^8), subtraction == addition == XOR)
num := byte(1)
den := byte(1)
for j := 0; j < n; j++ {
if i == j {
continue
}
xj := shares[j].Index
num = gf256Mul(num, xj)
den = gf256Mul(den, gf256Add(xi, xj))
}
// L_i(0) = num / den = num * den^(-1)
lagrange := gf256Mul(num, gf256Inv(den))
result = gf256Add(result, gf256Mul(yi, lagrange))
}
return result
}
// --- GF(2^8) arithmetic using AES polynomial (0x11b) ---
// gf256Add is addition in GF(2^8), which is XOR.
func gf256Add(a, b byte) byte {
return a ^ b
}
// gf256Mul is multiplication in GF(2^8) using the AES polynomial.
func gf256Mul(a, b byte) byte {
if a == 0 || b == 0 {
return 0
}
return gf256ExpTable[(int(gf256LogTable[a])+int(gf256LogTable[b]))%255]
}
// gf256Inv returns the multiplicative inverse in GF(2^8).
func gf256Inv(a byte) byte {
if a == 0 {
// Should never happen in well-formed shares.
panic("shamir: division by zero in GF(2^8)")
}
return gf256ExpTable[255-int(gf256LogTable[a])]
}
// Log and exp tables for GF(2^8) with generator 3 and AES polynomial 0x11b.
var gf256LogTable [256]byte
var gf256ExpTable [256]byte
func init() {
// Build log/exp tables using generator g=3 under polynomial 0x11b.
x := 1
for i := 0; i < 255; i++ {
gf256ExpTable[i] = byte(x)
gf256LogTable[x] = byte(i)
x = gf256MulNoTable(x, 3)
}
// gf256LogTable[0] is unused (log(0) is undefined); leave as 0.
// gf256ExpTable[255] wraps to gf256ExpTable[0] so that
// gf256Inv(1) = gf256ExpTable[255 - log(1)] = gf256ExpTable[255] = 1.
gf256ExpTable[255] = gf256ExpTable[0]
}
// gf256MulNoTable performs carry-less multiplication mod 0x11b without tables.
// Used only during init to build the tables.
func gf256MulNoTable(a, b int) int {
p := 0
for b > 0 {
if b&1 != 0 {
p ^= a
}
a <<= 1
if a&0x100 != 0 {
a ^= 0x11b
}
b >>= 1
}
return p
}