L1 反滥用共识内核:消息防刷底线 + 质押式匿名身份#69
Conversation
两个新的 memo 约定软分叉,均遵循 staking.ts/redpacket.ts 的既有模式(config 常量 + 纯函数解析 文件 + blockchain.ts 三处 redOpError/applyTx/applySelect)。 消息防刷底线: - minMessageBurnFor(memoLength) = MESSAGE_BURN + floor(memoLength / MESSAGE_BURN_PER_CHAR_UNIT), 整数除法同 minFeeFor 写法。校验落点是 redOpError(带 atHeight 参数)而非 verifyTransaction(无 height 参数,无法做激活高度门控)。只对 isRealMessage(真消息,非协议层 memo)生效,不影响 PET/FISH 等本就低烧币的协议操作。createMessage 默认 burn 同步改为按长度算,避免不知情客户端 激活后发长消息被拒。MIN_MESSAGE_BURN_ACTIVATION_HEIGHT=40000(占位,部署前须核对实时链高)。 质押式匿名身份(新文件 identity.ts): - IDCLAIM|<pseudonym> 转托管认领 / IDRELEASE|<claimTxid> 解锁,纯资金锁仓反女巫,无 SLASH、 无仲裁者。IDENTITY_LOCK_BLOCKS=240(中继质押锁定期 20 倍——没有罚没,锁仓时长本身就是全部代价)。 独立新原语,不挂在 names.ts 昵称系统上(昵称已实网免费上线,追加质押要求会打破既有承诺)。 语义:押金须持续持有才算拥有身份——IDRELEASE 后假名立刻可被别人重新认领,而非一次性入场费。 复用 names.ts 的 isValidName/RESERVED_NAMES 字符集规则。IDENTITY_ACTIVATION_HEIGHT=45000(占位)。 IDENTITY_ESCROW_ADDRESS=…4(红包…1/质押…2/铸币…3 之后下一个)。 交叉 wiring:isProtocolMemo 排除 IDCLAIM/IDRELEASE 前缀,防止被消息防刷误判。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
node.ts: claimIdentity/releaseIdentity/myIdentityClaims/resolveIdentity,照抄 stake/unstake 形状。
api.ts: GET /identity(自己的质押,免token) / GET /identity/resolve(公开查询) /
POST /identity/claim /POST /identity/release(Bearer 门控 + 激活高度前置校验)。
cli: 新增 identity 命令组(claim/release/list/resolve)。
web: 新增 IdentityPanel(轮询 + 认领表单 + 解锁按钮,锁定期内禁用),接入现有 poll loop。
消息防刷底线是纯共识层收紧,不需要新的下游端点。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t.ts 均照抄 staking-selftest.ts 的分层结构(配置向量→纯解析→forge-chain 廉价锻造到激活高度→ 状态机场景→computeState≡validateChain≡replaceChain 分叉安全→供应量守恒)。 message-burn-floor: 公式边界/单调/整数、isRealMessage 分类(含 IDCLAIM/IDRELEASE 交叉wiring 验证)、协议层低烧币操作不受影响、mempool 与选包两条路径独立一致拒绝(绕过 addTransaction 直塞 mempool 验证 selectMempoolTxs 也拦得住)。 identity: 配置向量(锁定期显著长于中继质押/托管地址不撞)、纯解析(复用 names.ts 字符集)、 状态机(押金不足/重复认领/锁定期内解锁/非质押人解锁/全额退回/二次解锁均被拒)、 **核心设计决定验证:解锁后假名立刻可被别人重新认领**。 本地全过;typecheck 6 包全绿;smoke/staking/onion/mint/antireplay 均无回归。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
smoke.ts 加密私信场景原挖 8 块(旧扁平 MESSAGE_BURN=5 够用),改成随长度递增的默认烧币值后 (136 码点密文默认烧 11+手续费1=12)本金不足,交易静默进不了 mempool、下一行访问 parseMessages(...)[0] 直接崩溃——这是我这次改动合理暴露的测试夹具问题,非共识 bug。 修复:挖 16 块给足余量,并给 addTransaction 补返回值检查(用 check() 报出清晰失败而非崩溃)。 ci.yml 新增 message-burn-floor-selftest.ts / identity-selftest.ts 两步(各锻造到 40000/45000 高度,本地测得均在 CI 15 分钟预算内)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9665dec1f9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (atHeight >= MIN_MESSAGE_BURN_ACTIVATION_HEIGHT && isRealMessage(tx)) { | ||
| const memoLen = [...tx.memo].length; // Unicode 码点,同 MAX_MEMO 口径 | ||
| const required = minMessageBurnFor(memoLen); | ||
| if ((tx.burn ?? 0) < required) return `消息销毁额过低(需 ≥ ${required},当前 ${tx.burn ?? 0})`; |
There was a problem hiding this comment.
Recompute default message burn at send time
At MIN_MESSAGE_BURN_ACTIVATION_HEIGHT, this check rejects long real messages unless burn >= minMessageBurnFor(memo length), but the node/CLI/web send paths still pass an explicit flat MESSAGE_BURN default (V0idNode.message defaults to MESSAGE_BURN, /info.messageBurn is MESSAGE_BURN, and the CLI msg --burn default is MESSAGE_BURN). In that context, users who do not manually calculate a higher burn for messages of 20+ code points will have otherwise valid messages rejected after activation, so the default should be derived from the final memo body (including encrypted text) before constructing the transaction.
Useful? React with 👍 / 👎.
|
|
||
| // ---- 质押身份(Phase 3B:纯资金锁仓反女巫,无罚没、无仲裁者,软分叉)---- | ||
| /** 身份托管地址:IDCLAIM 锁定的押金记到这里(不可花)。与红包 …1 / 质押 …2 / 铸币 …3 区分(…4)。 */ | ||
| export const IDENTITY_ESCROW_ADDRESS = '0x' + '0'.repeat(63) + '4'; |
There was a problem hiding this comment.
Add identity escrow to system addresses
This introduces a new protocol escrow address but leaves it out of SYSTEM_ADDRESSES, so code that relies on that set will treat the identity escrow as a normal user address. For example, the mint redeem guard only blocks destinations in SYSTEM_ADDRESSES, and the node's newcomer scan/provider validation also use that set, so after this change redemptions or UI flows can target/report the identity escrow even though it is a non-user protocol address like the other escrows.
Useful? React with 👍 / 👎.
| export function isRealMessage(tx: { amount: number; burn?: number; memo: string }): boolean { | ||
| return isMessageTx(tx) && !isProtocolMemo(tx.memo); |
There was a problem hiding this comment.
Validate protocol memos before exempting burns
After the burn-floor activation, any amount=0 transaction with burn>0 can avoid the length-based minimum just by starting its memo with a protocol prefix, even if it is not a valid protocol operation. For example, parseFish only treats the exact memo FISH| as a catch, but FISH| followed by hundreds of characters is still classified here as non-real-message and can be accepted with burn=1, which leaves a cheap chain-spam path around the new floor.
Useful? React with 👍 / 👎.
codex review P2 发现:isProtocolMemo 只用 startsWith 判断前缀,而 PET/FISH/LAND/ZONE/PLANT/
HARVEST/CROPX/MINE 这些游戏协议前缀的合法性完全由各自 parseXxx(chain) 在应用层重放判定,
共识层(blockchain.ts)从不校验——所以在消息防刷底线上线前,"前缀对、payload是垃圾"这种套壳
交易本就语义无效,但也无害(反正整体也不受约束)。上线后就不一样了:套壳交易能靠命中前缀逃过
新的消息烧币下限,用 burn=1 塞任意长度的垃圾内容,比老老实实发消息还便宜——这是本次改动新开的口子。
修复:isProtocolMemo 按两类前缀区别处理:
- STAKE/UNSTAKE/SLASH/RED/CLAIM/REFUND/IDCLAIM/IDRELEASE:共识层已原生校验合法性,不合法形态
根本进不了链,startsWith 不变。
- PET/FISH/LAND/ZONE/PLANT/HARVEST/CROPX/MINE:逐一核对 payload 格式 + 该操作要求的精确/达标
烧币额(复用各模块导出的现成常量/函数:PET_BREED_COST/PET_EVO_COST/PETFARM_COST/ZONE_COST/
SEED_COST/HARVEST_BURN/CROPS/ZONE_TYPES/parseMineMemo/mineDiscoveryBurn/mineMaterialBurn,
不在此重复定义一套规则,避免与游戏模块演进脱节)。孵化 PET|、铸渔获 FISH| 精确匹配(无 payload
可塞);预留未启用的 CROPX| 同样精确匹配(保守收紧);地价随链上状态浮动、mining 只有下限,
payload 收紧为纯数字/复用现成成本函数即可,无需重放全链状态。
isProtocolMemo 签名从 (memo: string) 改为 (tx: {memo, burn}) 以支持烧币值校验,同步改 feed.ts
调用点。message-burn-floor-selftest.ts 修正此前依据旧宽松行为写的测试用例(FISH|1 类不精确
payload 本就不是真实协议操作),并新增套壳攻击必须受消息门槛约束的核心回归测试。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
codex review P2 发现:identity 功能新增了 IDENTITY_ESCROW_ADDRESS 托管地址,但漏加进 SYSTEM_ADDRESSES 集合——铸币兑现的收款校验、节点新地址发现等处都依赖这个集合区分"系统/协议 地址"与真实用户地址,遗漏会导致铸币兑现能打给身份托管地址、或身份托管地址被误报成"新地址上链"。 SYSTEM_ADDRESSES 的定义顺带挪到文件末尾(在所有托管地址常量声明完毕之后):IDENTITY_ESCROW_ADDRESS 的声明本身在文件后段,若不挪动会引用尚未初始化的 const(暂时性死区)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
codex review P2 发现:上一轮我把 transaction.ts 的 createMessage 默认 burn 参数从 MESSAGE_BURN(扁平)改成 minMessageBurnFor(长度),但这个默认值在实践中从未被触发—— V0idNode.message() 自己的参数默认值仍是 burn=MESSAGE_BURN(扁平),CLI 的 --burn 选项默认 串死 String(MESSAGE_BURN),web 消息表单的 burn 输入框初始值也是 defaultBurn(同样扁平)。 这三处都会在调用 createMessage 之前就把 burn 解析成一个具体数字,createMessage 自己的默认 参数永远轮不到触发。消息防刷底线激活后,用户只要不手动算更高的 burn,发 20+ 码点的消息就会 被 mempool 拒收——这正是当初改默认值想避免的情况,改了个寂寞。 修复: - node.ts message():burn 参数改为可选(undefined),在函数体内、算出最终上链 memo(含加密 后的密文,而非明文)之后,用 burn ?? minMessageBurnFor(最终body长度) 计算实际值。 - api.ts /message 端点本就支持 burn=undefined 透传(无需改)。 - CLI msg 命令:--burn 去掉固定默认值,未传时保持 undefined 一路透传到 node.message()。 - web 消息表单:burn 输入框留空表示"按长度自动",不再预填一个扁平初始值;发送时留空则不显式 传 burn(undefined),交给节点按最终正文长度计算。 三处改动后计算逻辑唯一权威仍在 node.ts message(),CLI/web 只负责"要不要覆盖默认值",不重复 实现公式。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3ebcc54758
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| memo.startsWith(RED_PREFIX) || | ||
| memo.startsWith(CLAIM_PREFIX) || | ||
| memo.startsWith(REFUND_PREFIX) || | ||
| export function isProtocolMemo(tx: { memo: string; burn?: number }): boolean { |
There was a problem hiding this comment.
Pass full transaction context before exempting memos
After the parser tightening, this helper still only sees memo/burn, so it cannot enforce the context that actually makes a memo a protocol operation. Fresh evidence is that redOpError only validates create-style prefixes such as RED|/STAKE|/IDCLAIM| when the transaction is sent to the corresponding escrow address, and farm/pet/mine parsers also require self-burn; a post-activation amount=0,burn=1 transaction to a normal address with memo = 'RED|' + 'x'.repeat(500) or LAND| plus digits therefore falls through consensus as a normal tx while isRealMessage returns false, bypassing minMessageBurnFor. Please pass/check from, to, and amount here or leave non-context-valid protocol-shaped memos subject to the message floor.
Useful? React with 👍 / 👎.
| claimIdentity(pseudonym: string): { ok: boolean; tx?: Transaction; error?: string } { | ||
| const r = makeIdentityClaim(pseudonym); | ||
| if (!r.ok) return { ok: false, error: r.error }; | ||
| return this.submit(this.wallet, IDENTITY_ESCROW_ADDRESS, IDENTITY_STAKE_MIN, r.memo!, minFeeFor(IDENTITY_STAKE_MIN)); |
There was a problem hiding this comment.
Block pre-activation identity claims in node helper
When embedding code calls the exported V0idNode.claimIdentity() directly before the next block reaches IDENTITY_ACTIVATION_HEIGHT (rather than going through the HTTP route, which has its own guard), this submits an IDCLAIM transfer to the identity escrow. Consensus only blocks pre-activation IDRELEASE and only treats escrow transfers as claims once identity is active, so that transaction can be mined as an ordinary transfer to the system address with no identityClaims entry, permanently locking the user's 200-token deposit instead of creating a claim.
Useful? React with 👍 / 👎.
| const { balances, nonces, pools, stakes, mintReserve, identityClaims, identityByPseudonym } = this.computeState(); | ||
| // 红包/质押/铸币操作合法性:池存在/未抢完/未重复领/已过期、质押锁定期/度量者权限、铸币储备等(按 height+1 估算) | ||
| const redErr = redOpError(tx, pools, stakes, mintReserve, this.height + 1, this.tipDifficulty()); | ||
| const redErr = redOpError(tx, pools, stakes, mintReserve, identityClaims, identityByPseudonym, this.height + 1, this.tipDifficulty()); |
There was a problem hiding this comment.
Revalidate identity claims against pending state
This admission check only uses computeState() from confirmed blocks, so two pending IDCLAIM transactions for the same pseudonym can both enter the mempool. After one is mined, the losing claim remains in the sender's mempool at the next nonce but redOpError now rejects it, so selectMempoolTxs keeps skipping that nonce and all later transactions from the wallet are stuck until the mempool is manually cleared; apply pending identity claims during admission or purge invalid mempool entries after each block.
Useful? React with 👍 / 👎.
codex review 第二轮 P2:上一轮把 isProtocolMemo 从纯 startsWith 收紧为「payload 格式 + 精确/ 达标烧币值」,但还留了一个更深的口子——只看 memo/burn,不看 from/to/amount,而这些协议前缀 里相当一部分的合法性恰恰系于这三者: - STAKE|/RED|/IDCLAIM|(转托管创建类):consensus(redOpError)只在 `to` 精确等于对应托管地址 时才校验/接纳;发往任意别的地址,redOpError 的对应分支根本不触发,交易落到 NORMAL 兜底——此时 若 isProtocolMemo 仍判定为真,就是一笔「伪装成质押/红包/认领、实际只是发错地方的普通转账」逃过 消息门槛。补 `to === 对应托管地址` 校验。 - PET/FISH/PETBREED/PETEVO/PETFARM/LAND/ZONE/PLANT/HARVEST/MINE:各自 parseXxx(chain) 几乎全部 要求 `from === to`(自转烧币,即 pets.ts/farm.ts/mining.ts 里的 selfBurn 判断),纯看 payload 格式不够——`from!==to` 的非自转套壳同样能蹭到排除。补 `from === to` 校验。 - PETX|(送崽转移):真实语义是转移给别人 + 转 1 币,例外地要求 `to !== from` 且 `amount > 0`, amount=0 的伪转移不豁免。 CLAIM/REFUND/UNSTAKE/SLASH/IDRELEASE 这几个 id 引用类操作 consensus 不看 to,维持纯 startsWith 不变(不合法形态本就进不了链)。isProtocolMemo/isRealMessage 签名补 from/to/amount 字段,三个 调用点(parseMessages/feed.ts/blockchain.ts redOpError)本就传完整 tx 对象,无需改传参。 message-burn-floor-selftest.ts 补齐新增的语境测试用例(自转/托管地址/转账语境)+ 三条新回归 (payload 精确但 from≠to 算真消息、STAKE 发错地址算真消息、PETX amount=0 算真消息)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
codex review 第二轮 P2:V0idNode.claimIdentity() 自己没有激活高度检查,只有 api.ts 的 HTTP 路由有这层守卫。激活高度前若有代码直接调用 claimIdentity()(而非走 HTTP,比如把节点当库嵌入、 未来的 CLI 直连模式),会把一笔转给身份托管地址的转账当普通交易广播出去——consensus 在激活高度 前把它当历史普通转账接受(钱真的转进托管地址了),但 identityClaims 不会有任何记录(因为 applyTx 的 IDCLAIM 分支同样被 atHeight>=IDENTITY_ACTIVATION_HEIGHT 门控),用户的 200 押金 从此有去无回——没有对应的认领记录可以 IDRELEASE。 补的检查和 api.ts 里那道完全一致,属于防御性重复:HTTP 客户端已经被 api.ts 挡住,这里是给 「不经过 HTTP、直接调用方法」的调用方补上同一道闸。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
codex review 第二轮 P2:addTransaction 的准入检查只看 computeState()(已确认链),不看 mempool 里排队的其他交易。两笔 IDCLAIM 抢同一个假名可以同时成功进 mempool(提交时基于同一份「假名未被 占用」的已确认状态,互不知情)。等其中一笔被挖进块后,另一笔在 mempool 里原地等待——但它引用的 假名已经被抢,之后每次 selectMempoolTxs 重新扫描都会因 redOpError 判负而被跳过,且不会被清出 mempool。红包/质押这类「落选只是暂时的、下一块还有机会」的操作不受影响,但 IDCLAIM 的排他性使 这次判负是永久的:expected nonce 永远卡在这笔失败交易上,同一钱包后续所有交易都会跟着卡死, 直到手动清 mempool。 addBlock(正常出块路径)此前只调用 dropMined(清掉已成功打包的交易),没有像 replaceChain (reorg 路径)那样调用 revalidateMempool(对每笔剩余 mempool 交易重新跑一遍 addTransaction, 自动丢弃因新状态而失效的)。补上这一步,复用现成机制而非发明新逻辑。forgeTo 直接 push 绕过 addBlock,不受影响;smoke 测试实测变慢(真实挖矿路径每块多一次 mempool 重新校验),换取正确性, 在教学/小算力网络的定位下是合理权衡。 identity-selftest.ts 补核心回归:两笔 pending IDCLAIM 抢同一假名 + 输家紧跟一笔待处理的 nonce+1 交易 → 验证输家两笔都被清出 mempool、且能用干净的 nonce 立刻重新提交(不再永久卡死)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5bc702ea3c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (memo.startsWith(LAND_PREFIX)) { | ||
| // 地价随链上状态浮动(下限需重放 soldTotal/velocity 才能算出),此处无法精确核验金额; | ||
| // 但 payload 收紧为纯数字 + 自转要求已杜绝夹带任意字符串,真实地价下限仍由 parseFarm/共识层的经济防线把关。 | ||
| return selfTransfer && /^\d+$/.test(memo.slice(LAND_PREFIX.length)); |
There was a problem hiding this comment.
Keep underpriced LAND memos subject to the burn floor
Fresh evidence is that this revision still exempts LAND| on self-transfer plus numeric payload alone, while the dynamic land-price check only happens later in parseFarm and does not reject the transaction from consensus. After MIN_MESSAGE_BURN_ACTIVATION_HEIGHT, a self-transfer message like memo = 'LAND|' + '1'.repeat(500), amount=0, burn=1 is accepted as a normal burn transaction and hidden from the message floor even though it is not a valid land purchase, so the new long-message anti-spam cost can still be bypassed with arbitrary-length numeric LAND payloads.
Useful? React with 👍 / 👎.
| // ② 转托管创建类:只有真的发往对应托管地址,consensus 才会校验/接纳,其余地址等于普通转账 | ||
| if (memo.startsWith(STAKE_PREFIX)) return to === STAKE_ESCROW_ADDRESS; | ||
| if (memo.startsWith(RED_PREFIX)) return to === RED_ESCROW_ADDRESS; | ||
| if (memo.startsWith(IDCLAIM_PREFIX)) return to === IDENTITY_ESCROW_ADDRESS; |
There was a problem hiding this comment.
Do not exempt IDCLAIM before identity activation
Fresh evidence is the 5,000-block gap between MIN_MESSAGE_BURN_ACTIVATION_HEIGHT (40000) and IDENTITY_ACTIVATION_HEIGHT (45000): during that window redOpError does not validate IDCLAIM escrow transactions, but this line still classifies any IDCLAIM|... sent to the identity escrow as protocol. An amount=0,burn=1 transaction with a long IDCLAIM| memo to the escrow is therefore accepted as a normal burn transaction without paying minMessageBurnFor, so the message burn floor is bypassable until identity activates.
Useful? React with 👍 / 👎.
| if (!v.ok) return { ok: false, error: v.error }; | ||
| this.chain.push(block); | ||
| this.dropMined(block); | ||
| this.revalidateMempool(); |
There was a problem hiding this comment.
Avoid full mempool revalidation after every block
When a normal block is appended while the node still has many pending transactions, this new call revalidates the entire remaining mempool by calling addTransaction for each old entry, and addTransaction recomputes confirmed state from the whole chain each time. With the existing 5,000 transaction mempool cap and 50 transaction block cap, a busy node can rescan tens of thousands of blocks thousands of times after every received or mined block, stalling block processing; prefer a targeted purge or a single shared pending-state pass.
Useful? React with 👍 / 👎.
codex review 第三轮 P2,两个新发现: ① LAND 数字 payload 没限长度:payload 收紧成「纯数字 + 自转」后,数字本身可以是任意长度 (比如 500 位数字)——consensus 对 LAND 前缀完全不管(真实地价上限判断在 parseFarm 应用层, 不是共识拒绝),所以「LAND|<500位数字>」依然会被 isProtocolMemo 判定为协议操作而豁免烧币 下限,是套壳夹带垃圾的新载体。修复:数字类 payload(LAND 的地块号、ZONE 的区块号、PLANT 的 格位号)统一加位数上限(≤9 位,远超任何真实场景可能达到的数量级,真正的业务范围校验仍在 parseFarm)。 ② STAKE_ACTIVATION_HEIGHT/IDENTITY_ACTIVATION_HEIGHT 没被 isProtocolMemo 检查:上一轮补了 「to === 对应托管地址」,但漏了「这个前缀当前是否已经激活」这个维度。IDENTITY_ACTIVATION_HEIGHT (45000) 晚于 MIN_MESSAGE_BURN_ACTIVATION_HEIGHT(40000),中间隔了 5000 块窗口——这段窗口内 consensus 还不认识 IDCLAIM(applyTx 的身份分支被 atHeight>=IDENTITY_ACTIVATION_HEIGHT 门控), 一笔发往身份托管地址的 IDCLAIM 会被当作普通转账接受,但 isProtocolMemo 只看「to 对不对」不看 「这会儿身份系统上线了没」,误判为协议操作而豁免。STAKE 当前配置下不会触发(质押激活高度 16000 早于消息门槛 40000),但同样补上高度检查,防止未来调整激活高度数值时同样的漏洞复发。 isProtocolMemo/isRealMessage 签名加 atHeight 字段(三个调用点 parseMessages/feed.ts/ blockchain.ts redOpError 都补上对应的区块高度)。message-burn-floor-selftest.ts 新增两组 核心回归:LAND 超长数字套壳、IDCLAIM 在窗口期内不豁免(+ STAKE 高度检查的同款用例)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
codex review 第三轮 P2:上一轮修复身份认领竞态时,给 addBlock(正常出块路径)加了 revalidateMempool 调用,但那个实现是对每笔 mempool 交易各自调用一次 addTransaction,而 addTransaction 内部每次都要重新 computeState()(重放整条链)——mempool 上限 5000、每块只打包 50 笔,如果一个节点忙(mempool 积压到接近上限、链已经很长),每次出块/收块后的这轮清理就是 O(mempool × chainLength),链一长就会让每次出块都变得很慢,构成真实的性能隐患。 修复:把 addTransaction 里“依赖链上状态”的那部分(redOpError + nonce + 余额校验)抽成 admitAgainstState(tx, state),接收外部传入的 state 而不是内部重新计算。revalidateMempool 现在只调用一次 computeState(),把这份快照传给 admitAgainstState 逐笔复用,复杂度降到 O(chainLength + mempool)。mempool 内“同地址 pending 数/花费”的统计仍按 this.mempool 当前 内容动态算(循环中逐步重建),语义和之前完全一致,不影响已修的竞态清理效果——identity-selftest 的竞态回归测试重跑确认无回归。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
v0idchain/packages/core/src/config.ts
Line 62 in 84ebb92
Raising MAX_MEMO here changes verifyTransaction at every height, so as soon as an upgraded miner is running it can include a 129–512 codepoint memo before any of the new activation heights. Nodes still on the previous 128-codepoint rule will reject that block, so a single long memo during rollout can split upgraded and non-upgraded nodes; keep the old limit until an activation height or otherwise height-gate the larger memo rule.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| to: string; | ||
| atHeight: number; | ||
| }): boolean { | ||
| return isMessageTx(tx) && !isProtocolMemo(tx); |
There was a problem hiding this comment.
Apply the burn floor to memo-bearing self-transfers
Because the burn-floor path only treats isMessageTx (amount === 0 && burn > 0) as a real message, after MIN_MESSAGE_BURN_ACTIVATION_HEIGHT a sender can put an arbitrary 512-codepoint memo on a to === from, amount = 1, burn = 0 transfer and pay only the minimum fee, since the transferred coin is credited back by the normal transaction path. That bypasses the new length-based anti-spam cost for long on-chain text; apply the length floor to zero-net/self memo transfers or to memo-bearing transactions more generally.
Useful? React with 👍 / 👎.
| if (tx.to === IDENTITY_ESCROW_ADDRESS) { | ||
| const meta = parseIdentityClaim(m); | ||
| if (meta && tx.amount >= IDENTITY_STAKE_MIN && !pseudonymToClaimId.has(meta.pseudonym)) { |
There was a problem hiding this comment.
Ignore coinbase transactions when replaying identities
If a miner sets the coinbase recipient to IDENTITY_ESCROW_ADDRESS and includes enough fees for the coinbase amount to reach IDENTITY_STAKE_MIN, an IDCLAIM|name coinbase memo is recorded by this read-side replayer even though consensus computeState skips coinbase transactions before applyTx and never creates the claim. In that scenario /identity/resolve can show the pseudonym owned by NULL_ADDRESS and suppress a later real claim in UI/API views while consensus still accepts the real claim; skip coinbase/from-null transactions here to keep the identity view aligned with consensus.
Useful? React with 👍 / 👎.
| - name: Message burn floor selftest | ||
| run: pnpm exec tsx scripts/message-burn-floor-selftest.ts | ||
|
|
||
| - name: Identity staking selftest | ||
| run: pnpm exec tsx scripts/identity-selftest.ts |
There was a problem hiding this comment.
Keep the new selftests within the CI timeout
Both added selftests forge to very high activation heights (40000 and 45000) by repeatedly calling the real PoW mineBlock; I checked the workflow context and a local 70s run of the same forge helper only reached height 5000, so these two new steps alone are likely to exceed the check job's 15-minute timeout and make PR CI fail before tests complete. Use a cheaper deterministic block builder for these activation-height tests or lower the heights under test.
Useful? React with 👍 / 👎.
| if (!v.ok) return { ok: false, error: v.error }; | ||
| this.chain.push(block); | ||
| this.dropMined(block); | ||
| this.revalidateMempool(); |
There was a problem hiding this comment.
Let dropped mempool transactions be accepted again
When this normal-block path drops transactions via revalidateMempool, the node's seenTx cache is not updated; V0idNode.acceptTx returns ok immediately for any seen txid. In an IDCLAIM race this can drop the losing nonce-0 transaction and that sender's nonce-1 follow-up; after the sender confirms a replacement nonce-0, rebroadcasting the same signed nonce-1 tx is silently ignored by this node (ok but not pending/confirmed), so external wallets can lose a valid transaction until process restart.
Useful? React with 👍 / 👎.
codex review 第四轮 P2:消息防刷底线只对 isMessageTx(amount=0+burn>0)生效,攻击者可以把 消息伪装成「自己转给自己 1 个币 + 附带 512 码点内容,burn=0」——因为 amount>0 不满足 isMessageTx 的形态,完全不受 minMessageBurnFor 约束,只需付最低手续费就能在链上塞任意长度 文本,等于绕开了整条消息防刷底线(经济实质上就是一条消息:钱转回自己手里,没有真实价值转移)。 修复:新增 isMemoSpamCandidate(tx),在原有 isMessageTx 基础上**追加**一种新形态——自转 (from===to)+ memo 非空——不再要求 amount===0。⚠️ 过程中犯过一次严重错误:最初把这个新函数 写成完全替代 isMessageTx(只保留自转分支),导致最常见的「张三发消息给李四」(amount=0, from!==to)反而被排除在候选之外、完全绕开门槛——比要修的漏洞更严重,已在提交前用回归测试 自查发现并改成 isMessageTx(tx) || (自转+memo非空) 的正确形式(or 而非 replace)。 范围扩大后,两个此前靠「burn=0 被 isMessageTx 天然排除」蒙混过关的既有自转协议(NAME 昵称 抢注、MKT 集市上架,market.ts 里 `上架 = 自转 1 币`)不能再蒙混,必须显式加入 isProtocolMemo 排除名单(核对 payload 格式复用 names.ts/market.ts 现成规则 + burn===0)。RELAY 中继发布 同理(relays.ts 那句「不进 isProtocolMemo」的旧注释已过时,一并更新+导出 parseRelayMemo 供 messages.ts 复用格式校验,避免重复定义一套规则)。 message-burn-floor-selftest.ts 新增:isMemoSpamCandidate 单测(含「转给别人不算候选」的对照 组)、NAME/MKT/RELAY 激活后仍不受影响、真实 addTransaction 端到端验证(自转夹带被拒绝 + 补足 销毁额后被接受,付出与真消息一样的成本后套利空间消失)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
codex review 第四轮 P2:上一轮给 addBlock 加了 revalidateMempool(清理身份认领竞态里的输家 交易),但节点的 seenTx 去重缓存(防止 P2P 广播风暴重复处理同一笔交易)没有跟着同步。一笔交易 首次被节点看到时会记进 seenTx;之后 acceptTx/onTx 只要发现 txid 在 seenTx 里就直接短路返回 (HTTP 提交路径甚至直接答复 ok),不会重新走 addTransaction。IDCLAIM 竞态里的输家交易被 revalidateMempool 清出 mempool 后,它的 txid 依然留在 seenTx 里——如果外部钱包重新广播同一笔 已签名交易(比如网络重试逻辑),节点会静默吞掉它:不会重新进 mempool、永远不会 confirm,但 HTTP 提交却回一个 "ok",用户会一直卡在"看起来提交成功但没反应",直到节点进程重启(seenTx 清空)。 修复:Blockchain 新增 onMempoolDropped 可选回调,revalidateMempool 剔除交易时同步通知。用 回调而非返回值/共享字段:revalidateMempool 可能在挖矿(PoW 异步计算期间)与 P2P 收块两条 路径交错触发,共享字段会被后触发的一次覆盖、丢失前一次的清理信息;回调在 revalidateMempool 内部同步调用没有这个竞态。V0idNode 构造时挂上回调,把清理到的 txid 从 seenTx 里删掉。 identity-selftest.ts 的竞态回归测试补充验证:回调确实上报了输家 claimB 和 followUp 的 txid。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
codex review 第四轮 P2:createCoinbase 硬编码 memo='',但 verifyTransaction 对 coinbase 的校验只看 fee===0/burn===0/amount>0,完全不检查 memo——恶意矿工可以绕过这个官方辅助函数, 自己手写一笔 from=NULL_ADDRESS、to=IDENTITY_ESCROW_ADDRESS、memo=IDCLAIM|<name> 的出块 奖励,只要金额(BLOCK_REWARD+当块手续费总额)凑够 IDENTITY_STAKE_MIN 即可。真正的共识 computeState 处理 coinbase 时只做余额加法、直接 continue、从不进 applyTx,永远不会创建 identityClaims 记录;但 computeIdentityState(只读展示层,供 /identity/resolve 等用)遍历 交易时没有排除 coinbase,会把这笔交易误记成合法认领,导致 UI/API 显示该假名"已被占用",即便 链上共识状态里根本没有此记录——真实用户之后仍能成功认领,只是中间会有一段误导性的展示。 修复:computeIdentityState 遍历时用 isCoinbase(tx) 跳过 coinbase 交易(复用 transaction.ts 现成函数,不重复判断 tx.from===NULL_ADDRESS)。 identity-selftest.ts 新增回归:构造一个含恶意 coinbase 的伪造区块喂给 computeIdentityState, 验证该假名不会被记成已占用、resolveIdentityOwner 返回 undefined(与共识状态一致)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1ff7fe5dbf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| memo.startsWith(UNSTAKE_PREFIX) || | ||
| memo.startsWith(SLASH_PREFIX) || | ||
| memo.startsWith(FISH_PREFIX) || | ||
| memo.startsWith(LAND_PREFIX) || | ||
| memo.startsWith(ZONE_PREFIX) || | ||
| memo.startsWith(PLANT_PREFIX) || | ||
| memo.startsWith(HARVEST_PREFIX) || | ||
| memo.startsWith(CROPX_PREFIX) || | ||
| memo.startsWith(MINE_PREFIX) | ||
| ); | ||
| memo.startsWith(CLAIM_PREFIX) || | ||
| memo.startsWith(REFUND_PREFIX) || | ||
| memo.startsWith(IDRELEASE_PREFIX) | ||
| ) { |
There was a problem hiding this comment.
Gate IDRELEASE exemptions on identity activation
During the 40,000–44,999 window where the message burn floor is active but identity staking is not, an amount=1 self-transfer with a long IDRELEASE|... memo is accepted as an ordinary self-transfer because redOpError only blocks pre-activation IDRELEASE when amount === 0; this unconditional protocol exemption then makes isRealMessage false, so the sender can store a 512-codepoint memo while paying only the normal self-transfer fee instead of minMessageBurnFor. Please require identity activation and the real zero-amount release shape before exempting IDRELEASE memos.
Useful? React with 👍 / 👎.
| const name = memo.slice(NAME_PREFIX.length).trim().toLowerCase(); // 归一化口径同 parseNames | ||
| return isValidName(name); | ||
| } | ||
| if (memo.startsWith(MKT_PREFIX)) { |
There was a problem hiding this comment.
Exempt market delists from the burn floor
After MIN_MESSAGE_BURN_ACTIVATION_HEIGHT, V0idNode.marketDelist() still submits a self-transfer with memo = DEL|<listingId> and burn = 0; because isMemoSpamCandidate now treats self-transfers with memos as message candidates and this whitelist only handles MKT| listings, delist transactions fall through as real messages and are rejected for not paying minMessageBurnFor. Please include the valid DEL| delist shape in the protocol exemptions so sellers can remove listings after activation.
Useful? React with 👍 / 👎.
codex review 第五轮 P2,两个问题: ① DEL(集市撤单)漏加豁免:V0idNode.marketDelist() 固定构造「自转 1 币 + memo=DEL|<listingId> + burn=0」,和 MKT 上架同类形态。上一轮只加了 MKT_PREFIX 的豁免,漏了同一个 market.ts 模块 里的 DEL_PREFIX——isMemoSpamCandidate 扩大范围后,激活后所有卖家撤单都会被误判成真消息、 因为 burn=0 达不到 minMessageBurnFor 而被拒绝,等于把撤单功能完全打断。 ② IDRELEASE 的豁免条件遗漏了 amount 依赖:这次是我自己设计里的一个特有缺口——IDRELEASE 的 「激活前门控」当初特意加了 `&& tx.amount === 0` 条件(只拦 amount=0 的新边界,不 retroactive 拒绝激活前 amount>0 的历史普通转账,这个设计本身对资金安全没问题)。但这个条件让 IDRELEASE 和 UNSTAKE/SLASH(未激活时无条件拒绝,不看 amount)不再是同一类——「未激活 + amount≠0」 这个组合会漏过 redOpError 所有 IDRELEASE 专属分支、落到 NORMAL 当普通转账接受。上一轮把 IDRELEASE 和 UNSTAKE/SLASH/CLAIM/REFUND 一起归进「无条件 startsWith 足够」桶,没意识到这个 前提在 IDRELEASE 身上不成立,导致「自转 1 币 + 超长 IDRELEASE| 内容 + burn=0」在 MIN_MESSAGE_BURN_ACTIVATION_HEIGHT(40000) 到 IDENTITY_ACTIVATION_HEIGHT(45000) 这段窗口 期内可以免费夹带任意长度内容。 修复:DEL_PREFIX 加入自转+burn=0+payload=64hex 的豁免(同 MKT 的判断标准);IDRELEASE 从「id 引用类无条件」桶里单独拎出来,要求 `atHeight >= IDENTITY_ACTIVATION_HEIGHT && amount === 0` 才豁免——真正会被 consensus 当解锁处理的唯一形态。 message-burn-floor-selftest.ts 新增回归:DEL 撤单激活后不受影响(含端到端真实 addTransaction 验证)+ IDRELEASE 四象限测试(已激活×amount=0/≠0、未激活×amount=0/≠0)+ 端到端验证「未激活+ amount≠0」套壳被真实拒绝。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5760699287
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| * 避免升级节点把激活前已广播/挂在旧节点 mempool 里的低销毁消息 retroactive 判非法。 | ||
| * ⚠️ 占位值 40000 —— **合并/部署前必须确认它 ≥ 当前实时链高 + 升级窗口**(同 STAKING_ACTIVATION_HEIGHT=16000 的选法)。 | ||
| */ | ||
| export const MIN_MESSAGE_BURN_ACTIVATION_HEIGHT = 40_000; |
There was a problem hiding this comment.
Set burn-floor activation after the live tip
When a node runs this against a chain that has already reached #40000, redOpError starts applying the new message burn floor to historical blocks at and after this height. Any previously valid low-burn long message or self-transfer memo in that range will make validateChain fail during loadChain/replaceChain, causing upgraded nodes to reject their existing chain. Please replace the placeholder with a height safely beyond the current live tip plus an upgrade window before landing/deploying.
Useful? React with 👍 / 👎.
| * ⚠️ 占位值 45000 —— **合并/部署前必须确认它 ≥ 当前实时链高 + 升级窗口**(同 STAKING_ACTIVATION_HEIGHT=16000 的选法), | ||
| * 且与 MIN_MESSAGE_BURN_ACTIVATION_HEIGHT(40000)/STAKING_ACTIVATION_HEIGHT(16000)/MINT_ACTIVATION_HEIGHT(30000) 互不相同。 | ||
| */ | ||
| export const IDENTITY_ACTIVATION_HEIGHT = 45_000; |
There was a problem hiding this comment.
Set identity activation after the live tip
If the live chain has already passed #45000, upgraded nodes will retroactively treat historical transactions at and after that height as identity operations: transfers to IDENTITY_ESCROW_ADDRESS must now be valid IDCLAIMs, and any IDRELEASE| memo must satisfy the new release rules. Those were ordinary transfers before this commit, so an existing transaction to 0x…4 or with an IDRELEASE| memo can make validateChain fail during loadChain/replaceChain or be reinterpreted as a claim. This activation height needs to be moved beyond the current live tip plus an upgrade window before deployment.
Useful? React with 👍 / 👎.
| export * from './relays.js'; | ||
| export * from './redpacket.js'; | ||
| export * from './staking.js'; | ||
| export * from './identity.js'; |
There was a problem hiding this comment.
Mirror identity exports in the browser entrypoint
This exposes the new identity helpers only through the Node-oriented @v0idchain/core entrypoint; core/browser is documented as the browser-safe equivalent minus storage, but it still lacks identity.ts. Browser/self-custody clients that need to build or inspect IDCLAIM/IDRELEASE transactions cannot import makeIdentityClaim or computeIdentityState from @v0idchain/core/browser, and importing the main entrypoint risks pulling in the storage export with node:fs dependencies. Please add the same identity export to browser.ts.
Useful? React with 👍 / 👎.
identity.ts 全链路(config/transaction/block/names)零 node-only 依赖,和 browser.ts 已导出的模块完全同源,浏览器/自托管钱包客户端此前无法从 @v0idchain/core/browser 构造或解析 IDCLAIM/IDRELEASE 交易。 (staking.ts/mint.ts 同样缺失于 browser.ts,但那是 2026-06-25 就存在的老问题, 与本 PR 无关,不在此修——只补这个 PR 新加的 identity.ts。) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Round 6 review verdict#1 P1「Set burn-floor activation after the live tip」 — 不是代码 bug,是已知、已文档化的占位符。 #2 P1「Set identity activation after the live tip」 — 同上, #3 P2「Mirror identity exports in the browser entrypoint」 — 真 bug,已修( (顺手发现 全量回归通过:typecheck(6 包全绿)、smoke、两个新 selftest、staking/onion/mint/antireplay 四个既有 selftest,CI 全绿。 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f00f5d6a43
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| memo.startsWith(STAKE_PREFIX) || | ||
| export function isMemoSpamCandidate(tx: { amount: number; burn?: number; from: string; to: string; memo: string }): boolean { | ||
| if (isMessageTx(tx)) return true; | ||
| return tx.from === tx.to && tx.memo.length > 0; |
There was a problem hiding this comment.
Exempt room publishes from the message floor
After MIN_MESSAGE_BURN_ACTIVATION_HEIGHT, this new self-transfer candidate path also catches the game room publish transaction from packages/game-web/src/room.ts:36 (createTransaction(wallet, wallet.address, 1, ..., ROOM|<hash>) with burn=0). Because ROOM| is not included in isProtocolMemo, /api/tx will reject room publishes as under-burned messages even though they are the canonical room-version protocol; either whitelist the valid ROOM|<hash> shape or have the publisher attach the required burn.
Useful? React with 👍 / 👎.
game-web publishRoom() 固定自转 1 币 + burn=0 + ROOM|<64hex布局hash>, ROOM_PREFIX 此前只在 game-web/src/room.ts 本地定义,core 的 isProtocolMemo 无从得知这个前缀,导致激活消息防刷底线后所有房间布局发布都会被当成 "未达标真消息"拒绝——回归。 core 不能反向依赖 game-web,故把 ROOM_PREFIX 常量迁到 config.ts(唯一 真实定义处),room.ts 改从 @v0idchain/core/browser 导入,避免两处漂移。 isProtocolMemo 按既有⑤类模式(自转+burn=0+payload格式)加检查,复用 HEX64。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Round 7 review verdict「Exempt room publishes from the message floor」 — 真 bug,已修( 修法: 新增 3 条回归(合法发布不受影响 / burn>0 套壳仍被拒 / payload 非 64-hex 套壳仍被拒)+ 全量回归(typecheck 6 包、smoke、两个新 selftest、staking/onion/mint/antireplay 四个既有 selftest)全绿,CI 已跑通。 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 80a713b0d2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // ⑤ 自转、burn 恒为 0 的纯展示层约定:昵称抢注 / 集市上架 / 集市撤单 / 中继发布 | ||
| if (memo.startsWith(NAME_PREFIX)) { | ||
| if (!selfTransfer || burn !== 0) return false; | ||
| const name = memo.slice(NAME_PREFIX.length).trim().toLowerCase(); // 归一化口径同 parseNames |
There was a problem hiding this comment.
Require canonical NAME payloads before exempting
At and after MIN_MESSAGE_BURN_ACTIVATION_HEIGHT, a self-transfer with amount=1,burn=0 and a memo like NAME|${' '.repeat(506)}x reaches 512 code points but this branch trims the payload to x and exempts it from minMessageBurnFor; parseNames does the same trim, so the transaction is accepted as a name claim while storing a max-length memo for only the normal self-transfer fee. Please only exempt canonical NAME|<name> payloads, or otherwise reject/bound raw padding before treating it as protocol.
Useful? React with 👍 / 👎.
| const price = Number(rest.slice(0, sep)); | ||
| const title = rest.slice(sep + 1); | ||
| return Number.isInteger(price) && price > 0 && title.length > 0 && [...title].length <= MAX_TITLE; |
There was a problem hiding this comment.
Require canonical market prices before exempting
For post-activation self-transfers, the raw market price token can be padded to carry almost the whole memo, e.g. MKT|${'0'.repeat(505)}1|x is 512 code points but Number(...) is still the integer 1, so this returns protocol and skips the burn floor; parseMarket also accepts the same memo as a listing. Please require a canonical bounded decimal price token before exempting market listings, so padded market memos remain subject to minMessageBurnFor.
Useful? React with 👍 / 👎.
| const m = parseMineMemo(memo); | ||
| if (!m) return false; | ||
| const needed = m.type === 'discovery' ? mineDiscoveryBurn(m.depth, m.kind) : mineMaterialBurn(m.kind, m.count); | ||
| return burn >= needed; |
There was a problem hiding this comment.
Bound MINE numeric fields before exempting
After the burn floor activates, a self-burn memo such as MINE|MAT|copper|${'0'.repeat(495)}1 is exactly 512 code points, but parseMineMemo normalizes the padded count to 1, so this branch exempts it after paying only the 4-coin copper material burn instead of the 30-coin message floor. Since parseMines uses the same parser and records the asset, please require the canonical bounded numeric form emitted by makeMineMaterial/makeMineDiscovery before treating MINE memos as protocol.
Useful? React with 👍 / 👎.
| if (memo.startsWith(RELAY_PREFIX)) { | ||
| return selfTransfer && burn === 0 && parseRelayMemo(memo) !== null; |
There was a problem hiding this comment.
Require canonical relay ports before exempting
For post-activation self-transfers, parseRelayMemo accepts a port token with hundreds of leading zeroes because it parses the field with Number(...); a memo like RELAY|<64hex>|a:${'0'.repeat(434)}1|m|0 reaches 512 code points but is still whitelisted as a burn-free relay publish. That lets arbitrary-sized relay-shaped memos bypass minMessageBurnFor, so the exemption should require the canonical port representation produced by buildRelayMemo or otherwise bound the raw port token length.
Useful? React with 👍 / 👎.
|
|
||
| // ② 转托管创建类:只有真的发往对应托管地址 + 已过激活高度,consensus 才会校验/接纳,否则等于普通转账 | ||
| if (memo.startsWith(STAKE_PREFIX)) return to === STAKE_ESCROW_ADDRESS && atHeight >= STAKING_ACTIVATION_HEIGHT; | ||
| if (memo.startsWith(RED_PREFIX)) return to === RED_ESCROW_ADDRESS; // 红包从创世即生效,无激活高度 |
There was a problem hiding this comment.
Require canonical RED counts before exempting
After the burn floor activates, a send to RED_ESCROW_ADDRESS with amount=1,burn=0 and memo = 'RED|' + '0'.repeat(505) + '1|r' is a 512-codepoint memo, but this branch exempts it and parseRedCreate normalizes the count to 1, so consensus accepts it as a normal one-coin red packet. The coin can then be claimed or refunded after expiry, leaving a max-length memo on chain for only normal fees instead of minMessageBurnFor; require a canonical bounded count before exempting RED creates.
Useful? React with 👍 / 👎.
| const p = memo.slice(IDCLAIM_PREFIX.length).trim().toLowerCase(); | ||
| return isValidName(p) ? { pseudonym: p } : null; // 复用 names.ts 的字符集 + 保留名规则 |
There was a problem hiding this comment.
Reject padded identity claim names
Once identity staking is active, an escrow claim like IDCLAIM|${' '.repeat(503)}x is a 512-codepoint memo, but this parser trims it to the pseudonym x; the IDCLAIM escrow branch then treats it as protocol and skips the message burn floor. Because the stake can be released after the lock, this allows repeated max-length identity memos for fees plus temporary lockup rather than the long-message burn, so the parser should require the raw memo to be the canonical IDCLAIM|<pseudonym> form.
Useful? React with 👍 / 👎.
四个真实 bug(NAME/MKT/MINE/RELAY):这几个分支只做「像不像合法 payload」的 粗筛,但底层解析用 .trim()/Number() 归一化,攻击者可以把真实内容压缩成 1 个 字符 + 几百位空格/前导零填充撑满 512 码点,仍被判定成合法小值,从而绕开消息 防刷底线: - NAME:trim() 会把首尾空白当填充吞掉——改成不 trim(NAME_RE 本就不含空白 字符,不 trim 直接判定既保留大小写不敏感、又堵死填充路径)。 - MKT/RELAY/MINE:价格/端口/深度坐标数量等数字字段用 Number() 解析,前导零 填充不改变数值——补上「反推规范 memo 逐字节比对」(MKT 用 String(price) 回比,RELAY/MINE 用各自 build*/make* 建造函数回比)。 RED/IDCLAIM 的字面 review 场景(真实 amount 的红包/认领带填充 payload)经核实 是假阳性——isMemoSpamCandidate 要求 amount=0 或自转,真实 amount>0 发往托管地址 的第三方转账天然不落入候选范围,无论 isProtocolMemo 怎么判都不受消息门槛影响。 但顺着同一处代码深挖发现了一个更窄的真实缺口:amount=0(未达最低要求)+ 对应 前缀发往托管地址时,此前只核对 to+atHeight,没核对 amount 是否达标——这类交易 根本不会被 consensus 当真操作接受(只是普通烧币),却被误判成协议层豁免。补上 `amount >= meta.count`(RED)/ `amount >= IDENTITY_STAKE_MIN`(IDCLAIM)。STAKE 有同类缺口但其门槛依赖区块难度、此处拿不到,暂不处理(已在注释标注、本轮范围外, 且该缺口下 memo 定长无法填充,经济影响可忽略)。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Round 8 review verdict (6 findings)四条真实 bug,已修(
两条(RED/IDCLAIM padding)核实后是假阳性,但顺着同一处代码深挖发现了一个更窄的真实缺口,已一并修复: 字面描述的攻击场景(真实 amount 的红包/认领带填充 payload)不成立—— (STAKE 有同类缺口,但其门槛 新增 12 条回归(4 个填充攻击场景 + 各自对照组 + RED/IDCLAIM amount 不足场景 + 已有 window/control 测试按新逻辑更新)+ 全量回归(typecheck 6 包、smoke、两个新 selftest、staking/onion/mint/antireplay 四个既有 selftest)全绿,CI 已跑通。 |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 45392733f1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (memo === PET_PREFIX) return selfTransfer; // 孵化:memo 精确、自转、burn 只要求 >0(无固定值),无 payload 可塞 | ||
| if (memo === FISH_PREFIX) return selfTransfer; // 铸渔获:同上 |
There was a problem hiding this comment.
Require burn on burn-based protocol exemptions
When a user submits an exact PET| or FISH| self-transfer with amount > 0 and burn = 0 after MIN_MESSAGE_BURN_ACTIVATION_HEIGHT, isMemoSpamCandidate includes it because it is a self-transfer with a memo, but these branches return true, so redOpError skips minMessageBurnFor. The game parsers require burn > 0 for these mints, so the transaction is not a real protocol action; it just lets chain spam avoid the new burn floor by paying only the normal fee. Require the same burn > 0 context here, and similarly for the LAND exemption below.
Useful? React with 👍 / 👎.
review 点名 PET|/FISH|(精确 memo 分支从未检查 burn,尽管其自身注释早已写着 "burn 只要求 >0")——parsePets/parseFish 都要求 burn>0 才算真实铸造 (tx.from!==tx.to || (tx.burn??0)<=0 早退),此前 isProtocolMemo 完全没检查 这个前提,burn=0 的精确 PET|/FISH| 自转会被误判成真实铸造而免费绕开消息门槛。 顺着同一处代码通盘检查了 isProtocolMemo 里其余每个分支是否也有"该核对 burn 却没核对"的同类缺口(逐条对照各自的真实 parser:staking/redpacket/identity 的 id-引用类操作本就不依赖 burn 值,consensus 只看 amount,故不需要补;PETX/ PETUNSTATION 经核实真实协议也不要求特定 burn,维持原样;CROPX 是尚未实现的 预留前缀,parseFarm 无条件忽略,没有"真实 burn 要求"可核对,维持原样)—— 唯一还有的真实缺口是 LAND(parseFarm 的 selfBurn 门槛要求 burn>0,地价恒为正), 一并补上。 PET|/FISH| 是精确匹配(无 payload 可塞)、LAND 数字 payload 早前已限 9 位, 三者的 spam 规模都有限,但判定本身是错的——它们根本不是真实协议操作,理应 受消息门槛约束。 新增 3 条回归(PET/FISH/LAND 各自 burn=0 场景)+ 全量回归(typecheck 6 包、 smoke、两个新 selftest、staking/onion/mint/antireplay 四个既有 selftest)全绿。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Round 9 review verdict (1 finding, checked comprehensively)真 bug,已修( 按你的要求全局过了一遍
PET/FISH 是精确匹配(无 payload 可塞)、LAND 数字 payload 早前已限 9 位,三者的实际 spam 规模都有限,但判定本身是错的——它们根本不是真实协议操作,理应受消息门槛约束,所以还是修了。 新增 3 条回归(PET/FISH/LAND 各自 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 71c284cd10
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| memo.startsWith(STAKE_PREFIX) || | ||
| export function isMemoSpamCandidate(tx: { amount: number; burn?: number; from: string; to: string; memo: string }): boolean { | ||
| if (isMessageTx(tx)) return true; | ||
| return tx.from === tx.to && tx.memo.length > 0; |
There was a problem hiding this comment.
Apply the burn floor to two-wallet memo shuttles
When the sender controls both addresses, excluding every from !== to transfer leaves the new floor bypassable: after activation they can send amount=1,burn=0 with a 512-codepoint memo to their own second wallet, have it accepted as a normal transfer for only minFeeFor(1), and send the coin back later. That stores the same long on-chain text without paying minMessageBurnFor, so the anti-spam burn floor should also cover low-cost/non-net-value memo shuttles rather than only literal self-transfers.
Useful? React with 👍 / 👎.
| } | ||
| const identityActive = atHeight >= IDENTITY_ACTIVATION_HEIGHT; | ||
| // 只拦「amount=0 的解锁新边界」;激活前带 IDRELEASE| 前缀但 amount>0 的普通转账仍按历史普通交易处理。 | ||
| if (!identityActive && m.startsWith(IDRELEASE_PREFIX) && tx.amount === 0) { |
There was a problem hiding this comment.
Preserve pre-activation IDRELEASE messages
When upgrading a chain that already contains an ordinary pre-activation message whose text starts with IDRELEASE| (amount=0,burn>0), this guard makes redOpError reject it even though that message shape was valid before this PR. Because validateChain runs this check while replaying historical blocks, a single old message with that prefix can make loadChain/replaceChain reject the chain even when identity activation is still in the future; restrict the pre-activation guard to the new zero-burn release shape rather than all amount-0 IDRELEASE memos.
Useful? React with 👍 / 👎.
| export function isProtocolMemo(tx: { | ||
| memo: string; | ||
| burn?: number; | ||
| from?: string; | ||
| to?: string; | ||
| amount?: number; | ||
| atHeight?: number; | ||
| }): boolean { |
There was a problem hiding this comment.
Preserve the memo-string isProtocolMemo API
Downstream browser/JS clients that already import the exported helper as isProtocolMemo(tx.memo) now hit this object-only signature at runtime: destructuring a string leaves memo undefined, and the first memo.startsWith(...) throws. The repository call sites were updated, but @v0idchain/core/browser is the public client entrypoint, so keep a string overload/backward-compatible normalization while adding the richer transaction context.
Useful? React with 👍 / 👎.
redOpError 的身份未激活守卫此前无条件拒绝「amount=0 + IDRELEASE| 前缀」的交易。 但真实解锁形态是 amount=0 **且 burn=0**(见 275 行强制 burn=0);「amount=0 + burn>0 + 正文恰好以 IDRELEASE| 开头」是一条合法的普通链上消息——本 PR 之前旧节点 照收(verifyTransaction 里 burn>0 即非空操作)。守卫少了 burn=0 条件,升级节点重放 历史链时会把这类老消息判非法 → validateChain 失败 → loadChain/replaceChain 拒绝 整条链(retroactive 分叉)。 补 `&& (tx.burn ?? 0) === 0`:真实解锁(amount=0+burn=0)仍被拦(与旧节点一致地拒 空操作),IDRELEASE| 开头的历史消息(burn>0)照常放行,两端行为一致、不分叉。 (同类结构 REDEEM(212 行,mint)/UNSTAKE·SLASH(207 行,staking)的未激活守卫也有相同 隐患,但属本 PR 之前的既有代码、且改动它们会触及 mint/staking 共识路径,不在本轮范围; 已另行记录待单独处理。) 新增回归:激活前 IDRELEASE| 开头的普通消息(burn>0)必须仍被接受、整链校验通过。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
本轮加固把 isProtocolMemo 从历史的 `(memo: string)` 改成需要完整交易语境的对象
签名。仓库内所有源码调用点已同步改为对象形式,但该函数经 @v0idchain/core/browser
公共出口暴露——外部 JS/浏览器客户端若仍按旧签名传裸字符串,会在 memo.startsWith
处崩溃(解构字符串得 memo=undefined)。
加一行归一:裸字符串 → `{ memo }`(无 from/to/amount/burn/atHeight 语境)。缺语境时
所有「需语境才敢豁免」的分支(②转托管创建需 to===托管地址、④游戏类需自转+达标烧币、
⑤需自转+burn=0)都保守返回 false(判为真消息、落回消息门槛),绝不会因缺语境误豁免——
字符串模式只会更严、不开新绕过口子;只有①id 引用类(纯 startsWith)与旧版完全一致。
新增回归:裸字符串 UNSTAKE| 返回 true(同旧版)、STAKE|guard 缺语境返回 false(保守)、
普通正文返回 false 且不抛异常。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Round 10 review verdict (3 findings)#2「Preserve pre-activation IDRELEASE messages」(blockchain.ts:217) — 真 bug,已修(
#3「Preserve the memo-string isProtocolMemo API」(messages.ts) — 真 bug(对公共出口而言),已修( #1「Apply the burn floor to two-wallet memo shuttles」(messages.ts) — 真实缺口,但修法是产品/设计取舍,需维护者拍板,未擅自改。确认可绕过: 为什么不直接修:任何干净的堵法都要改动「给别人转账+备注」这个本 PR 计划阶段明确拍板过的豁免("真实转账+备注不受门槛约束,产品上允许自由备注")。链上无法区分「A→自己的第二个钱包」和「A→真实第三方」(地址不可证明同属一人),所以要堵这条只能:(a) 对所有带 memo 的交易一律套门槛(连真实付款的短备注也要按长度烧币,破坏"自由备注");或 (b) 引入「免费备注额度」新参数,超额部分才套门槛(保住短备注、堵住长文接力,但这是新的经济设计参数)。两者都改变了既定产品语义,属维护者的取舍,不该由 review 轮次里我单方面重塑。已在对话中向维护者提出,等其定夺。 #2/#3 全量回归(typecheck 6 包、smoke、两个新 selftest、staking/onion/mint/antireplay 四个既有 selftest)全绿,CI 已跑通。 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b7d3154c02
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (typeof tx === 'string') tx = { memo: tx }; | ||
| const { memo, from, to } = tx; | ||
| const burn = tx.burn ?? 0; | ||
| const amount = tx.amount ?? 0; | ||
| const atHeight = tx.atHeight ?? Infinity; // 未传高度(如旧调用点/纯单元测试)时按“已激活”处理,不新增拒绝面 | ||
| const selfTransfer = from !== undefined && to !== undefined && from === to; |
There was a problem hiding this comment.
Preserve legacy string protocol checks
When downstream/browser clients keep using the exported helper as isProtocolMemo(tx.memo), this compatibility overload still discards all transaction context; selfTransfer is false and burn defaults to 0, so legacy memo-only protocol checks such as isProtocolMemo('PET|') or isProtocolMemo('FISH|') now return false instead of the pre-PR true result. Fresh evidence after the compatibility fix is that this line only wraps strings as { memo } while the game-protocol branches below require from/to/burn, so clients relying on the documented memo-only filter can start displaying protocol mints as user messages; preserve the old prefix-only behavior for string inputs or split the strict transaction-aware API into a separate helper.
Useful? React with 👍 / 👎.
| if (memo.startsWith(PETX_PREFIX)) { | ||
| // 送崽:转移给别人(非自转)+ 真的转了币,payload=petId。amount=0 的「转移」不是真实语义,不豁免。 | ||
| return !selfTransfer && amount > 0 && HEX64.test(memo.slice(PETX_PREFIX.length)); |
There was a problem hiding this comment.
Keep PETX classification aligned with the parser
For post-activation PETX|<petId> transactions where amount === 0 and burn > 0, this now returns false because it requires amount > 0, but parsePets still treats the same non-self transfer as a valid ownership transfer as long as the sender owns the pet and to is a valid different address. In that scenario a previously valid low-burn PETX transfer is either rejected by the new burn floor or, if it pays the floor, is also shown as a message, so either tighten parsePets to require the same amount shape or keep these parser-valid PETX memos exempt.
Useful? React with 👍 / 👎.
绕过:A→自己控制的第二个钱包 amount=1 + burn=0 + 512 码点 memo,因既非 amount=0
消息(isMessageTx)、又非自转,漏过 isMemoSpamCandidate 的前两条,只付 minFeeFor(1)=1
即可在链上塞满 MAX_MEMO 文本,绕开 minMessageBurnFor(512)=30 的销毁门槛(约 15× 便宜)。
链上无法证明两地址是否同属一人,无法靠 from/to 区分真实付款与自我接力。
维护者拍板方案=免费备注额度(在"堵接力"与"保住真实付款的自由附言"间折中):
- 新增 MESSAGE_FREE_MEMO_CHARS=64。
- isMemoSpamCandidate 加第 (3) 条:memo 码点数 > 额度 → 候选,无论 from/to/amount。
- 真实付款的短附言(≤64,一行中英文够用)仍完全免费;超长文本不管怎么路由(自转/发消息/
双钱包接力)统一按 minMessageBurnFor(全长) 受门槛。接力每笔最多免费夹带 64 码点,
要塞满 512 需 8 笔交易+8 份手续费+8 次来回,灌水成本抬高约 8×。
不引入新共识边界:与消息门槛同挂 MIN_MESSAGE_BURN_ACTIVATION_HEIGHT。协议层 memo
(RELAY/PLANT 等本就可能 >64)仍由 isProtocolMemo 单独豁免,不受第 (3) 条误伤。收件箱
展示仍由 isMessageTx 独立把关,付款+长备注不进消息列表。
两条编码了旧产品决定("转给别人的长 memo 一律豁免")的既有测试按新语义重写;
新增回归:接力候选/边界(>而非≥)/端到端拒收与烧够放行/短备注付款仍免费。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
#1「双钱包接力」— 已按维护者选定方案修复(
|
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ebef92a0fb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| export function isMemoSpamCandidate(tx: { amount: number; burn?: number; from: string; to: string; memo: string }): boolean { | ||
| if (isMessageTx(tx)) return true; // (1) | ||
| if (tx.from === tx.to && tx.memo.length > 0) return true; // (2) | ||
| return [...tx.memo].length > MESSAGE_FREE_MEMO_CHARS; // (3) 按 Unicode 码点计,同 MAX_MEMO/minMessageBurnFor 口径 |
There was a problem hiding this comment.
Thread burn support through long memo sends
After activation, this branch makes any transfer memo longer than MESSAGE_FREE_MEMO_CHARS require minMessageBurnFor(...), but the node-signed /send path still goes through V0idNode.send -> submit -> createTransaction, and neither that helper nor the CLI /send --memo exposes a burn amount. In that context, a normal payment with a 65+ character memo is always signed with burn=0 and then rejected by redOpError, with no way for wallet/API users to pay the newly required burn except hand-crafting /tx/submit transactions.
Useful? React with 👍 / 👎.
| '非质押人(eve)解锁被拒(即便挂着 alice 的 claimId)', | ||
| !bc.addTransaction(createTransaction(eve, eve.address, 0, bc.nonceOf(eve.address), `${IDRELEASE_PREFIX}${claimId}`, MIN_FEE)).ok, | ||
| ); | ||
| while (bc.height < claimHeight + IDENTITY_LOCK_BLOCKS) await bc.mine(alice.address); |
There was a problem hiding this comment.
Avoid full-chain mining in the new identity CI test
In the new CI selftest, this loop starts from a chain forged to height 45000 and then calls bc.mine for the 240-block identity lock period; each bc.mine goes through addBlock and validateChain([...]), revalidating and rehashing the whole ~45k-block chain per iteration. This makes the newly added CI step extremely slow or prone to hanging; use the existing forge helper for these empty wait blocks and validate once afterward.
Useful? React with 👍 / 👎.
| if (memo.startsWith(DEL_PREFIX)) { | ||
| // 撤单:V0idNode.marketDelist() 固定自转 1 币 + burn=0;payload = 上架交易 txid(64-hex)。 | ||
| return selfTransfer && burn === 0 && HEX64.test(memo.slice(DEL_PREFIX.length)); |
There was a problem hiding this comment.
Whitelist BUY memos before applying the burn floor
This whitelist covers marketplace listings and delists but omits purchases: marketBuy signs a normal transfer to the seller with memo BUY|<64-hex listing id> and burn=0. Once MIN_MESSAGE_BURN_ACTIVATION_HEIGHT is reached, that 68-character memo exceeds the free allowance, is not recognized as protocol, and redOpError rejects every marketplace buy as an under-burned message.
Useful? React with 👍 / 👎.
两处都是上两轮改动的连锁副作用: #3(P1) BUY 购买被消息门槛误杀:marketBuy 付 price 给卖家、memo `BUY|<64hex>`(68 码点)、 burn=0。#1 的"超长 memo 一律受约束"长度网把它算进候选,而 isProtocolMemo 只白名单了 MKT/DEL 上架撤单、漏了 BUY → 激活后每一笔集市购买都被当"未达标真消息"拒收。补 BUY 分支 (付款给别人的唯一协议形态:from≠to + amount>0 + burn=0 + payload 定长 64hex 不可填充,豁免安全)。 #1(P2) 字符串入参语义错位:#3(round-10)的 string→{memo} shim 让 PET|/FISH| 这类"需自转+ burn>0 语境"的分支在缺语境时返回 false → 历史"展示过滤"字符串调用方(parseMessages 式)会把 崽/渔获协议 memo 当私信显示。展示层与经济层的保守方向相反(展示缺信息应"当协议别显示",经济缺 信息应"当真消息收门槛")。故字符串入参恢复历史纯前缀语义(命中任一协议前缀即 true,见 PROTOCOL_MEMO_PREFIXES),精确复刻旧展示行为;对象入参才走完整语境化豁免。消息门槛只由对象 调用点(isRealMessage→redOpError)驱动,字符串分支永不参与经济门槛、不开绕过口子。 新增回归:BUY 三形态(真实/自转/burn>0)、字符串 UNSTAKE/STAKE/PET/FISH/ENC/普通正文。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
#1 免费备注额度激活后,>64 码点的付款备注需按 minMessageBurnFor 销毁,但 V0idNode.send→ submit→createTransaction 全链没有 burn 参数,CLI/API 也没暴露 → 长备注普通付款永远 burn=0、 被 redOpError 静默拒收,钱包/API 用户除了手搓 /tx/submit 无从补烧。 打通 burn: - createTransaction 加可选 burn=0(transactionPayloadHash 仅在 burn>0 时计入 → burn=0 逐字节 与升级前一致,不动任何旧交易哈希)。 - V0idNode.send 加可选 burn;省略时自动判定:若这笔转账会落入消息门槛(复用共识同款 isRealMessage 谓词 + MIN_MESSAGE_BURN_ACTIVATION_HEIGHT 门控)则自动补足 minMessageBurnFor,普通短备注付款 保持 burn=0 不受影响。submit 透传 burn。 - API /send、CLI send --burn 暴露显式覆盖;省略走 send 的自动补足。 新增回归:createTransaction 长备注付款 burn=0 被拒 / 带足 burn 被接受(验证 burn 逐字节流入 txid)。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
parsePets 的 PETX 分支只校验「归属+接收方合法+非自转」,不看 amount;但文件头约定与 isProtocolMemo 都定「送崽 = 转 1 币给对方」(amount>0)。#1 长度网让 `PETX|<64hex>`(69 码点) 落入候选后,一笔 amount=0 的 PETX 会出现分类冲突:parsePets 认它有效转移了崽,而 isProtocolMemo 因 amount=0 不豁免 → 消息门槛把这笔"有效送崽"当真消息误杀/误显示。 给 parsePets 补 `tx.amount > 0` 守卫,让解析器与分类器、与文档约定三方一致:amount=0 的 PETX 不再被当有效转移。 新增回归(smoke):amount=0 的 PETX(burn>0 过校验)进池后 parsePets 不转移归属,崽仍归原主。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
identity-selftest 过 IDENTITY_LOCK_BLOCKS(240) 锁定期用逐块 bc.mine——每块 addBlock→ validateChain 全链重放,在已 forge 到 45000 的链上 240 次 ≈ 全链×240,CI 里这一步 ~236s。 改用 forgeTo 廉价推进(直接 append+跳过逐块整链校验,最后一次性 validateChain),该 selftest 从 ~236s 降到 ~48s(约 5×)。 配套修 forgeAppendBlock 的时间戳:原「创世+index×60s」在**真实时间块(bc.mine 用 Date.now) 之上继续 forge** 时,+60s/块会把时间戳推到当前时钟数小时之后 → 触发 validateChain 的 「未来时间戳」上界。钳到不超过 Date.now():从创世 forge 时合成时间恒 < now → 与旧版逐字一致 (仍 60s 间隔、难度降地板);在真实块之上 forge 时回退 prev+1ms、贴当前时钟递增不越界。 已回归全部用 forgeTo 的 selftest(staking/message-burn-floor/identity)+ smoke,均全绿。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Round 11 review verdict (5 findings — 全部真实,全部已修)这一轮大多是上两轮改动(#3 字符串兼容、#1 免费额度长度网)的连锁副作用,系统排查了 #3(P1)「Whitelist BUY memos」— 已修( #1(P2)「Preserve legacy string protocol checks」— 已修( #2(P2)「Keep PETX aligned with parser」— 已修( #4(P2)「Thread burn through long-memo sends」— 已修( #5(P2)「Avoid full-chain mining in identity test」— 已修( 全量回归(typecheck 6 包、smoke、两个新 selftest、staking/onion/mint/antireplay)全绿,CI 已跑通。(P1 的两个激活高度占位符、REDEEM/UNSTAKE·SLASH 未激活守卫仍是既有事项,分别待部署时确认 / 已在独立任务跟进。) |
|
@codex review |
|
Codex Review: Didn't find any major issues. Bravo. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
背景
上一轮(PR #53)立了 ROADMAP thesis:$V0ID = 让开放匿名网络不靠监控就能抵御滥用/女巫/审查的经济层。本 PR 开始动手实现 L1(反滥用/反女巫燃料)。
L1 原有三个候选,经探索发现工程量差异巨大——付费托管隐藏服务架构上今天做不到(中继纯转发、从不托管别人内容,需要独立改中继/HS/mint 三个子系统),本 PR 不碰,留给未来单独立项。本 PR 只做另外两个:
改了什么
Feature A:消息防刷底线(随 memo 长度递增)
现状
MESSAGE_BURN=5只是 CLI 默认值,共识层完全不强制——--burn 1一样能发。新增:minMessageBurnFor(memoLength) = MESSAGE_BURN + floor(memoLength / 20),整数除法(禁浮点跨节点分叉)。redOpError(带atHeight参数)而非verifyTransaction(没有 height 参数,做不了激活高度门控)——这是设计上的关键决定,已核对源码确认。isRealMessage(真消息,非协议层 memo)生效,isProtocolMemo排除列表保证PET|/FISH|等本就低烧币的协议操作(如钓鱼烧 2)不受影响。createMessage默认烧币值同步改为按长度算,避免激活后不知情客户端发长消息被拒。Feature B:质押式匿名身份(纯资金锁仓,无罚没,无仲裁者)
新文件
identity.ts:IDCLAIM|<pseudonym>转托管认领 /IDRELEASE|<claimTxid>解锁。names.ts昵称系统上——昵称已实网免费上线,追加质押要求会打破既有承诺。names.ts的字符集/保留名规则,避免用户学两套规则。两个功能均严格照抄
staking.ts/redpacket.ts的软分叉套路(config 常量 + 纯函数 +blockchain.ts三处redOpError/applyTx/applySelect),激活高度均为占位值(40000/45000,注释写明部署前须核对实时链高)。下游
node.ts/api.ts(GET/POST /identity*)/cli(identity命令组)/web(IdentityPanel)。消息防刷是纯共识层收紧,无需新端点。顺手修的回归
scripts/smoke.ts的加密私信场景原挖 8 块(旧扁平MESSAGE_BURN=5够用),新默认值按密文长度算出 11+手续费=12,余额不足导致交易静默进不了 mempool、下一行访问parseMessages(...)[0]崩溃——这是本次改动合理暴露的测试夹具问题,已修复(挖 16 块 + 补addTransaction返回值检查)。验证
typecheck(6 包全绿)smoke(含修复后的加密私信场景)message-burn-floor-selftest.ts/identity-selftest.ts(均含分叉安全computeState≡validateChain≡replaceChain+ 供应量守恒)staking-selftest.ts/onion-selftest.ts/mint-selftest.ts/antireplay-test.ts重跑无回归明确不做
names.ts一行未改(除被identity.ts只读 import 的isValidName/RESERVED_NAMES)。🤖 Generated with Claude Code