链群名称校验规则
概述
LOVE20 Group 系统中的链群名称需要严格校验,以保障安全性、一致性与良好的用户体验。本规则采取了宽松但安全的方案,支持国际化字符,同时避免滥用风险。
重要:链群名称采用大小写不敏感(仅 ASCII)的唯一性检查,即仅将 ASCII 字母 A-Z 视为与 a-z 等价(例如 MyGroup、mygroup、MYGROUP 视为同一名称,先注册者得)。系统会保留用户注册时的原始大小写用于显示。
注意:系统不做 Unicode casefold,也不做 NFC/NFKC 等 Unicode 规范化;除 ASCII A-Z 外,其他字符按 UTF-8 字节序列精确比较。
校验规则
✅ 允许
-
长度:1~64 字节(UTF-8 编码)
- 注意:单个 Unicode 字符可能占用多个字节
- 例子:”群”(UTF-8 下占用 3 字节)
-
字符类型:
- ✅ 字母:
a-z,A-Z - ✅ 数字:
0-9 - ✅ 特殊符号:所有 ASCII 可打印字符(0x21-0x7E,除空格外),包括
-、_、.、@、!、#、$、%、^、&、*等 - ✅ Unicode 字符:中文、日文、韩文、单码点表情符号等
- ⚠️ 注意:需要零宽连接符(ZWJ,U+200D)的复合 Emoji(如 👨👩👧👦)不被支持,因为 ZWJ 字符已被禁止以防止混淆攻击。仅支持单个码点的表情符号(如 😀、🎉、❤️)
- ✅ 字母:
-
格式:
- ✅ 大小写混合(注意:唯一性检查仅对 ASCII 字母不区分大小写)
- ✅ 混用特殊符号
❌ 拒绝
-
空名称
- 报错:
GroupNameEmpty()
- 报错:
-
超过 64 字节的名称
- 报错:
InvalidGroupName()
- 报错:
-
空格字符 (0x20 及所有 Unicode 空格)
- 例子:” Group”、”Group “、”Group Name”、”Group Name”(全角空格)
- 报错:
InvalidGroupName() - 包括但不限于:
- U+0020: 普通空格 (Space)
- U+00A0: 不换行空格 (No-Break Space)
- U+1680: Ogham 空格标记 (Ogham Space Mark)
- U+2000-U+200A: 各种宽度的空格(En Quad, Em Quad, En Space, Em Space 等)
- U+202F: 窄不换行空格 (Narrow No-Break Space)
- U+205F: 中等数学空格 (Medium Mathematical Space)
- U+3000: 中文全角空格 (Ideographic Space)
-
控制字符 (0x00-0x1F)
- 包含:换行符(
\n)、制表符(\t)、回车符(\r) 等 - 报错:
InvalidGroupName()
- 包含:换行符(
-
C1 控制字符 (0x80-0x9F)
- Latin-1 补充中的控制字符
- 报错:
InvalidGroupName()
-
DEL 字符 (0x7F)
- 报错:
InvalidGroupName()
- 报错:
-
行分隔符和段落分隔符
- U+2028: 行分隔符 (Line Separator)
- U+2029: 段落分隔符 (Paragraph Separator)
- 报错:
InvalidGroupName() - 说明:这些字符可能被解析为换行,导致显示异常
-
方向格式化字符
- U+061C: 阿拉伯字母标记 (Arabic Letter Mark)
- U+202A-U+202E: 双向文本格式化字符
- U+202A: 左到右嵌入 (Left-to-Right Embedding)
- U+202B: 右到左嵌入 (Right-to-Left Embedding)
- U+202C: 弹出方向格式 (Pop Directional Formatting)
- U+202D: 左到右覆盖 (Left-to-Right Override)
- U+202E: 右到左覆盖 (Right-to-Left Override)
- U+2066-U+2069: 双向隔离控制字符
- U+2066: 左到右隔离 (Left-to-Right Isolate)
- U+2067: 右到左隔离 (Right-to-Left Isolate)
- U+2068: 首强隔离 (First Strong Isolate)
- U+2069: 弹出方向隔离 (Pop Directional Isolate)
- 报错:
InvalidGroupName() - 说明:这些字符可用于创建视觉欺骗,改变文本显示方向(包括 Trojan Source 攻击 CVE-2021-42574)
- 零宽字符(Zero-Width Characters)
- 零宽空格 (U+200B)
- 零宽非连接符 (U+200C)
- 零宽连接符 (U+200D)
- 左到右标记 (U+200E)
- 右到左标记 (U+200F)
- 组合用字形连接符 (U+034F)
- 零宽不换行空格/BOM (U+FEFF)
- 词连接符 (U+2060)
- 软连字符 (U+00AD)
- 报错:
InvalidGroupName() - 说明:这些不可见字符常被用于混淆和欺骗攻击,因此被完全禁止
- 不可见数学运算符(Invisible Mathematical Operators)
- U+2061: 函数应用 (Function Application)
- U+2062: 不可见乘号 (Invisible Times)
- U+2063: 不可见分隔符 (Invisible Separator)
- U+2064: 不可见加号 (Invisible Plus)
- 报错:
InvalidGroupName() - 说明:这些用于数学排版的不可见运算符在文本中可能造成混淆
- 废弃格式化字符(Deprecated Format Characters)
- U+206A: 禁止对称交换 (Inhibit Symmetric Swapping)
- U+206B: 激活对称交换 (Activate Symmetric Swapping)
- U+206C: 禁止阿拉伯形态 (Inhibit Arabic Form Shaping)
- U+206D: 激活阿拉伯形态 (Activate Arabic Form Shaping)
- U+206E: 国家数字形态 (National Digit Shapes)
- U+206F: 标称数字形态 (Nominal Digit Shapes)
- 报错:
InvalidGroupName() - 说明:这些是已废弃的 Unicode 格式化字符,可能被某些系统解析导致意外行为
示例
合法链群名称示例 ✅
"MyGroup" // 简单 ASCII
"Group-123" // 含数字与短横线
"My_Group.v2" // 多种特殊符号
"链群名称" // 中文
"Group链群" // 中英文混合
"😀PartyGroup" // 单码点 Emoji(支持)
"Group@Company" // 含 @ 符号
"a" // 单字符
"1234567890123456789012345678901234567890123456789012345678901234" // 64 字节(最大)
大小写不敏感示例
// 假设 "MyGroup" 已被注册
"mygroup" // 失败:GroupNameAlreadyExists()(与 MyGroup 冲突)
"MYGROUP" // 失败:GroupNameAlreadyExists()(与 MyGroup 冲突)
"mYgRoUp" // 失败:GroupNameAlreadyExists()(与 MyGroup 冲突)
// 查询时也不区分大小写
tokenIdOf("MyGroup") // 返回 token ID
tokenIdOf("mygroup") // 返回相同的 token ID
isGroupNameUsed("MYGROUP") // 返回 true
// 但 groupNameOf() 返回原始大小写
groupNameOf(tokenId) // 返回 "MyGroup"(用户注册时的原始格式)
非法链群名称示例 ❌
"" // 空字符串 (GroupNameEmpty)
" Group" // 首部空格 (InvalidGroupName)
"Group " // 尾部空格 (InvalidGroupName)
"Group Name" // 内部 ASCII 空格 (InvalidGroupName)
"Group Name" // 中文全角空格 U+3000 (InvalidGroupName)
"Group\u00A0Name" // 不换行空格 (InvalidGroupName)
"Group\u2003Name" // Em Space (InvalidGroupName)
"Group\nName" // 换行符 (InvalidGroupName)
"Group\tName" // 制表符 (InvalidGroupName)
"Group\u0080Name" // C1 控制字符 (InvalidGroupName)
"Group\u2028Name" // 行分隔符 (InvalidGroupName)
"Group\u2029Name" // 段落分隔符 (InvalidGroupName)
"Group\u061CName" // 阿拉伯字母标记 (InvalidGroupName)
"Group\u202EName" // 右到左覆盖 (InvalidGroupName)
"Group\u200BName" // 零宽空格 (InvalidGroupName)
"Group\u200CName" // 零宽非连接符 (InvalidGroupName)
"Group\u200DName" // 零宽连接符 ZWJ (InvalidGroupName)
"👨👩👧👦Group" // 复合 Emoji(含 ZWJ)(InvalidGroupName)
"Group\u034FName" // 组合用字形连接符 (InvalidGroupName)
"Group\u00ADName" // 软连字符 (InvalidGroupName)
"\uFEFFGroupName" // BOM字符 (InvalidGroupName)
"Group\u2066Name" // 左到右隔离 (InvalidGroupName)
"Group\u2067Name" // 右到左隔离 (InvalidGroupName)
"Group\u2061Name" // 不可见函数应用 (InvalidGroupName)
"Group\u206AName" // 废弃格式化字符 (InvalidGroupName)
"12345678901234567890123456789012345678901234567890123456789012345" // 65 字节 (InvalidGroupName)
技术说明
字节长度与字符数的区别
64 字节上限指的是UTF-8 编码下的字节数,并非字符数量。即:
- 英文/数字/常见符号:每个 1 字节,可用 64 个字符
- 中文/日文/韩文等 CJK 字符:通常每个 3 字节,约 21 个字符
- Emoji 表情:通常每个 4 字节,约 16 个字符
- 混合内容:可用字符数根据组合而变化
实现说明
实际校验由合约内部 _isValidGroupName() 函数实现,其步骤包括:
- 校验字节长度为 1~64
- 遍历字节过滤 C0 控制字符 (0x00-0x1F) 和 ASCII 空格 (0x20)
- 拒绝 DEL 字符 (0x7F)
- 检测并拒绝 C1 控制字符 (0x80-0x9F)
- 检测并拒绝所有 Unicode 空格字符(U+00A0、U+1680、U+2000-U+200A、U+202F、U+205F、U+3000)
- 检测并拒绝行/段落分隔符(U+2028、U+2029)
- 检测并拒绝方向格式化字符(U+061C、U+202A-U+202E、U+2066-U+2069)
- 检测并拒绝零宽字符(通过匹配特定的 UTF-8 字节序列)
- 检测并拒绝不可见数学运算符(U+2061-U+2064)
- 检测并拒绝废弃格式化字符(U+206A-U+206F)
- 验证 UTF-8 编码有效性(拒绝无效起始字节 0x80-0xC1、0xF5-0xFF,检测过长编码和无效代理对)
- 允许所有其他字符(包括多字节 Unicode)
UTF-8 编码验证
合约实现了完整的 UTF-8 编码验证,以下情况会被拒绝:
| 情况 | 说明 |
|---|---|
| 无效起始字节 (0x80-0xC1) | 这些字节不能作为 UTF-8 序列的起始字节 |
| 无效起始字节 (0xF5-0xFF) | 超出 UTF-8 编码范围的字节 |
| 过长编码 | 使用多于必要字节数编码的字符(如用 2 字节编码 ASCII 字符) |
| UTF-16 代理对 (U+D800-U+DFFF) | 这些码点在 UTF-8 中无效 |
| 超出范围的码点 (> U+10FFFF) | Unicode 标准不允许的码点 |
| 不完整的多字节序列 | 多字节字符的后续字节缺失或格式错误 |
大小写处理
系统采用双重存储方案实现大小写不敏感的唯一性检查:
_groupNames:存储原始名称(保留用户输入的大小写)_normalizedNameToTokenId:存储小写版本用于唯一性检查
转换规则:
- 仅转换 ASCII 大写字母
A-Z(0x41-0x5A)为小写a-z(0x61-0x7A) - 中文、日文、Emoji 等非 ASCII 字符保持不变
Gas 影响:
_toLowerCase()函数遍历字节数组,O(n) 复杂度- 额外一个 mapping 写入,增加约 20k gas
- 对于 64 字节名称,转换开销约 3-5k gas(包括内存分配和字节遍历)
安全注意事项
规则设计初衷
- 拒绝控制字符:防止注入攻击及显示问题(包括 C0、C1 控制字符)
- 禁止所有空格:包括 ASCII 空格和所有 Unicode 空格字符,避免混淆、欺骗与输入错误
- 拒绝分隔符:防止行/段落分隔符导致的显示异常
- 拒绝方向格式化:防止双向文本覆盖攻击和视觉欺骗
- 拒绝零宽字符:防止不可见字符造成的混淆和欺骗攻击
- 限制长度:减少 GAS 消耗与存储负担
- UTF-8 国际化支持:为全球用户提供原生命名体验
空格字符防护
系统已实现对所有常见 Unicode 空格字符的检测和拒绝:
| Unicode | 名称 | UTF-8 编码 | 说明 |
|---|---|---|---|
| U+0020 | 普通空格 | 0x20 | ASCII 空格 |
| U+00A0 | 不换行空格 | 0xC2 0xA0 | No-Break Space |
| U+1680 | Ogham 空格标记 | 0xE1 0x9A 0x80 | Ogham Space Mark |
| U+2000 | En Quad | 0xE2 0x80 0x80 | En Quad |
| U+2001 | Em Quad | 0xE2 0x80 0x81 | Em Quad |
| U+2002 | En Space | 0xE2 0x80 0x82 | En Space |
| U+2003 | Em Space | 0xE2 0x80 0x83 | Em Space |
| U+2004 | Three-Per-Em Space | 0xE2 0x80 0x84 | Three-Per-Em Space |
| U+2005 | Four-Per-Em Space | 0xE2 0x80 0x85 | Four-Per-Em Space |
| U+2006 | Six-Per-Em Space | 0xE2 0x80 0x86 | Six-Per-Em Space |
| U+2007 | Figure Space | 0xE2 0x80 0x87 | Figure Space |
| U+2008 | Punctuation Space | 0xE2 0x80 0x88 | Punctuation Space |
| U+2009 | Thin Space | 0xE2 0x80 0x89 | Thin Space |
| U+200A | Hair Space | 0xE2 0x80 0x8A | Hair Space |
| U+202F | 窄不换行空格 | 0xE2 0x80 0xAF | Narrow No-Break |
| U+205F | 中等数学空格 | 0xE2 0x81 0x9F | Medium Math Space |
| U+3000 | 中文全角空格 | 0xE3 0x80 0x80 | Ideographic Space |
防护原理:通过在字节级别检测这些字符的 UTF-8 编码序列,可有效防止:
- 视觉相同但实际不同的名称欺骗(如使用不同空格字符)
- 隐藏字符导致的安全漏洞
- 前端显示不一致的问题
- 用户误输入全角空格等不易察觉的字符
行/段落分隔符防护
| Unicode | 名称 | UTF-8 编码 | 说明 |
|---|---|---|---|
| U+2028 | 行分隔符 | 0xE2 0x80 0xA8 | Line Separator |
| U+2029 | 段落分隔符 | 0xE2 0x80 0xA9 | Paragraph Separator |
防护原理:这些字符虽然不是传统的换行符,但在某些上下文中可能被解析为换行,导致:
- 显示异常和布局错乱
- 验证逻辑与显示逻辑不一致
- 潜在的注入攻击风险
方向格式化字符防护
| Unicode | 名称 | UTF-8 编码 | 说明 |
|---|---|---|---|
| U+061C | 阿拉伯字母标记 | 0xD8 0x9C | Arabic Letter Mark |
| U+202A | 左到右嵌入 | 0xE2 0x80 0xAA | Left-to-Right Embedding |
| U+202B | 右到左嵌入 | 0xE2 0x80 0xAB | Right-to-Left Embedding |
| U+202C | 弹出方向格式 | 0xE2 0x80 0xAC | Pop Directional Formatting |
| U+202D | 左到右覆盖 | 0xE2 0x80 0xAD | Left-to-Right Override |
| U+202E | 右到左覆盖 | 0xE2 0x80 0xAE | Right-to-Left Override |
| U+2066 | 左到右隔离 | 0xE2 0x81 0xA6 | Left-to-Right Isolate |
| U+2067 | 右到左隔离 | 0xE2 0x81 0xA7 | Right-to-Left Isolate |
| U+2068 | 首强隔离 | 0xE2 0x81 0xA8 | First Strong Isolate |
| U+2069 | 弹出方向隔离 | 0xE2 0x81 0xA9 | Pop Directional Isolate |
防护原理:这些双向文本(Bidi)格式化字符可以改变文本的显示方向,被用于:
- 视觉欺骗攻击:显示的文本与实际字符序列不同
- 钓鱼攻击:创建看似合法但实际恶意的名称
- 同形异义攻击:利用方向变化创建视觉上相同的不同名称
著名案例:2021 年的”Trojan Source”攻击(CVE-2021-42574)就是利用这些字符进行代码注入。
C1 控制字符防护
| 范围 | UTF-8 编码 | 说明 |
|---|---|---|
| 0x80-0x9F | 0xC2 0x80-0x9F | Latin-1 补充控制字符 |
防护原理:C1 控制字符是 Latin-1 字符集中的控制字符,虽然不如 C0 常见,但同样可能导致:
- 终端和显示异常
- 与某些系统的兼容性问题
- 潜在的安全风险
零宽字符防护
系统已实现对以下零宽字符的检测和拒绝:
| Unicode | 名称 | UTF-8 编码 |
|---|---|---|
| U+200B | 零宽空格 | 0xE2 0x80 0x8B |
| U+200C | 零宽非连接符 | 0xE2 0x80 0x8C |
| U+200D | 零宽连接符 | 0xE2 0x80 0x8D |
| U+200E | 左到右标记 | 0xE2 0x80 0x8E |
| U+200F | 右到左标记 | 0xE2 0x80 0x8F |
| U+034F | 组合用字形连接符 | 0xCD 0x8F |
| U+FEFF | BOM/零宽不换行空格 | 0xEF 0xBB 0xBF |
| U+2060 | 词连接符 | 0xE2 0x81 0xA0 |
| U+00AD | 软连字符 | 0xC2 0xAD |
防护原理:通过在字节级别检测这些字符的 UTF-8 编码序列,可有效防止:
- 视觉相同但实际不同的名称欺骗
- 隐藏字符导致的安全漏洞
- 前端显示不一致的问题
不可见数学运算符防护
| Unicode | 名称 | UTF-8 编码 | 说明 |
|---|---|---|---|
| U+2061 | 函数应用 | 0xE2 0x81 0xA1 | Function Application |
| U+2062 | 不可见乘号 | 0xE2 0x81 0xA2 | Invisible Times |
| U+2063 | 不可见分隔符 | 0xE2 0x81 0xA3 | Invisible Separator |
| U+2064 | 不可见加号 | 0xE2 0x81 0xA4 | Invisible Plus |
防护原理:这些字符用于数学排版,在普通文本中不可见,可能导致:
- 名称看起来相同但实际包含隐藏字符
- 字节长度与可见长度不一致造成的混淆
- 前端显示与实际存储数据不匹配
废弃格式化字符防护
| Unicode | 名称 | UTF-8 编码 | 说明 |
|---|---|---|---|
| U+206A | 禁止对称交换 | 0xE2 0x81 0xAA | Inhibit Symmetric Swapping |
| U+206B | 激活对称交换 | 0xE2 0x81 0xAB | Activate Symmetric Swapping |
| U+206C | 禁止阿拉伯形态 | 0xE2 0x81 0xAC | Inhibit Arabic Form Shaping |
| U+206D | 激活阿拉伯形态 | 0xE2 0x81 0xAD | Activate Arabic Form Shaping |
| U+206E | 国家数字形态 | 0xE2 0x81 0xAE | National Digit Shapes |
| U+206F | 标称数字形态 | 0xE2 0x81 0xAF | Nominal Digit Shapes |
防护原理:这些是 Unicode 已废弃的格式化字符,虽然已被弃用,但:
- 某些旧系统或特定上下文仍可能解析这些字符
- 可能导致不同系统间的显示不一致
- 作为不可见字符,存在与零宽字符类似的安全风险
潜在边缘情况
-
右到左(RTL)字符:允许使用,但显示可能造成困扰
- 前端应妥善处理 RTL 文字方向
- 注意:RTL 标记符(U+200E/U+200F)已被禁止
-
同形异义攻击(Homograph):有些脚本下字符极为相似
- 例:”A”(拉丁)与 “А”(西里尔)
- 例:”0”(数字零)与 “O”(字母 O)
- 建议应用层增加前端提示与风险提醒(如 confusables 检测、混合脚本提示、相似名称提示)
集成方最佳实践
前端校验范例
合约调用前建议先在前端做一次校验:
function isValidGroupName(name) {
// 校验字节长度(UTF-8 编码)
const bytes = new TextEncoder().encode(name);
if (bytes.length === 0 || bytes.length > 64) return false;
// 校验 ASCII 空格和控制字符 (U+0000-U+0020, U+007F)
if (/[\u0000-\u0020\u007F]/.test(name)) return false;
// 校验 C1 控制字符 (U+0080-U+009F)
if (/[\u0080-\u009F]/.test(name)) return false;
// 校验所有 Unicode 空格字符
const unicodeSpaces = /[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/;
if (unicodeSpaces.test(name)) return false;
// 校验行/段落分隔符
if (/[\u2028\u2029]/.test(name)) return false;
// 校验方向格式化字符(包括双向隔离控制字符)
const bidiChars = /[\u061C\u202A-\u202E\u2066-\u2069]/;
if (bidiChars.test(name)) return false;
// 校验零宽字符
const zeroWidthChars = /[\u200B-\u200F\uFEFF\u2060\u00AD\u034F]/;
if (zeroWidthChars.test(name)) return false;
// 校验不可见数学运算符
const invisibleMathOps = /[\u2061-\u2064]/;
if (invisibleMathOps.test(name)) return false;
// 校验废弃格式化字符
const deprecatedChars = /[\u206A-\u206F]/;
if (deprecatedChars.test(name)) return false;
return true;
}
用户体验建议
- 实时校验:输入时就给出反馈
- 字节计数提示:显示已用/剩余字节(如 “15/64 字节”)
- 详细报错信息:引导用户修正
- 展示有效/无效案例:帮助理解规则
- 安全警告:提醒用户注意不可见字符和方向格式化字符
- 大小写提示:查询时提醒用户名称仅对 ASCII 字母不区分大小写
合约地址铸造提示
- 若调用方为合约地址,该合约必须实现
IERC721Receiver接口,否则铸造会失败(revert)。 - 这是安全设计:防止 NFT 被永久锁在不支持 ERC721 的合约中。
大小写处理示例
// 预览名称会被 normalize 成什么(pure 函数,不消耗 gas)
const normalized = await contract.normalizedNameOf("MyGroup");
// normalized === "mygroup"
// 查询时使用任意大小写都能找到
const tokenId1 = await contract.tokenIdOf("MyGroup");
const tokenId2 = await contract.tokenIdOf("mygroup");
const tokenId3 = await contract.tokenIdOf("MYGROUP");
// tokenId1 === tokenId2 === tokenId3
// 获取原始名称用于显示
const originalName = await contract.groupNameOf(tokenId1);
// originalName === "MyGroup" (用户注册时的格式)
// 检查名称是否可用(不区分大小写)
const isUsed = await contract.isGroupNameUsed("mygroup");
// 如果 "MyGroup" 已注册,返回 true
合约错误提示
| 异常 | 触发原因 |
|---|---|
GroupNameEmpty() |
名称为空字符串 |
InvalidGroupName() |
名称不符合校验规则(长度、空格、控制字符等) |
GroupNameAlreadyExists() |
链群名称已被注册(不区分大小写) |