Skip to content

Commit b6cbda2

Browse files
stackdumpclaude
andcommitted
Built-in dev wallet for testing without MetaMask
Adds -dev flag that enables: - /api/dev/sign endpoint: signs messages with a built-in test key (anvil account 0: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266) - dev-wallet.js: browser shim that provides window.ethereum compatible interface, activated by ?dev-wallet URL parameter - Dev wallet banner shows at bottom of page when active Usage: ./bitwrap -port 8088 -dev # start with dev mode open http://localhost:8088/poll?dev-wallet # use dev wallet in browser Also fixes ecRecover v-parity bug: v=0/1 encodes R.y parity, not the rare rx+N case. Both devSign and cast wallet sign now produce signatures that RecoverAddress correctly verifies. The dev wallet calls /api/dev/sign server-side to avoid implementing ECDSA in JavaScript. Endpoint is gated behind -dev flag and returns 403 in production. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3357b61 commit b6cbda2

6 files changed

Lines changed: 444 additions & 6 deletions

File tree

cmd/bitwrap/main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ func main() {
2626
noProver := flag.Bool("no-prover", false, "Disable ZK prover (faster startup)")
2727
noSolgen := flag.Bool("no-solgen", false, "Disable Solidity generation endpoints")
2828
keyDir := flag.String("key-dir", "", "Directory for persistent circuit keys (enables fast restarts)")
29+
devMode := flag.Bool("dev", false, "Enable dev mode (built-in test wallet, /api/dev/* endpoints)")
2930
compile := flag.String("compile", "", "Compile a .btw file and output JSON schema to stdout")
3031
validate := flag.String("validate", "", "Validate a .btw file: compile → generate Solidity → forge build → forge test → deploy")
3132
output := flag.String("output", "", "Save generated Foundry project to this directory (use with -validate)")
@@ -67,6 +68,7 @@ func main() {
6768
EnableProver: !*noProver,
6869
EnableSolidity: !*noSolgen,
6970
KeyDir: *keyDir,
71+
DevMode: *devMode,
7072
})
7173

7274
addr := fmt.Sprintf(":%d", *port)
@@ -78,6 +80,9 @@ func main() {
7880
if *noSolgen {
7981
log.Printf("Solidity generation: disabled")
8082
}
83+
if *devMode {
84+
log.Printf("Dev mode: enabled (add ?dev-wallet to poll URL for built-in wallet)")
85+
}
8186
if err := http.ListenAndServe(addr, srv); err != nil {
8287
log.Fatalf("Server failed: %v", err)
8388
}

internal/server/auth.go

Lines changed: 95 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,106 @@ package server
22

33
import (
44
"encoding/hex"
5+
"encoding/json"
56
"errors"
67
"fmt"
78
"math/big"
9+
"net/http"
810
"strings"
911

1012
"golang.org/x/crypto/sha3"
1113
)
1214

15+
// devPrivateKey is a well-known test key (anvil account 0). Only used in dev mode.
16+
var devPrivateKey = fromHex("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80")
17+
18+
// handleDevSign signs a message with the built-in dev key.
19+
// Only available when server is started with -dev flag.
20+
func (s *Server) handleDevSign(w http.ResponseWriter, r *http.Request) {
21+
if !s.opts.DevMode {
22+
http.Error(w, "dev mode not enabled (start with -dev flag)", http.StatusForbidden)
23+
return
24+
}
25+
26+
var req struct {
27+
Message string `json:"message"`
28+
}
29+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Message == "" {
30+
http.Error(w, "message field required", http.StatusBadRequest)
31+
return
32+
}
33+
34+
sig, addr := devSign(req.Message)
35+
36+
w.Header().Set("Content-Type", "application/json")
37+
json.NewEncoder(w).Encode(map[string]string{
38+
"signature": sig,
39+
"address": addr,
40+
})
41+
}
42+
43+
// devSign signs a message with the dev private key using EIP-191 personal_sign.
44+
func devSign(message string) (string, string) {
45+
// EIP-191 prefix
46+
prefix := fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(message))
47+
hash := keccak256(append([]byte(prefix), []byte(message)...))
48+
z := new(big.Int).SetBytes(hash)
49+
50+
// Deterministic k (RFC 6979 simplified)
51+
kMat := make([]byte, 64)
52+
copy(kMat[:32], devPrivateKey.Bytes())
53+
copy(kMat[32:], hash)
54+
k := new(big.Int).SetBytes(keccak256(kMat))
55+
k.Mod(k, new(big.Int).Sub(secp256k1N, big.NewInt(1)))
56+
k.Add(k, big.NewInt(1))
57+
58+
rx, ry := ecMul(secp256k1Gx, secp256k1Gy, k)
59+
r := new(big.Int).Mod(rx, secp256k1N)
60+
s := new(big.Int).Mul(r, devPrivateKey)
61+
s.Add(s, z)
62+
s.Mod(s, secp256k1N)
63+
s.Mul(s, new(big.Int).ModInverse(k, secp256k1N))
64+
s.Mod(s, secp256k1N)
65+
66+
// Recovery id
67+
v := byte(27)
68+
if ry.Bit(0) == 1 {
69+
v = 28
70+
}
71+
72+
// Low-s normalization (EIP-2)
73+
halfN := new(big.Int).Rsh(secp256k1N, 1)
74+
if s.Cmp(halfN) > 0 {
75+
s.Sub(secp256k1N, s)
76+
if v == 27 {
77+
v = 28
78+
} else {
79+
v = 27
80+
}
81+
}
82+
83+
sigBytes := make([]byte, 65)
84+
rBytes := r.Bytes()
85+
sBytes := s.Bytes()
86+
copy(sigBytes[32-len(rBytes):32], rBytes)
87+
copy(sigBytes[64-len(sBytes):64], sBytes)
88+
sigBytes[64] = v
89+
90+
sigHex := "0x" + hex.EncodeToString(sigBytes)
91+
92+
// Derive address
93+
pubX, pubY := ecMul(secp256k1Gx, secp256k1Gy, devPrivateKey)
94+
pubBytes := make([]byte, 64)
95+
pxBytes := pubX.Bytes()
96+
pyBytes := pubY.Bytes()
97+
copy(pubBytes[32-len(pxBytes):32], pxBytes)
98+
copy(pubBytes[64-len(pyBytes):64], pyBytes)
99+
addrHash := keccak256(pubBytes)
100+
addr := "0x" + hex.EncodeToString(addrHash[12:])
101+
102+
return sigHex, addr
103+
}
104+
13105
// secp256k1 curve parameters
14106
var (
15107
secp256k1P = fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F")
@@ -76,14 +168,11 @@ func RecoverAddress(message string, signature string) (string, error) {
76168
// ecRecover recovers the public key from an ECDSA signature on secp256k1.
77169
// Returns (pubX, pubY) or error.
78170
func ecRecover(hash []byte, r, s *big.Int, v byte) (*big.Int, *big.Int, error) {
79-
// Calculate R point x-coordinate
171+
// R point x-coordinate is just r (the rx += N case is for v >= 2, extremely rare)
80172
rx := new(big.Int).Set(r)
81-
if v == 1 {
82-
rx.Add(rx, secp256k1N)
83-
}
84173

85-
// Decompress R point: find y from x on secp256k1
86-
ry := decompressPoint(rx, v%2 == 1)
174+
// v encodes the parity of R.y: v=0 → even, v=1 → odd
175+
ry := decompressPoint(rx, v == 1)
87176
if ry == nil {
88177
return nil, nil, errors.New("invalid signature: R not on curve")
89178
}

internal/server/devsign_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package server
2+
3+
import (
4+
"os/exec"
5+
"strings"
6+
"testing"
7+
)
8+
9+
func TestDevSignRecovery(t *testing.T) {
10+
msg := "bitwrap-create-poll:hello"
11+
12+
// Test devSign → RecoverAddress round trip
13+
sig, addr := devSign(msg)
14+
recovered, err := RecoverAddress(msg, sig)
15+
if err != nil {
16+
t.Fatalf("RecoverAddress(devSign): %v", err)
17+
}
18+
t.Logf("devSign addr: %s", addr)
19+
t.Logf("recovered addr: %s", recovered)
20+
21+
if recovered != addr {
22+
t.Errorf("devSign/RecoverAddress mismatch")
23+
}
24+
25+
// Also test with cast-generated signature if cast is available
26+
castBin, err := exec.LookPath("cast")
27+
if err != nil {
28+
t.Log("cast not installed, skipping cast parity test")
29+
return
30+
}
31+
32+
privKey := "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
33+
cmd := exec.Command(castBin, "wallet", "sign", "--private-key", privKey, msg)
34+
out, err := cmd.CombinedOutput()
35+
if err != nil {
36+
t.Fatalf("cast wallet sign: %v\n%s", err, out)
37+
}
38+
castSig := strings.TrimSpace(string(out))
39+
40+
castRecovered, err := RecoverAddress(msg, castSig)
41+
if err != nil {
42+
t.Fatalf("RecoverAddress(cast sig): %v", err)
43+
}
44+
t.Logf("cast recovered: %s", castRecovered)
45+
t.Logf("expected: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")
46+
47+
if !strings.EqualFold(castRecovered, "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266") {
48+
t.Errorf("RecoverAddress failed for cast signature too — RecoverAddress is broken")
49+
}
50+
}

internal/server/server.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type Options struct {
2626
EnableProver bool
2727
EnableSolidity bool
2828
KeyDir string // directory for persistent circuit keys (empty = no persistence)
29+
DevMode bool // enable dev endpoints (e.g., /api/dev/sign for wallet-free testing)
2930
}
3031

3132
// Server is the bitwrap HTTP server.
@@ -112,6 +113,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
112113
s.handleVK(w, r)
113114
case r.URL.Path == "/api/compile" && r.Method == http.MethodPost:
114115
s.handleCompile(w, r)
116+
case r.URL.Path == "/api/dev/sign" && r.Method == http.MethodPost:
117+
s.handleDevSign(w, r)
115118

116119
// Poll routes
117120
case r.URL.Path == "/api/polls" && r.Method == http.MethodPost:

0 commit comments

Comments
 (0)