验证

自己查,别信我们的话

本站的每个宇宙都是比特币区块哈希的纯函数,而这个函数就是开源的 BNBBANG 引擎。验证一个比特币宇宙比验证一个 BNB 宇宙多一步:NFT 在 BNB Smart Chain 上,区块却在比特币上,所以你要在比特币上确认区块,其余一切在 BSC 上确认。五个步骤,不需要账号,不经过服务端。

合约为什么说 verified = false

铸造时,MirrorUniverse 把提交的哈希与 BSC 上的 blockhash(blockNumber) 比对,把结果存进 verified 字段。BSC 看不到比特币,所以这个比对对每个比特币区块都会失败,每个比特币宇宙的 verified 都是 false。合约记录下来,不会 revert。对一个比特币宇宙来说,这个标志位无论哪种取值都说明不了什么;下面的第 2 步就是取而代之的检查,任何人都能做。

第 1 步:在 BSC 上读出 NFT

在地址 0x1661086ee19cc8ef938e98ad9058e74fa870aa78(BNB Smart Chain,chainId 56)的 MirrorUniverse 合约上调用 universeOf(id)。BscScan 的「Read Contract」页签可以用,任何 RPC 客户端也行。结构体按声明顺序返回:

字段对比特币宇宙的含义
blockHash比特币区块哈希,32 字节,即奇点
blockNumber比特币区块高度
mintedAt、minter在 BSC 上铸造的时间与铸造人
outcome合约十二种结局的索引,顺序与引擎相同
verified对比特币区块永远是 false(见上文)
rarity0 到 4 = S、A、B、C、D

cardOf(id) 返回服务端签过名的 cardHash,tokenOfHash(blockHash) 返回某个哈希对应的 token id(0 表示还没人铸)。记下 blockHash 和 blockNumber,下一步要用。

第 2 步:在比特币上确认区块

把这一对拿到比特币上。下面任一种都足够:

# 按哈希:页面会显示高度
https://mempool.space/block/<blockHash>

# 按高度:以纯文本返回哈希
curl https://mempool.space/api/block-height/<blockNumber>

那个高度的哈希必须等于 NFT 里存的 blockHash。相等,这枚 NFT 就是比特币宇宙,而且在你查看时这个哈希就在比特币主链上。不相等,这枚 NFT 就不是比特币宇宙;拿同一对去 BSC 上核,它会是一个 BNB 宇宙。任何浏览器或节点给出的答案都一样,例如 https://blockstream.info/api/block-height/<blockNumber>,或者在你自己的节点上 bitcoin-cli getblockhash <blockNumber>。别按前导零判断:早期比特币区块只有八个,一小部分 BNB 区块也以八个零开头。

本站自己也在 /api/btc/block/<height|hash> 提供同样的数据(带缓存),但这一步的意义就在于直接去问比特币。

第 3 步:从哈希重算常数

种子是区块哈希,不做任何变换。每个参数对「哈希 + 一个槽位字节」做一次 keccak256 抽一个槽位;档位来自槽位 255:

u_i  = uint256(keccak256(blockHash ‖ uint8(i))) mod 1e9
tier = uint256(keccak256(blockHash ‖ uint8(255))) mod 1000

这就是 BNBBANG 的推导,原封不动;它在 bnbbang-engine 引擎目录下的 bnbhash.js 里,导出 derive(hash)。下面这段代码只用 ethers 就能为比特币创世区块重算前几个槽位和档位,你可以不依赖引擎独立确认这个公式:

// npm i ethers   (v6)
const { keccak256, solidityPacked } = require('ethers');

// 比特币区块 #0,2009-01-03
const hash = '0x000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f';

const slot = (i, m) => BigInt(keccak256(solidityPacked(['bytes32', 'uint8'], [hash, i]))) % BigInt(m);

for (let i = 0; i < 4; i++) console.log('u_' + i, slot(i, 1_000_000_000));
console.log('tier', slot(255, 1000));

// 完整向量(按引擎的参数顺序),由引擎本身给出:
//   const { derive } = require('./engine/bnbhash.js');   // 在 bnbbang-engine 的检出目录里
//   const d = derive(hash);  console.log(d.uInt, d.tier);

这些值必须与 derive(hash) 打印的一致。注意,现代哈希的十九个前导零(或创世区块的十个)在这里毫无影响:对「哈希 + 一个字节」做 keccak256,无论输入长什么样,输出都是均匀的。

第 4 步:跑引擎并比对

git clone https://github.com/q3579338/bnbbang-engine
cd bnbbang-engine
node tools/recompute.js 0x000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f

recompute.js 只凭哈希重建 23 个参数、结局、等级和 cardHash。推导只用整数,所以在任何平台上结果都相同。零依赖,Node 18 或更高。把结局和等级与本站的卡片、以及第 1 步的 outcome 和 rarity 对比;它们必须一致,而卡片上的徽章(创世、减半、难度周期首块、名块)不得改变其中任何一项。

第 5 步:重算 cardHash

cardHash 是服务端签名、合约存进 cardOf(id) 的指纹。它的计算方式与 BNBBANG 完全一致,推导版本 3:

cardHash = keccak256(abi.encode(
  bytes32  blockHash,
  uint32[22] uInt,        // 第 3 步的 22 个参数槽位,引擎顺序
  uint32   uG,            // 槽位 200:引力耦合
  uint32   uF1, uF2,      // 槽位 201、202:两次参照系抽取
  uint8    outcome,
  uint8    rarity,
  uint32   derivationVersion = 3
))

第 3 到 5 步的一条命令版本:从 BSC 读出 NFT,凭它的 blockHash 重算一切,再把参数、结局、等级和 cardHash 逐字节比对:

node tools/verify-onchain.js <tokenId>

被拯救过的宇宙(通过烧 BANG 微调过常数)由区块哈希加上写进交易里的操作日志重算,不需要服务端的存档。verify-onchain.js 唯一不检查的,是这个哈希是不是比特币区块;那是第 2 步,它需要的是比特币,不是 BSC。

引擎仓库附带 292 项引擎自检和 118 项链上比对测试。node engine/test.js 在本地运行前一组。

签名证明了什么,没证明什么

铸造需要服务端签名:合约只暴露 bangSigned()。服务端自己从比特币获取所请求高度的哈希(不信任客户端发来的哈希),不满六个确认、或保留区块未到开闸时间一律拒签,然后重算参数并盖章。签名地址可通过链上的 signer() 读出,也列在下方;任何人都能核对一枚 NFT 的签名是否恢复为该地址。协议连同可运行的验证器和真实向量在 docs/signing-protocol.md。

签名不允许服务端凭空造宇宙:签名的参数必须等于 recompute.js 从哈希算出的结果,而这正是 verify-onchain.js 检查的东西。它也不允许服务端凭空造比特币区块:哈希和高度明文存在 NFT 里,第 2 步会抓出任何比特币不承认的组合。

BSC 主网上的合约

chainId 56,2026-08-22 部署,与 bnbbang.com 共用。无代理、无升级路径、无 selfdestruct、无 delegatecall、无暂停。BTCBANG 自己不部署任何东西。

实时检查:/api/health 报告服务端正在使用的链 id 和签名地址,必须与合约一致。/api/btc/tip 报告服务端当前看到的比特币高度与哈希,以及签名前要求的确认数。/api/market/status 显示市场索引落后链多少。

四件不是去信任的事