LINUX SB 快照站

单php文件实现的工作量证明代币平台

原帖: linux.sb/topic/10552 · 共 4 楼 · 标题快照 2026-08-12 10:40:03

楼主
发帖 2026-08-10 13:08:36 · 快照 2026-08-12 10:40:03

不管怎样,适合学校学生使用,或者朋友间玩耍。
直接点击按钮就可以获得一个密码,密码直接用于登录。
可以使用自带的web worker轻松挖矿。
直接部署index.php到任何文件可写的服务器即可。
index.php:

<?php
$DATA_DIR = __DIR__ . '/data';
$USER_FILE = $DATA_DIR . '/users.json';
$GLOBAL_HEAD_FILE = $DATA_DIR . '/head';
$MINTED_DIR = $DATA_DIR . '/minted';
$WALLETS_DIR = $DATA_DIR . '/wallets';
$ZERO_HASH = str_repeat('0', 64);
$MINT_PREFIX = '000000';

date_default_timezone_set('PRC');

/** 64 进制字母表(64 字符) */
const RADIX64_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_';

function intToRadix64(int $n): string
{
    if ($n === 0) {
        return '0';
    }
    $s = '';
    while ($n > 0) {
        $s = RADIX64_ALPHABET[$n % 64] . $s;
        $n = intdiv($n, 64);
    }
    return $s;
}

function randomRadix64Id(): string
{
    return intToRadix64(random_int(0, PHP_INT_MAX));
}

function q(string $s): string
{
    return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}

function currentCredential(): string
{
    return isset($_GET['credential']) ? (string)$_GET['credential'] : (isset($_POST['credential']) ? (string)$_POST['credential'] : '');
}

function jsonResponse(array $data, int $code = 200): void
{
    http_response_code($code);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($data, JSON_UNESCAPED_UNICODE);
    exit;
}

function ensureDataDir(): void
{
    global $DATA_DIR, $MINTED_DIR, $WALLETS_DIR;
    if (!is_dir($DATA_DIR)) {
        mkdir($DATA_DIR, 0755, true);
    }
    if (!is_dir($MINTED_DIR)) {
        mkdir($MINTED_DIR, 0755, true);
    }
    if (!is_dir($WALLETS_DIR)) {
        mkdir($WALLETS_DIR, 0755, true);
    }
}

function loadUsers(): array
{
    global $USER_FILE;
    if (!is_readable($USER_FILE)) {
        return [];
    }
    $raw = file_get_contents($USER_FILE);
    $d = $raw !== false ? json_decode($raw, true) : null;
    return is_array($d) ? $d : [];
}

function saveUsers(array $users): void
{
    global $USER_FILE;
    ensureDataDir();
    file_put_contents($USER_FILE, json_encode($users, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
}

function findUsernameByCredential(string $credential): ?string
{
    $users = loadUsers();
    foreach ($users as $name => $info) {
        if (isset($info['credential']) && (string)$info['credential'] === (string)$credential) {
            return $name;
        }
    }
    return null;
}

function safeUsername(string $username): string
{
    return preg_replace('/[^a-zA-Z0-9\-_]/', '', $username);
}

function walletDir(string $username): string
{
    global $WALLETS_DIR;
    return $WALLETS_DIR . '/' . safeUsername($username);
}

function ensureWalletDir(string $username): string
{
    $dir = walletDir($username);
    if (!is_dir($dir)) {
        mkdir($dir, 0755, true);
    }
    return $dir;
}

function mintedTokenPath(string $hash): string
{
    global $MINTED_DIR;
    return $MINTED_DIR . '/' . $hash . '.token';
}

function walletTokenPath(string $username, string $hash): string
{
    return walletDir($username) . '/' . $hash . '.token';
}

function getLatestHash(): string
{
    global $ZERO_HASH, $GLOBAL_HEAD_FILE;
    if (!is_readable($GLOBAL_HEAD_FILE)) {
        return $ZERO_HASH;
    }
    $h = trim((string)file_get_contents($GLOBAL_HEAD_FILE));
    if (!preg_match('/^[0-9a-f]{64}$/', $h)) {
        return $ZERO_HASH;
    }
    return $h;
}

function setLatestHash(string $hash): void
{
    global $GLOBAL_HEAD_FILE;
    if (!preg_match('/^[0-9a-f]{64}$/', $hash)) {
        throw new InvalidArgumentException('无效的代币哈希');
    }
    ensureDataDir();
    file_put_contents($GLOBAL_HEAD_FILE, $hash);
}

function listTokenFiles(string $username): array
{
    $dir = walletDir($username);
    if (!is_dir($dir)) {
        return [];
    }
    $files = glob($dir . '/*.token');
    if ($files === false) {
        return [];
    }
    usort($files, function ($a, $b) {
        $ma = @filemtime($a);
        $mb = @filemtime($b);
        if ($ma === $mb) {
            return strcmp($a, $b);
        }
        return $ma <=> $mb;
    });
    return $files;
}

function getTokenCount(string $username): int
{
    return count(listTokenFiles($username));
}

function sha256Hex(string $input): string
{
    return hash('sha256', $input);
}

function verifyMintInput(string $input, string $prevHash): string
{
    global $MINT_PREFIX, $ZERO_HASH;
    if ($prevHash === '') {
        $prevHash = $ZERO_HASH;
    }
    if (!preg_match('/^[0-9a-f]{64}$/', $prevHash)) {
        throw new InvalidArgumentException('上一个代币哈希无效');
    }
    if ($input === '' || strpos($input, $prevHash) !== 0) {
        throw new InvalidArgumentException('输入必须以当前上一个代币哈希为前缀');
    }
    $hash = sha256Hex($input);
    if (strpos($hash, $MINT_PREFIX) !== 0) {
        throw new InvalidArgumentException('哈希未满足 6 个前导零');
    }
    return $hash;
}

function addToken(string $username, string $input): array
{
    global $GLOBAL_HEAD_FILE, $ZERO_HASH;

    ensureWalletDir($username);
    ensureDataDir();

    $fp = fopen($GLOBAL_HEAD_FILE, 'c+');
    if ($fp === false) {
        throw new RuntimeException('无法打开全局链头');
    }
    if (!flock($fp, LOCK_EX)) {
        fclose($fp);
        throw new RuntimeException('无法锁定全局链头');
    }

    $raw = stream_get_contents($fp);
    $prevHash = $ZERO_HASH;
    if ($raw !== false && $raw !== '') {
        $h = trim($raw);
        if (preg_match('/^[0-9a-f]{64}$/', $h)) {
            $prevHash = $h;
        }
    }

    try {
        $hash = verifyMintInput($input, $prevHash);

        $mintedPath = mintedTokenPath($hash);
        if (is_file($mintedPath)) {
            throw new InvalidArgumentException('该代币已存在');
        }

        if (file_put_contents($mintedPath, $input, LOCK_EX) === false) {
            throw new RuntimeException('写入全局代币失败');
        }

        $walletPath = walletTokenPath($username, $hash);
        if (file_put_contents($walletPath, $input, LOCK_EX) === false) {
            @unlink($mintedPath);
            throw new RuntimeException('写入持有记录失败');
        }

        rewind($fp);
        ftruncate($fp, 0);
        fwrite($fp, $hash);
        fflush($fp);
    } finally {
        flock($fp, LOCK_UN);
        fclose($fp);
    }

    return [
        'hash' => $hash,
        'count' => getTokenCount($username),
        'prev_hash' => $prevHash,
        'latest_hash' => $hash,
    ];
}

function transferTokens(string $fromUser, string $toUser, int $count): array
{
    if ($fromUser === $toUser) {
        throw new InvalidArgumentException('不能向自己转账');
    }
    if ($count <= 0) {
        throw new InvalidArgumentException('转账数量必须大于 0');
    }

    $users = loadUsers();
    if (!isset($users[$toUser])) {
        throw new InvalidArgumentException('目标账户不存在');
    }

    ensureWalletDir($toUser);
    $files = listTokenFiles($fromUser);

    if (count($files) < $count) {
        throw new InvalidArgumentException('代币数量不足');
    }

    $moved = [];
    for ($i = 0; $i < $count; $i++) {
        $src = $files[$i];
        $hash = basename($src, '.token');
        if (!preg_match('/^[0-9a-f]{64}$/', $hash)) {
            throw new RuntimeException('代币文件损坏');
        }
        if (!is_file(mintedTokenPath($hash))) {
            throw new RuntimeException('全局代币不存在');
        }
        $dst = walletTokenPath($toUser, $hash);
        if (is_file($dst)) {
            throw new RuntimeException('目标账户已持有该代币');
        }
        if (!rename($src, $dst)) {
            throw new RuntimeException('转账失败');
        }
        $moved[] = $hash;
    }

    return [
        'moved' => $moved,
        'from_count' => getTokenCount($fromUser),
        'to_count' => getTokenCount($toUser),
    ];
}

function requireUser(string $cred): string
{
    if ($cred === '') {
        throw new InvalidArgumentException('请先登录');
    }
    $name = findUsernameByCredential($cred);
    if ($name === null) {
        throw new InvalidArgumentException('凭证无效');
    }
    return $name;
}

// ---------- 路由 ----------
if (isset($_POST['api'])) {
    $api = (string)$_POST['api'];
    $cred = isset($_POST['credential']) ? (string)$_POST['credential'] : '';

    try {
        switch ($api) {
            case 'create_account': {
                ensureDataDir();
                $username = randomRadix64Id();
                $password = randomRadix64Id();
                $users = loadUsers();
                while (isset($users[$username])) {
                    $username = randomRadix64Id();
                }
                $users[$username] = ['credential' => $password];
                saveUsers($users);
                ensureWalletDir($username);
                ?>账户已创建。<br>
                用户名:<strong><?= q($username) ?></strong><br>
                密码:<strong><?= q($password) ?></strong><br>
                请妥善保存,丢失将无法登录。<br>
                <a href="index.php?credential=<?= q(urlencode($password)) ?>">使用此密码直接进入</a> |
                <a href="index.php">返回</a><?php
                exit;
            }
            case 'login': {
                if ($cred === '') {
                    header('Location: index.php?err=empty');
                    exit;
                }
                $name = findUsernameByCredential($cred);
                if ($name === null) {
                    header('Location: index.php?err=invalid');
                    exit;
                }
                header('Location: index.php?credential=' . urlencode($cred));
                exit;
            }
            case 'get_status': {
                $name = requireUser($cred);
                jsonResponse([
                    'ok' => true,
                    'username' => $name,
                    'token_count' => getTokenCount($name),
                    'latest_hash' => getLatestHash(),
                ]);
            }
            case 'submit_token': {
                $name = requireUser($cred);
                $input = isset($_POST['input']) ? (string)$_POST['input'] : '';
                if ($input === '') {
                    jsonResponse(['error' => '缺少输入'], 400);
                }
                $result = addToken($name, $input);
                jsonResponse(['ok' => true] + $result);
            }
            case 'transfer': {
                $name = requireUser($cred);
                $target = isset($_POST['target_username']) ? trim((string)$_POST['target_username']) : '';
                $count = isset($_POST['count']) ? (int)$_POST['count'] : 0;
                if ($target === '') {
                    jsonResponse(['error' => '请输入目标用户名'], 400);
                }
                $result = transferTokens($name, $target, $count);
                jsonResponse(['ok' => true] + $result);
            }
            default:
                jsonResponse(['error' => '未知 api'], 400);
        }
    } catch (InvalidArgumentException $e) {
        if (!empty($_POST['ajax']) || in_array($api, ['get_status', 'submit_token', 'transfer'], true)) {
            jsonResponse(['error' => $e->getMessage()], 400);
        }
        ?><?= q($e->getMessage()) ?> <a href="index.php<?= $cred !== '' ? '?credential=' . urlencode($cred) : '' ?>">返回</a><?php
        exit;
    } catch (RuntimeException $e) {
        jsonResponse(['error' => $e->getMessage()], 500);
    }
}

// ---------- 页面 ----------
$cred = currentCredential();
$name = $cred !== '' ? findUsernameByCredential($cred) : null;
$loggedIn = $name !== null;
?>
<h1>代币系统</h1>

<?php if (!$loggedIn): ?>
    <?php if (isset($_GET['err'])): ?>
        <?php if ($_GET['err'] === 'invalid'): ?>密码无效。<br><?php endif ?>
        <?php if ($_GET['err'] === 'empty'): ?>请输入密码。<br><?php endif ?>
    <?php endif ?>

    <form method="post" action="index.php">
        <input type="hidden" name="api" value="create_account">
        <button type="submit">获取一个账户</button>
    </form>
    <br>
    <form method="post" action="index.php">
        <input type="hidden" name="api" value="login">
        密码:<input type="text" name="credential" required>
        <button type="submit">登录</button>
    </form>
    基于POW的代币系统。<br/>
    当前用户:请先登录
    用户名(转账用):<strong>请先登录</strong>
    代币数量:<span id="token-count">请先登录</span><br>
    全局链头哈希(挖矿前缀,所有用户共用):<br>
    <span id="latest-hash" style="word-break:break-all"><?=file_get_contents("data/head")?></span><br><br>

    <hr>
    <h2>挖矿</h2>
    规则:构造字符串,前缀为当前「全局链头哈希」,后面追加任意内容,使 SHA256 的十六进制结果以 6 个 0 开头。任何人挖到新代币都会推进同一条链。<br>
    Worker 数量:<input type="number" id="worker-count" value="1" min="1" max="32" size="3" disabled>
    <button type="button" id="btn-start-mining" disabled>开始计算</button>
    <button type="button" id="btn-stop-mining" disabled>停止计算</button><br>
    状态:<span id="mining-status">未开始</span><br>
    已尝试次数:<span id="mining-tries">0</span><br><br>

    <hr>
    <h2>转账</h2>
    向目标账户发送代币(服务器选取最老的代币,不保留转账记录):<br>
    目标用户名:<input type="text" id="transfer-target" size="20" disabled>
    数量:<input type="number" id="transfer-count" value="1" min="1" size="6" disabled>
    <button type="button" id="btn-transfer" disabled>发送</button>
<?php else: ?>
    基于POW的代币系统。<br/>
    当前用户:<?= q($name) ?>
    用户名(转账用):<strong><?= q($name) ?></strong>
    <a href="index.php">退出登录</a><br><br>
    代币数量:<span id="token-count">加载中…</span><br>
    全局链头哈希(挖矿前缀,所有用户共用):<br>
    <span id="latest-hash" style="word-break:break-all">加载中…</span><br><br>

    <hr>
    <h2>挖矿</h2>
    规则:构造字符串,前缀为当前「全局链头哈希」,后面追加任意内容,使 SHA256 的十六进制结果以 6 个 0 开头。任何人挖到新代币都会推进同一条链。<br>
    Worker 数量:<input type="number" id="worker-count" value="1" min="1" max="32" size="3">
    <button type="button" id="btn-start-mining">开始计算</button>
    <button type="button" id="btn-stop-mining" disabled>停止计算</button><br>
    状态:<span id="mining-status">未开始</span><br>
    已尝试次数:<span id="mining-tries">0</span><br><br>

    <hr>
    <h2>转账</h2>
    向目标账户发送代币(服务器选取最老的代币,不保留转账记录):<br>
    目标用户名:<input type="text" id="transfer-target" size="20">
    数量:<input type="number" id="transfer-count" value="1" min="1" size="6">
    <button type="button" id="btn-transfer">发送</button>
    <span id="transfer-result"></span>

    <script>
    (function(){
        var cred = <?= json_encode($cred, JSON_UNESCAPED_UNICODE) ?>;
        var ZERO_HASH = '<?= $ZERO_HASH ?>';
        var mining = false;
        var workers = [];
        var currentPrevHash = ZERO_HASH.toLowerCase();
        var totalTries = 0;
        var foundPending = false;
        var statusReqSeq = 0;
        var headPollTimer = null;

        function esc(s){ var d=document.createElement('div'); d.textContent=s; return d.innerHTML; }

        function postApi(api, data, cb) {
            var fd = new FormData();
            fd.append('api', api);
            fd.append('credential', cred);
            fd.append('ajax', '1');
            for (var k in data) if (data.hasOwnProperty(k)) fd.append(k, data[k]);
            var x = new XMLHttpRequest();
            x.open('POST', 'index.php');
            x.onload = function(){
                try { cb(JSON.parse(x.responseText)); }
                catch(e) { cb({ error: '响应解析失败' }); }
            };
            x.onerror = function(){ cb({ error: '网络错误' }); };
            x.send(fd);
        }

        function setStatus(text) {
            var el = document.getElementById('mining-status');
            if (el) el.textContent = text;
        }

        function updateTries(n) {
            totalTries = n;
            var el = document.getElementById('mining-tries');
            if (el) el.textContent = String(n);
        }

        function sha256HexClient(str) {
            return crypto.subtle.digest('SHA-256', new TextEncoder().encode(str)).then(function(h){
                var a = new Uint8Array(h), s = '';
                for (var i = 0; i < a.length; i++) {
                    var x = a[i].toString(16);
                    if (x.length < 2) x = '0' + x;
                    s += x;
                }
                return s;
            });
        }

        function verifyMintClient(input, prevHash) {
            if (!input || input.indexOf(prevHash) !== 0) {
                return Promise.resolve({ ok: false, error: '前缀不匹配' });
            }
            return sha256HexClient(input).then(function(hash){
                if (hash.indexOf('000000') !== 0) {
                    return { ok: false, error: '哈希未满足难度' };
                }
                return { ok: true, hash: hash };
            });
        }

        var workerBlobUrl = null;

        function terminateWorkers() {
            for (var i = 0; i < workers.length; i++) {
                try { workers[i].postMessage({ stop: true }); } catch(e) {}
                workers[i].terminate();
            }
            workers = [];
        }

        function stopMining() {
            mining = false;
            foundPending = false;
            terminateWorkers();
            document.getElementById('btn-start-mining').disabled = false;
            document.getElementById('btn-stop-mining').disabled = true;
            setStatus('已停止');
        }

        function ensureHeadPollRunning() {
            if (!headPollTimer) startHeadPoll();
        }

        function getWorkerBlobUrl() {
            if (workerBlobUrl) return workerBlobUrl;
            var workerCode = [
                'var PREFIX="000000";',
                'var active=false;',
                'function sha256Hex(str){',
                '  return crypto.subtle.digest("SHA-256", new TextEncoder().encode(str)).then(function(h){',
                '    var a=new Uint8Array(h), s="";',
                '    for(var i=0;i<a.length;i++){ var x=a[i].toString(16); if(x.length<2) x="0"+x; s+=x; }',
                '    return s;',
                '  });',
                '}',
                'async function mine(prev, startNonce, batchSize){',
                '  var nonce=startNonce;',
                '  while(active){',
                '    for(var i=0;i<batchSize&&active;i++,nonce++){',
                '      var input=prev+nonce;',
                '      var hash=await sha256Hex(input);',
                '      if(hash.indexOf(PREFIX)===0){',
                '        active=false;',
                '        self.postMessage({found:true,input:input,hash:hash});',
                '        return;',
                '      }',
                '    }',
                '    if(active) self.postMessage({tries:batchSize});',
                '  }',
                '}',
                'self.onmessage=function(e){',
                '  if(e.data.stop){ active=false; return; }',
                '  active=true;',
                '  mine(e.data.prevHash, e.data.startNonce, e.data.batchSize||800);',
                '};'
            ].join('\n');
            workerBlobUrl = URL.createObjectURL(new Blob([workerCode], {type:'application/javascript'}));
            return workerBlobUrl;
        }

        function spawnWorkers() {
            var n = parseInt(document.getElementById('worker-count').value, 10);
            if (!n || n < 1) n = 1;
            if (n > 32) n = 32;
            var url = getWorkerBlobUrl();
            for (var w = 0; w < n; w++) {
                (function(workerId){
                    var worker = new Worker(url);
                    worker.onmessage = function(ev) {
                        var d = ev.data;
                        if (d.found && !foundPending) {
                            foundPending = true;
#1
发帖 2026-08-10 13:28:46 · 快照 2026-08-12 10:40:03

额,怎么被截断了…
github/QianCF/php-PoW
自取

#2
发帖 2026-08-10 13:29:05 · 快照 2026-08-12 10:40:03

太长了

#3
发帖 2026-08-10 13:31:11 · 快照 2026-08-12 10:40:03

代码截断可还行,完整版放GitHub也行吧。