Merge pull request #229 from CJackHwang/dev

refactor: replace WASM-based PoW solver with a native Go implementation in the pow package
This commit is contained in:
CJACK.
2026-04-07 00:57:33 +08:00
committed by GitHub
21 changed files with 472 additions and 357 deletions

View File

@@ -79,7 +79,7 @@ jobs:
CGO_ENABLED=0 GOOS="${GOOS}" GOARCH="${GOARCH}" \
go build -trimpath -ldflags="-s -w -X ds2api/internal/version.BuildVersion=${BUILD_VERSION}" -o "${STAGE}/${BIN}" ./cmd/ds2api
cp config.example.json .env.example internal/deepseek/assets/sha3_wasm_bg.7b9ca65ddd.wasm LICENSE README.MD README.en.md "${STAGE}/"
cp config.example.json .env.example LICENSE README.MD README.en.md "${STAGE}/"
cp -R static/admin "${STAGE}/static/admin"
if [ "${GOOS}" = "windows" ]; then

View File

@@ -34,7 +34,7 @@ CMD ["/usr/local/bin/ds2api"]
FROM runtime-base AS runtime-from-source
COPY --from=go-builder /out/ds2api /usr/local/bin/ds2api
COPY --from=go-builder /app/internal/deepseek/assets/sha3_wasm_bg.7b9ca65ddd.wasm /app/sha3_wasm_bg.7b9ca65ddd.wasm
COPY --from=go-builder /app/config.example.json /app/config.example.json
COPY --from=webui-builder /app/static/admin /app/static/admin
@@ -53,13 +53,13 @@ RUN set -eux; \
test -n "${PKG_DIR}"; \
mkdir -p /out/static; \
cp "${PKG_DIR}/ds2api" /out/ds2api; \
cp "${PKG_DIR}/sha3_wasm_bg.7b9ca65ddd.wasm" /out/sha3_wasm_bg.7b9ca65ddd.wasm; \
cp "${PKG_DIR}/config.example.json" /out/config.example.json; \
cp -R "${PKG_DIR}/static/admin" /out/static/admin
FROM runtime-base AS runtime-from-dist
COPY --from=dist-extract /out/ds2api /usr/local/bin/ds2api
COPY --from=dist-extract /out/sha3_wasm_bg.7b9ca65ddd.wasm /app/sha3_wasm_bg.7b9ca65ddd.wasm
COPY --from=dist-extract /out/config.example.json /app/config.example.json
COPY --from=dist-extract /out/static/admin /app/static/admin

1
go.mod
View File

@@ -8,7 +8,6 @@ require (
github.com/google/uuid v1.6.0
github.com/refraction-networking/utls v1.8.2
github.com/router-for-me/CLIProxyAPI/v6 v6.9.14
github.com/tetratelabs/wazero v1.11.0
)
require (

2
go.sum
View File

@@ -18,8 +18,6 @@ github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=

View File

@@ -275,7 +275,7 @@ func TestHandleNonStreamFencedToolCallExamplePromotesToolCall(t *testing.T) {
TestHandleNonStreamFencedToolCallExampleDoesNotPromoteToolCall(t)
}
func TestHandleNonStreamReturns502WhenUpstreamOutputEmpty(t *testing.T) {
func TestHandleNonStreamReturns429WhenUpstreamOutputEmpty(t *testing.T) {
h := &Handler{}
resp := makeSSEHTTPResponse(
`data: {"p":"response/content","v":""}`,
@@ -284,8 +284,8 @@ func TestHandleNonStreamReturns502WhenUpstreamOutputEmpty(t *testing.T) {
rec := httptest.NewRecorder()
h.handleNonStream(rec, context.Background(), resp, "cid-empty", "deepseek-chat", "prompt", false, nil)
if rec.Code != http.StatusBadGateway {
t.Fatalf("expected status 502 for empty upstream output, got %d body=%s", rec.Code, rec.Body.String())
if rec.Code != http.StatusTooManyRequests {
t.Fatalf("expected status 429 for empty upstream output, got %d body=%s", rec.Code, rec.Body.String())
}
out := decodeJSONBody(t, rec.Body.String())
errObj, _ := out["error"].(map[string]any)

View File

@@ -627,7 +627,7 @@ func TestHandleResponsesNonStreamToolChoiceNoneStillAllowsFunctionCall(t *testin
}
}
func TestHandleResponsesNonStreamReturns502WhenUpstreamOutputEmpty(t *testing.T) {
func TestHandleResponsesNonStreamReturns429WhenUpstreamOutputEmpty(t *testing.T) {
h := &Handler{}
rec := httptest.NewRecorder()
resp := &http.Response{
@@ -639,8 +639,8 @@ func TestHandleResponsesNonStreamReturns502WhenUpstreamOutputEmpty(t *testing.T)
}
h.handleResponsesNonStream(rec, resp, "owner-a", "resp_test", "deepseek-chat", "prompt", false, nil, util.DefaultToolChoicePolicy(), "")
if rec.Code != http.StatusBadGateway {
t.Fatalf("expected 502 for empty upstream output, got %d body=%s", rec.Code, rec.Body.String())
if rec.Code != http.StatusTooManyRequests {
t.Fatalf("expected 429 for empty upstream output, got %d body=%s", rec.Code, rec.Body.String())
}
out := decodeJSONBody(t, rec.Body.String())
errObj, _ := out["error"].(map[string]any)

View File

@@ -10,6 +10,6 @@ func writeUpstreamEmptyOutputError(w http.ResponseWriter, thinking, text string,
writeOpenAIErrorWithCode(w, http.StatusBadRequest, "Upstream content filtered the response and returned no output.", "content_filter")
return true
}
writeOpenAIErrorWithCode(w, http.StatusBadGateway, "Upstream model returned empty output.", "upstream_empty_output")
writeOpenAIErrorWithCode(w, http.StatusTooManyRequests, "Upstream model returned empty output.", "upstream_empty_output")
return true
}

View File

@@ -33,10 +33,6 @@ func ConfigPath() string {
return ResolvePath("DS2API_CONFIG_PATH", "config.json")
}
func WASMPath() string {
return ResolvePath("DS2API_WASM_PATH", "sha3_wasm_bg.7b9ca65ddd.wasm")
}
func RawStreamSampleRoot() string {
return ResolvePath("DS2API_RAW_STREAM_SAMPLE_ROOT", "tests/raw_stream_samples")
}

View File

@@ -109,7 +109,7 @@ func (c *Client) GetPow(ctx context.Context, a *auth.RequestAuth, maxAttempts in
data, _ := resp["data"].(map[string]any)
bizData, _ := data["biz_data"].(map[string]any)
challenge, _ := bizData["challenge"].(map[string]any)
answer, err := c.powSolver.Compute(ctx, challenge)
answer, err := ComputePow(challenge)
if err != nil {
attempts++
continue

View File

@@ -23,7 +23,6 @@ type Client struct {
stream trans.Doer
fallback *http.Client
fallbackS *http.Client
powSolver *PowSolver
maxRetries int
}
@@ -36,11 +35,11 @@ func NewClient(store *config.Store, resolver *auth.Resolver) *Client {
stream: trans.New(0),
fallback: &http.Client{Timeout: 60 * time.Second},
fallbackS: &http.Client{Timeout: 0},
powSolver: NewPowSolver(config.WASMPath()),
maxRetries: 3,
}
}
func (c *Client) PreloadPow(ctx context.Context) error {
return c.powSolver.init(ctx)
// PreloadPow 保留兼容接口,纯 Go 实现无需预加载。
func (c *Client) PreloadPow(_ context.Context) error {
return nil
}

View File

@@ -105,43 +105,16 @@ func TestBuildPowHeaderEmptyChallenge(t *testing.T) {
}
}
// ─── PowSolver pool size ─────────────────────────────────────────────
func TestPowPoolSizeFromEnvDefault(t *testing.T) {
t.Setenv("DS2API_POW_POOL_SIZE", "")
got := powPoolSizeFromEnv()
if got < 1 {
t.Fatalf("expected positive default pool size, got %d", got)
}
}
func TestPowPoolSizeFromEnvInvalid(t *testing.T) {
t.Setenv("DS2API_POW_POOL_SIZE", "abc")
got := powPoolSizeFromEnv()
if got < 1 {
t.Fatalf("expected positive default for invalid, got %d", got)
}
}
func TestPowPoolSizeFromEnvSpecificValue(t *testing.T) {
t.Setenv("DS2API_POW_POOL_SIZE", "5")
got := powPoolSizeFromEnv()
if got != 5 {
t.Fatalf("expected 5, got %d", got)
}
}
// ─── NewClient ───────────────────────────────────────────────────────
func TestNewClientInitialState(t *testing.T) {
client := NewClient(nil, nil)
if client.powSolver == nil {
t.Fatal("expected powSolver to be initialized")
if client == nil {
t.Fatal("expected non-nil client")
}
}
func TestNewClientPreloadPowIdempotent(t *testing.T) {
t.Setenv("DS2API_POW_POOL_SIZE", "1")
client := NewClient(nil, nil)
if err := client.PreloadPow(context.Background()); err != nil {
t.Fatalf("first preload failed: %v", err)
@@ -150,16 +123,3 @@ func TestNewClientPreloadPowIdempotent(t *testing.T) {
t.Fatalf("second preload failed: %v", err)
}
}
// ─── PowSolver init and module pool ──────────────────────────────────
func TestPowSolverPoolSizeMatchesEnv(t *testing.T) {
t.Setenv("DS2API_POW_POOL_SIZE", "2")
solver := NewPowSolver("test.wasm")
if err := solver.init(context.Background()); err != nil {
t.Fatalf("init failed: %v", err)
}
if cap(solver.pool) != 2 {
t.Fatalf("expected pool capacity 2, got %d", cap(solver.pool))
}
}

View File

@@ -1,6 +0,0 @@
package deepseek
import _ "embed"
//go:embed assets/sha3_wasm_bg.7b9ca65ddd.wasm
var embeddedWASM []byte

View File

@@ -1,220 +1,28 @@
package deepseek
import (
"context"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"math"
"os"
stdruntime "runtime"
"strconv"
"sync"
"ds2api/internal/config"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"ds2api/pow"
)
type PowSolver struct {
wasmPath string
once sync.Once
err error
runtime wazero.Runtime
compiled wazero.CompiledModule
pool chan *pooledModule
poolSize int
}
type pooledModule struct {
mod api.Module
stackFn api.Function
allocFn api.Function
freeFn api.Function
solveFn api.Function
}
func NewPowSolver(wasmPath string) *PowSolver {
return &PowSolver{wasmPath: wasmPath}
}
func (p *PowSolver) init(ctx context.Context) error {
p.once.Do(func() {
wasmBytes, err := os.ReadFile(p.wasmPath)
if err != nil {
if len(embeddedWASM) == 0 {
p.err = err
return
}
wasmBytes = embeddedWASM
}
p.runtime = wazero.NewRuntime(ctx)
p.compiled, p.err = p.runtime.CompileModule(ctx, wasmBytes)
if p.err == nil {
p.poolSize = powPoolSizeFromEnv()
p.pool = make(chan *pooledModule, p.poolSize)
for range p.poolSize {
inst, err := p.createModule(ctx)
if err != nil {
p.err = err
return
}
p.pool <- inst
}
}
})
return p.err
}
func (p *PowSolver) Compute(ctx context.Context, challenge map[string]any) (int64, error) {
if err := p.init(ctx); err != nil {
return 0, err
}
// ComputePow 使用纯 Go 实现求解 PoW challenge (DeepSeekHashV1)。
func ComputePow(challenge map[string]any) (int64, error) {
algo, _ := challenge["algorithm"].(string)
if algo != "DeepSeekHashV1" {
return 0, errors.New("unsupported algorithm")
}
challengeStr, _ := challenge["challenge"].(string)
salt, _ := challenge["salt"].(string)
signature, _ := challenge["signature"].(string)
targetPath, _ := challenge["target_path"].(string)
_ = signature
_ = targetPath
difficulty := toFloat64(challenge["difficulty"], 144000)
expireAt := toInt64(challenge["expire_at"], 1680000000)
prefix := salt + "_" + itoa(expireAt) + "_"
difficulty := toInt64FromFloat(challenge["difficulty"], 144000)
pm, err := p.acquireModule(ctx)
if err != nil {
return 0, err
}
defer p.releaseModule(pm)
mem := pm.mod.Memory()
if mem == nil {
return 0, errors.New("wasm memory missing")
}
retPtrs, err := pm.stackFn.Call(ctx, uint64(uint32(^uint32(15)))) // -16 i32
if err != nil || len(retPtrs) == 0 {
return 0, errors.New("stack alloc failed")
}
retptr := uint32(retPtrs[0])
defer func() {
_, _ = pm.stackFn.Call(context.Background(), 16)
}()
chPtr, chLen, err := writeUTF8(ctx, pm.allocFn, mem, challengeStr)
if err != nil {
return 0, err
}
defer freeUTF8(pm.freeFn, chPtr, chLen)
prefixPtr, prefixLen, err := writeUTF8(ctx, pm.allocFn, mem, prefix)
if err != nil {
return 0, err
}
defer freeUTF8(pm.freeFn, prefixPtr, prefixLen)
if _, err := pm.solveFn.Call(ctx,
uint64(retptr),
uint64(chPtr), uint64(chLen),
uint64(prefixPtr), uint64(prefixLen),
math.Float64bits(difficulty),
); err != nil {
return 0, err
}
statusBytes, ok := mem.Read(retptr, 4)
if !ok {
return 0, errors.New("read status failed")
}
status := int32(binary.LittleEndian.Uint32(statusBytes))
valueBytes, ok := mem.Read(retptr+8, 8)
if !ok {
return 0, errors.New("read value failed")
}
value := math.Float64frombits(binary.LittleEndian.Uint64(valueBytes))
if status == 0 {
return 0, errors.New("pow solve failed")
}
return int64(value), nil
}
func (p *PowSolver) createModule(ctx context.Context) (*pooledModule, error) {
mod, err := p.runtime.InstantiateModule(ctx, p.compiled, wazero.NewModuleConfig())
if err != nil {
return nil, err
}
stackFn := mod.ExportedFunction("__wbindgen_add_to_stack_pointer")
allocFn := mod.ExportedFunction("__wbindgen_export_0")
solveFn := mod.ExportedFunction("wasm_solve")
if stackFn == nil || allocFn == nil || solveFn == nil {
_ = mod.Close(context.Background())
return nil, errors.New("required wasm exports missing")
}
return &pooledModule{
mod: mod,
stackFn: stackFn,
allocFn: allocFn,
freeFn: mod.ExportedFunction("__wbindgen_export_2"),
solveFn: solveFn,
}, nil
}
func (p *PowSolver) acquireModule(ctx context.Context) (*pooledModule, error) {
if p.pool != nil {
for {
select {
case pm := <-p.pool:
if pm != nil {
return pm, nil
}
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
return p.createModule(ctx)
}
func (p *PowSolver) releaseModule(pm *pooledModule) {
if pm == nil || pm.mod == nil {
return
}
if p.pool != nil {
select {
case p.pool <- pm:
return
default:
}
}
_ = pm.mod.Close(context.Background())
}
func writeUTF8(ctx context.Context, allocFn api.Function, mem api.Memory, text string) (uint32, uint32, error) {
data := []byte(text)
res, err := allocFn.Call(ctx, uint64(len(data)), 1)
if err != nil || len(res) == 0 {
return 0, 0, errors.New("alloc failed")
}
ptr := uint32(res[0])
if !mem.Write(ptr, data) {
return 0, 0, errors.New("mem write failed")
}
return ptr, uint32(len(data)), nil
}
func freeUTF8(freeFn api.Function, ptr, size uint32) {
if freeFn == nil || ptr == 0 || size == 0 {
return
}
_, _ = freeFn.Call(context.Background(), uint64(ptr), uint64(size), 1)
return pow.SolvePow(challengeStr, salt, expireAt, difficulty)
}
// BuildPowHeader 序列化 {algorithm,challenge,salt,answer,signature,target_path} 为 base64(JSON)。
func BuildPowHeader(challenge map[string]any, answer int64) (string, error) {
payload := map[string]any{
"algorithm": challenge["algorithm"],
@@ -257,32 +65,7 @@ func toInt64(v any, d int64) int64 {
}
}
func itoa(n int64) string {
return strconv.FormatInt(n, 10)
}
func powPoolSizeFromEnv() int {
const fallback = 4
n := fallback
if cpus := stdruntime.GOMAXPROCS(0); cpus > 0 {
n = cpus
}
if raw := os.Getenv("DS2API_POW_POOL_SIZE"); raw != "" {
if v, err := strconv.Atoi(raw); err == nil && v > 0 {
n = v
}
}
if n > 64 {
return 64
}
return n
}
func PreloadWASM(wasmPath string) {
solver := NewPowSolver(wasmPath)
if err := solver.init(context.Background()); err != nil {
config.Logger.Warn("[WASM] preload failed", "error", err)
return
}
config.Logger.Info("[WASM] module preloaded", "path", wasmPath)
// toInt64FromFloat 与 toInt64 等价,仅名称区分用途。
func toInt64FromFloat(v any, d int64) int64 {
return toInt64(v, d)
}

View File

@@ -3,66 +3,18 @@ package deepseek
import (
"context"
"testing"
"time"
)
func TestPowPoolSizeFromEnv(t *testing.T) {
t.Setenv("DS2API_POW_POOL_SIZE", "3")
if got := powPoolSizeFromEnv(); got != 3 {
t.Fatalf("expected pool size 3, got %d", got)
}
}
func TestPowSolverAcquireReleaseReusesModule(t *testing.T) {
t.Setenv("DS2API_POW_POOL_SIZE", "1")
solver := NewPowSolver("missing-file.wasm")
if err := solver.init(context.Background()); err != nil {
t.Fatalf("init failed: %v", err)
}
pm1, err := solver.acquireModule(context.Background())
if err != nil {
t.Fatalf("acquire first module failed: %v", err)
}
solver.releaseModule(pm1)
pm2, err := solver.acquireModule(context.Background())
if err != nil {
t.Fatalf("acquire second module failed: %v", err)
}
if pm1 != pm2 {
t.Fatalf("expected pooled module reuse, got different instances")
}
solver.releaseModule(pm2)
}
func TestPowSolverAcquireHonorsContextWhenPoolExhausted(t *testing.T) {
t.Setenv("DS2API_POW_POOL_SIZE", "1")
solver := NewPowSolver("missing-file.wasm")
if err := solver.init(context.Background()); err != nil {
t.Fatalf("init failed: %v", err)
}
held, err := solver.acquireModule(context.Background())
if err != nil {
t.Fatalf("acquire held module failed: %v", err)
}
defer solver.releaseModule(held)
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
defer cancel()
if _, err := solver.acquireModule(ctx); err == nil {
t.Fatalf("expected context cancellation while pool is exhausted")
}
}
func TestClientPreloadPowUsesClientSolver(t *testing.T) {
t.Setenv("DS2API_POW_POOL_SIZE", "1")
func TestPreloadPowNoOp(t *testing.T) {
client := NewClient(nil, nil)
if err := client.PreloadPow(context.Background()); err != nil {
t.Fatalf("preload failed: %v", err)
}
if client.powSolver.runtime == nil || client.powSolver.compiled == nil {
t.Fatalf("expected client pow solver to be initialized")
t.Fatalf("PreloadPow should be no-op, got error: %v", err)
}
}
func TestComputePowUnsupportedAlgorithm(t *testing.T) {
_, err := ComputePow(map[string]any{"algorithm": "unknown"})
if err == nil {
t.Fatal("expected error for unsupported algorithm")
}
}

View File

@@ -42,9 +42,9 @@ func NewApp() (*App, error) {
})
dsClient = deepseek.NewClient(store, resolver)
if err := dsClient.PreloadPow(context.Background()); err != nil {
config.Logger.Warn("[WASM] preload failed", "error", err)
config.Logger.Warn("[PoW] init failed", "error", err)
} else {
config.Logger.Info("[WASM] module preloaded", "path", config.WASMPath())
config.Logger.Info("[PoW] pure Go solver ready")
}
openaiHandler := &openai.Handler{Store: store, Auth: resolver, DS: dsClient}

64
pow/README.md Normal file
View File

@@ -0,0 +1,64 @@
# DeepSeek PoW 纯算实现
替代 `internal/deepseek/assets/sha3_wasm_bg.*.wasm` + wazero 运行时。
## 算法
DeepSeekHashV1 = SHA3-256 但 **Keccak-f[1600] 跳过 round 0** (只做 rounds 1..23)。其余参数不变:
rate=136, padding=0x06+0x80, output=32 字节。
PoW 协议:服务端选 answer ∈ [0, difficulty),计算 `challenge = hash(prefix + str(answer))`
客户端遍历 [0, difficulty) 找到匹配的 nonce。
```
prefix = salt + "_" + str(expire_at) + "_"
input = (prefix + str(nonce)).encode("utf-8")
hash = DeepSeekHashV1(input) → 32 bytes
header = base64(json({algorithm, challenge, salt, answer, signature, target_path}))
```
## 性能 (Apple M4, Go 1.25)
```
BenchmarkHash 187.5 ns/op 0 alloc → 5.33M hash/s
BenchmarkSolve 13.4 ms/op 2 alloc → 75 道/秒/核 (difficulty=144000)
```
对比 wazero 调 WASM: hash 快 **5×**, solve 快 **2.8×**
## 测试
```bash
cd pow && go test -v ./... && go test -bench=. -benchmem
```
## 替换 WASM
替换 `internal/deepseek/pow.go``PowSolver.Compute`:
```go
// 原: 调 wasm_solve(retptr, chPtr, chLen, prefixPtr, prefixLen, difficulty)
// 新:
import "ds2api/pow"
func (c *Client) GetPow(ctx context.Context, a *auth.RequestAuth, ...) (string, error) {
// ... 省略 token/retry 逻辑,只改 compute 部分 ...
challenge, _ := bizData["challenge"].(map[string]any)
ch := &pow.Challenge{
Algorithm: challenge["algorithm"].(string),
Challenge: challenge["challenge"].(string),
Salt: challenge["salt"].(string),
ExpireAt: int64(challenge["expire_at"].(float64)),
Difficulty: int64(challenge["difficulty"].(float64)),
Signature: challenge["signature"].(string),
TargetPath: challenge["target_path"].(string),
}
return pow.SolveAndBuildHeader(ch)
}
```
可删除:
- `internal/deepseek/assets/sha3_wasm_bg.*.wasm`
- `internal/deepseek/embedded_pow.go`
- `internal/deepseek/pow.go``PowSolver` 结构体、wazero 相关池化代码
- `go.mod``github.com/tetratelabs/wazero` 依赖

153
pow/deepseek_hash.go Normal file
View File

@@ -0,0 +1,153 @@
// Package pow 提供 DeepSeekHashV1 纯 Go 实现。
// DeepSeekHashV1 = SHA3-256 但跳过 Keccak-f[1600] round 0 (只做 rounds 1..23)。
package pow
import "encoding/binary"
var rc = [24]uint64{
0x0000000000000001, 0x0000000000008082, 0x800000000000808A, 0x8000000080008000,
0x000000000000808B, 0x0000000080000001, 0x8000000080008081, 0x8000000000008009,
0x000000000000008A, 0x0000000000000088, 0x0000000080008009, 0x000000008000000A,
0x000000008000808B, 0x800000000000008B, 0x8000000000008089, 0x8000000000008003,
0x8000000000008002, 0x8000000000000080, 0x000000000000800A, 0x800000008000000A,
0x8000000080008081, 0x8000000000008080, 0x0000000080000001, 0x8000000080008008,
}
func rotl64(v uint64, k uint) uint64 { return v<<k | v>>(64-k) }
func keccakF23(s *[25]uint64) {
a0, a1, a2, a3, a4 := s[0], s[1], s[2], s[3], s[4]
a5, a6, a7, a8, a9 := s[5], s[6], s[7], s[8], s[9]
a10, a11, a12, a13, a14 := s[10], s[11], s[12], s[13], s[14]
a15, a16, a17, a18, a19 := s[15], s[16], s[17], s[18], s[19]
a20, a21, a22, a23, a24 := s[20], s[21], s[22], s[23], s[24]
for r := 1; r < 24; r++ {
c0 := a0 ^ a5 ^ a10 ^ a15 ^ a20
c1 := a1 ^ a6 ^ a11 ^ a16 ^ a21
c2 := a2 ^ a7 ^ a12 ^ a17 ^ a22
c3 := a3 ^ a8 ^ a13 ^ a18 ^ a23
c4 := a4 ^ a9 ^ a14 ^ a19 ^ a24
d0 := c4 ^ rotl64(c1, 1)
d1 := c0 ^ rotl64(c2, 1)
d2 := c1 ^ rotl64(c3, 1)
d3 := c2 ^ rotl64(c4, 1)
d4 := c3 ^ rotl64(c0, 1)
a0 ^= d0
a5 ^= d0
a10 ^= d0
a15 ^= d0
a20 ^= d0
a1 ^= d1
a6 ^= d1
a11 ^= d1
a16 ^= d1
a21 ^= d1
a2 ^= d2
a7 ^= d2
a12 ^= d2
a17 ^= d2
a22 ^= d2
a3 ^= d3
a8 ^= d3
a13 ^= d3
a18 ^= d3
a23 ^= d3
a4 ^= d4
a9 ^= d4
a14 ^= d4
a19 ^= d4
a24 ^= d4
b0 := a0
b10 := rotl64(a1, 1)
b20 := rotl64(a2, 62)
b5 := rotl64(a3, 28)
b15 := rotl64(a4, 27)
b16 := rotl64(a5, 36)
b1 := rotl64(a6, 44)
b11 := rotl64(a7, 6)
b21 := rotl64(a8, 55)
b6 := rotl64(a9, 20)
b7 := rotl64(a10, 3)
b17 := rotl64(a11, 10)
b2 := rotl64(a12, 43)
b12 := rotl64(a13, 25)
b22 := rotl64(a14, 39)
b23 := rotl64(a15, 41)
b8 := rotl64(a16, 45)
b18 := rotl64(a17, 15)
b3 := rotl64(a18, 21)
b13 := rotl64(a19, 8)
b14 := rotl64(a20, 18)
b24 := rotl64(a21, 2)
b9 := rotl64(a22, 61)
b19 := rotl64(a23, 56)
b4 := rotl64(a24, 14)
a0 = b0 ^ (^b1 & b2)
a1 = b1 ^ (^b2 & b3)
a2 = b2 ^ (^b3 & b4)
a3 = b3 ^ (^b4 & b0)
a4 = b4 ^ (^b0 & b1)
a5 = b5 ^ (^b6 & b7)
a6 = b6 ^ (^b7 & b8)
a7 = b7 ^ (^b8 & b9)
a8 = b8 ^ (^b9 & b5)
a9 = b9 ^ (^b5 & b6)
a10 = b10 ^ (^b11 & b12)
a11 = b11 ^ (^b12 & b13)
a12 = b12 ^ (^b13 & b14)
a13 = b13 ^ (^b14 & b10)
a14 = b14 ^ (^b10 & b11)
a15 = b15 ^ (^b16 & b17)
a16 = b16 ^ (^b17 & b18)
a17 = b17 ^ (^b18 & b19)
a18 = b18 ^ (^b19 & b15)
a19 = b19 ^ (^b15 & b16)
a20 = b20 ^ (^b21 & b22)
a21 = b21 ^ (^b22 & b23)
a22 = b22 ^ (^b23 & b24)
a23 = b23 ^ (^b24 & b20)
a24 = b24 ^ (^b20 & b21)
a0 ^= rc[r]
}
s[0], s[1], s[2], s[3], s[4] = a0, a1, a2, a3, a4
s[5], s[6], s[7], s[8], s[9] = a5, a6, a7, a8, a9
s[10], s[11], s[12], s[13], s[14] = a10, a11, a12, a13, a14
s[15], s[16], s[17], s[18], s[19] = a15, a16, a17, a18, a19
s[20], s[21], s[22], s[23], s[24] = a20, a21, a22, a23, a24
}
// DeepSeekHashV1 返回 data 的 32 字节摘要,与 WASM wasm_deepseek_hash_v1 等价。
func DeepSeekHashV1(data []byte) [32]byte {
const rate = 136
var s [25]uint64
off := 0
for off+rate <= len(data) {
for i := 0; i < rate/8; i++ {
s[i] ^= binary.LittleEndian.Uint64(data[off+i*8:])
}
keccakF23(&s)
off += rate
}
var final [rate]byte
copy(final[:], data[off:])
final[len(data)-off] = 0x06
final[rate-1] |= 0x80
for i := 0; i < rate/8; i++ {
s[i] ^= binary.LittleEndian.Uint64(final[i*8:])
}
keccakF23(&s)
var out [32]byte
binary.LittleEndian.PutUint64(out[0:], s[0])
binary.LittleEndian.PutUint64(out[8:], s[1])
binary.LittleEndian.PutUint64(out[16:], s[2])
binary.LittleEndian.PutUint64(out[24:], s[3])
return out
}

139
pow/deepseek_pow.go Normal file
View File

@@ -0,0 +1,139 @@
package pow
import (
"encoding/base64"
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
"strconv"
)
// Challenge 对应 /api/v0/chat/create_pow_challenge 返回的 data.biz_data.challenge。
type Challenge struct {
Algorithm string `json:"algorithm"`
Challenge string `json:"challenge"`
Salt string `json:"salt"`
ExpireAt int64 `json:"expire_at"`
Difficulty int64 `json:"difficulty"`
Signature string `json:"signature"`
TargetPath string `json:"target_path"`
}
// BuildPrefix: "<salt>_<expire_at>_" (对应 pow.go:89)
func BuildPrefix(salt string, expireAt int64) string {
return salt + "_" + strconv.FormatInt(expireAt, 10) + "_"
}
// SolvePow 搜索 nonce ∈ [0, difficulty) 使得 DeepSeekHashV1(prefix+str(nonce)) == challenge。
// prefix 预吸收进 state,循环内零分配。
func SolvePow(challengeHex, salt string, expireAt, difficulty int64) (int64, error) {
if len(challengeHex) != 64 {
return 0, errors.New("pow: challenge must be 64 hex chars")
}
target, err := hex.DecodeString(challengeHex)
if err != nil {
return 0, err
}
var ta [32]byte
copy(ta[:], target)
t0 := binary.LittleEndian.Uint64(ta[0:])
t1 := binary.LittleEndian.Uint64(ta[8:])
t2 := binary.LittleEndian.Uint64(ta[16:])
t3 := binary.LittleEndian.Uint64(ta[24:])
prefix := []byte(BuildPrefix(salt, expireAt))
const rate = 136
var baseState [25]uint64
off := 0
for off+rate <= len(prefix) {
for i := 0; i < rate/8; i++ {
baseState[i] ^= binary.LittleEndian.Uint64(prefix[off+i*8:])
}
keccakF23(&baseState)
off += rate
}
tailLen := len(prefix) - off
var tail [rate]byte
copy(tail[:], prefix[off:])
var numBuf [20]byte
for n := int64(0); n < difficulty; n++ {
v := uint64(n)
pos := 20
if v == 0 {
pos--
numBuf[pos] = '0'
} else {
for v > 0 {
pos--
numBuf[pos] = byte('0' + v%10)
v /= 10
}
}
numLen := 20 - pos
s := baseState
totalTail := tailLen + numLen
if totalTail < rate {
var buf [rate]byte
copy(buf[:tailLen], tail[:tailLen])
copy(buf[tailLen:totalTail], numBuf[pos:])
buf[totalTail] = 0x06
buf[rate-1] |= 0x80
for i := 0; i < rate/8; i++ {
s[i] ^= binary.LittleEndian.Uint64(buf[i*8:])
}
keccakF23(&s)
} else {
var buf [rate]byte
copy(buf[:tailLen], tail[:tailLen])
copy(buf[tailLen:rate], numBuf[pos:pos+(rate-tailLen)])
for i := 0; i < rate/8; i++ {
s[i] ^= binary.LittleEndian.Uint64(buf[i*8:])
}
keccakF23(&s)
var buf2 [rate]byte
rem := totalTail - rate
copy(buf2[:rem], numBuf[pos+(rate-tailLen):pos+(rate-tailLen)+rem])
buf2[rem] = 0x06
buf2[rate-1] |= 0x80
for i := 0; i < rate/8; i++ {
s[i] ^= binary.LittleEndian.Uint64(buf2[i*8:])
}
keccakF23(&s)
}
if s[0] == t0 && s[1] == t1 && s[2] == t2 && s[3] == t3 {
return n, nil
}
}
return 0, errors.New("pow: no solution within difficulty")
}
// BuildPowHeader 序列化 {algorithm,challenge,salt,answer,signature,target_path} 为 base64(JSON)。
// 不含 difficulty/expire_at (对应 pow.go:218)。
func BuildPowHeader(c *Challenge, answer int64) (string, error) {
b, err := json.Marshal(map[string]any{
"algorithm": c.Algorithm, "challenge": c.Challenge, "salt": c.Salt,
"answer": answer, "signature": c.Signature, "target_path": c.TargetPath,
})
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(b), nil
}
// SolveAndBuildHeader 端到端: Challenge → x-ds-pow-response header string。
func SolveAndBuildHeader(c *Challenge) (string, error) {
if c.Algorithm != "DeepSeekHashV1" {
return "", errors.New("pow: unsupported algorithm: " + c.Algorithm)
}
d := c.Difficulty
if d == 0 {
d = 144000
}
answer, err := SolvePow(c.Challenge, c.Salt, c.ExpireAt, d)
if err != nil {
return "", err
}
return BuildPowHeader(c, answer)
}

79
pow/deepseek_pow_test.go Normal file
View File

@@ -0,0 +1,79 @@
package pow
import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"strconv"
"testing"
)
// 测试向量来自直接调用 DeepSeek 官方 WASM。
func TestDeepSeekHashV1(t *testing.T) {
for _, tc := range []struct{ in, want string }{
{"", "e594808bc5b7151ac160c6d39a02e0a8e261ed588578403099e3561dc40c26b3"},
{"testsalt_1700000000_42", "d4a2ea58c89e40887c933484868380c6f803eaa8dc53a3b9df8e431b921a4f09"},
{"testsalt_1700000000_100000", "abea2f35796b65486e9be1b36f7878c66cab021e96faa473fdf4decd31f9ba30"},
{"abc123salt_1700000000_12345", "74b3b7452745b70e85eb32ee7f0a9ec0381d42dd5137b695da915e104fc390e1"},
} {
h := DeepSeekHashV1([]byte(tc.in))
got := hex.EncodeToString(h[:])
if got != tc.want {
t.Errorf("hash(%q) = %s, want %s", tc.in, got, tc.want)
}
}
}
func TestSolvePow(t *testing.T) {
for _, tc := range []struct {
salt string
expire int64
answer int64
diff int64
}{
{"testsalt", 1700000000, 42, 1000},
{"testsalt", 1700000000, 500, 2000},
{"abc123salt", 1700000000, 12345, 20000},
} {
h := DeepSeekHashV1([]byte(BuildPrefix(tc.salt, tc.expire) + strconv.FormatInt(tc.answer, 10)))
got, err := SolvePow(hex.EncodeToString(h[:]), tc.salt, tc.expire, tc.diff)
if err != nil || got != tc.answer {
t.Errorf("salt=%q answer=%d: got=%d err=%v", tc.salt, tc.answer, got, err)
}
}
}
func TestSolveAndBuildHeader(t *testing.T) {
t0 := DeepSeekHashV1([]byte("salt_1712345678_777"))
header, err := SolveAndBuildHeader(&Challenge{
Algorithm: "DeepSeekHashV1", Challenge: hex.EncodeToString(t0[:]),
Salt: "salt", ExpireAt: 1712345678, Difficulty: 2000,
Signature: "sig", TargetPath: "/api/v0/chat/completion",
})
if err != nil {
t.Fatal(err)
}
raw, _ := base64.StdEncoding.DecodeString(header)
var m map[string]any
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
if int64(m["answer"].(float64)) != 777 {
t.Errorf("answer = %v, want 777", m["answer"])
}
}
func BenchmarkHash(b *testing.B) {
d := []byte("realisticsalt_1712345678_12345")
for i := 0; i < b.N; i++ {
DeepSeekHashV1(d)
}
}
func BenchmarkSolve(b *testing.B) {
h := DeepSeekHashV1([]byte("realisticsalt_1712345678_72000"))
ch := hex.EncodeToString(h[:])
for i := 0; i < b.N; i++ {
_, _ = SolvePow(ch, "realisticsalt", 1712345678, 144000)
}
}

View File

@@ -4,7 +4,6 @@
"outputDirectory": "static",
"functions": {
"api/chat-stream.js": {
"includeFiles": "internal/deepseek/assets/sha3_wasm_bg.7b9ca65ddd.wasm",
"maxDuration": 300
},
"api/index.go": {