楼主
不管怎样,适合学校学生使用,或者朋友间玩耍。
直接点击按钮就可以获得一个密码,密码直接用于登录。
可以使用自带的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;