mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-01 23:15:27 +08:00
refactor: replace WASM-based PoW solver with a native Go implementation in the pow package
This commit is contained in:
2
.github/workflows/release-artifacts.yml
vendored
2
.github/workflows/release-artifacts.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
1
go.mod
@@ -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
2
go.sum
@@ -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=
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package deepseek
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed assets/sha3_wasm_bg.7b9ca65ddd.wasm
|
||||
var embeddedWASM []byte
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
64
pow/README.md
Normal 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
153
pow/deepseek_hash.go
Normal 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
139
pow/deepseek_pow.go
Normal 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
79
pow/deepseek_pow_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user