LOVE20 Group

LOVE20链群是基于ERC721标准的NFT资产,每个链群都是独一无二的,具有唯一性和稀缺性。每个链群都有自己的名称和ID,并且可以自由转让和交易。

View on GitHub

链群名称校验规则

概述

LOVE20 Group 系统中的链群名称需要严格校验,以保障安全性、一致性与良好的用户体验。本规则采取了宽松但安全的方案,支持国际化字符,同时避免滥用风险。

重要:链群名称采用大小写不敏感(仅 ASCII)的唯一性检查,即仅将 ASCII 字母 A-Z 视为与 a-z 等价(例如 MyGroupmygroupMYGROUP 视为同一名称,先注册者得)。系统会保留用户注册时的原始大小写用于显示。

注意:系统不做 Unicode casefold,也不做 NFC/NFKC 等 Unicode 规范化;除 ASCII A-Z 外,其他字符按 UTF-8 字节序列精确比较。

校验规则

✅ 允许

  1. 长度:1~64 字节(UTF-8 编码)

    • 注意:单个 Unicode 字符可能占用多个字节
    • 例子:”群”(UTF-8 下占用 3 字节)
  2. 字符类型

    • ✅ 字母:a-zA-Z
    • ✅ 数字:0-9
    • ✅ 特殊符号:所有 ASCII 可打印字符(0x21-0x7E,除空格外),包括 -_.@!#$%^&*
    • ✅ Unicode 字符:中文、日文、韩文、单码点表情符号等
    • ⚠️ 注意:需要零宽连接符(ZWJ,U+200D)的复合 Emoji(如 👨‍👩‍👧‍👦)不被支持,因为 ZWJ 字符已被禁止以防止混淆攻击。仅支持单个码点的表情符号(如 😀、🎉、❤️)
  3. 格式

    • ✅ 大小写混合(注意:唯一性检查仅对 ASCII 字母不区分大小写)
    • ✅ 混用特殊符号

❌ 拒绝

  1. 空名称

    • 报错:GroupNameEmpty()
  2. 超过 64 字节的名称

    • 报错:InvalidGroupName()
  3. 空格字符 (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)
  4. 控制字符 (0x00-0x1F)

    • 包含:换行符(\n)、制表符(\t)、回车符(\r) 等
    • 报错:InvalidGroupName()
  5. C1 控制字符 (0x80-0x9F)

    • Latin-1 补充中的控制字符
    • 报错:InvalidGroupName()
  6. DEL 字符 (0x7F)

    • 报错:InvalidGroupName()
  7. 行分隔符和段落分隔符

    • U+2028: 行分隔符 (Line Separator)
    • U+2029: 段落分隔符 (Paragraph Separator)
    • 报错:InvalidGroupName()
    • 说明:这些字符可能被解析为换行,导致显示异常
  8. 方向格式化字符

    • 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)
  9. 零宽字符(Zero-Width Characters)
    • 零宽空格 (U+200B)
    • 零宽非连接符 (U+200C)
    • 零宽连接符 (U+200D)
    • 左到右标记 (U+200E)
    • 右到左标记 (U+200F)
    • 组合用字形连接符 (U+034F)
    • 零宽不换行空格/BOM (U+FEFF)
    • 词连接符 (U+2060)
    • 软连字符 (U+00AD)
    • 报错:InvalidGroupName()
    • 说明:这些不可见字符常被用于混淆和欺骗攻击,因此被完全禁止
  10. 不可见数学运算符(Invisible Mathematical Operators)
    • U+2061: 函数应用 (Function Application)
    • U+2062: 不可见乘号 (Invisible Times)
    • U+2063: 不可见分隔符 (Invisible Separator)
    • U+2064: 不可见加号 (Invisible Plus)
    • 报错:InvalidGroupName()
    • 说明:这些用于数学排版的不可见运算符在文本中可能造成混淆
  11. 废弃格式化字符(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 编码下的字节数,并非字符数量。即:

实现说明

实际校验由合约内部 _isValidGroupName() 函数实现,其步骤包括:

  1. 校验字节长度为 1~64
  2. 遍历字节过滤 C0 控制字符 (0x00-0x1F) 和 ASCII 空格 (0x20)
  3. 拒绝 DEL 字符 (0x7F)
  4. 检测并拒绝 C1 控制字符 (0x80-0x9F)
  5. 检测并拒绝所有 Unicode 空格字符(U+00A0、U+1680、U+2000-U+200A、U+202F、U+205F、U+3000)
  6. 检测并拒绝行/段落分隔符(U+2028、U+2029)
  7. 检测并拒绝方向格式化字符(U+061C、U+202A-U+202E、U+2066-U+2069)
  8. 检测并拒绝零宽字符(通过匹配特定的 UTF-8 字节序列)
  9. 检测并拒绝不可见数学运算符(U+2061-U+2064)
  10. 检测并拒绝废弃格式化字符(U+206A-U+206F)
  11. 验证 UTF-8 编码有效性(拒绝无效起始字节 0x80-0xC1、0xF5-0xFF,检测过长编码和无效代理对)
  12. 允许所有其他字符(包括多字节 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 标准不允许的码点
不完整的多字节序列 多字节字符的后续字节缺失或格式错误

大小写处理

系统采用双重存储方案实现大小写不敏感的唯一性检查:

  1. _groupNames:存储原始名称(保留用户输入的大小写)
  2. _normalizedNameToTokenId:存储小写版本用于唯一性检查

转换规则

Gas 影响

安全注意事项

规则设计初衷

  1. 拒绝控制字符:防止注入攻击及显示问题(包括 C0、C1 控制字符)
  2. 禁止所有空格:包括 ASCII 空格和所有 Unicode 空格字符,避免混淆、欺骗与输入错误
  3. 拒绝分隔符:防止行/段落分隔符导致的显示异常
  4. 拒绝方向格式化:防止双向文本覆盖攻击和视觉欺骗
  5. 拒绝零宽字符:防止不可见字符造成的混淆和欺骗攻击
  6. 限制长度:减少 GAS 消耗与存储负担
  7. 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 已废弃的格式化字符,虽然已被弃用,但:

潜在边缘情况

  1. 右到左(RTL)字符:允许使用,但显示可能造成困扰

    • 前端应妥善处理 RTL 文字方向
    • 注意:RTL 标记符(U+200E/U+200F)已被禁止
  2. 同形异义攻击(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;
}

用户体验建议

  1. 实时校验:输入时就给出反馈
  2. 字节计数提示:显示已用/剩余字节(如 “15/64 字节”)
  3. 详细报错信息:引导用户修正
  4. 展示有效/无效案例:帮助理解规则
  5. 安全警告:提醒用户注意不可见字符和方向格式化字符
  6. 大小写提示:查询时提醒用户名称仅对 ASCII 字母不区分大小写

合约地址铸造提示

大小写处理示例

// 预览名称会被 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() 链群名称已被注册(不区分大小写)