Skip to content

Web3 空投防女巫:用 IP 归属地聚类识别批量领号(Node.js 实现) ​

做过空投的项目方都懂这幕:活动规则写「按钱包地址数分配」,结果领奖名单里冒出几百个钱包,创建时间差几秒、交互行为完全同步,背后的操作者可能就坐在同一间工作室里。Chainalysis 的公开报告里有个常被引用的数字:女巫账号能吃掉空投份额的 1% 到 10%,对几百万美元的空投来说,这就是几万到几十万美元,白送给撸毛工作室。这篇文章讲项目方侧怎么用 IP 归属地把「批量领号」的账号簇圈出来,代码用 Node.js 写,跑一遍就能看到聚类结果。

一、女巫怎么混进空投 ​

女巫攻击(Sybil Attack)在空投场景里就是「一个人伪装成一群人」。撸毛工作室的手法高度套路化:

  • 批量创建钱包:助记词生成器一次跑几万个地址,钱包创建时间高度聚集。
  • 共享网络出口:几百个钱包挂同一个代理节点、同一台云服务器或同一家机房,行为上「同进同出」。
  • 交互脚本化:跨链桥、DEX 交易、NFT 铸造,同一批钱包的操作间隔、Gas 设置几乎一致。

链上数据能查钱包,但「这批钱包背后是不是同一个人」光看链上很难定论。IP 归属地是线下证据里最便宜的一块:它把网络出口映射到省市和运营商,同一工作室、同一代理池的请求自然会聚到同一个或相邻的标签上。

二、方案:报名时打属地标签,再按标签聚簇 ​

空投一般分报名、交互、领奖三个阶段,最晚在领奖前要完成筛查。实用做法是在钱包发起链下签名或领取接口时,把来源 IP 记下来:

  1. 拿真实客户端 IP(注意 CDN 后的 XFF 头处理),调归属地接口打成「省-市-运营商」标签;
  2. 按标签把钱包分组;
  3. 组内钱包数超阈值(比如同一个省-市-运营商标签下超过 20 个钱包)→ 标记为可疑簇,进人工或链上二次验证队列。

IP 归属地只负责「圈」,不负责「定案」。同标签不等于同一个人——同一栋写字楼、同一家网吧、同一个学校出口,天然共享 IP 段。所以可疑簇出来之后,要叠链上特征(钱包创建时间、首笔资金来源、交互路径相似度)做精判。

三、Node.js 实现 ​

为什么用 Node.js:空投 DApp 的前后端很多就是 TS/JS 一套,领取接口顺手就能加这个检查,不用另起技术栈。下面是完整可运行示例:读入钱包 IP 清单 → 逐个查归属地(带缓存和限速)→ 按「省-市-运营商」聚簇 → 输出可疑钱包组。归属地用 IP9 免费接口 https://ip9.com.cn/get?ip=<ip>,无需注册,60 次/分钟/IP 的额度配合缓存完全够用。

javascript
// anti-sybil.mjs — Node 18+ 直接运行
import { readFileSync } from 'node:fs'

const GEO_API = 'https://ip9.com.cn/get?ip='
const cache = new Map() // ip -> {region, isp}

// 归属地查询:带缓存 + 简单限速(免费接口 60 次/分钟,逐个 IP 串行足够)
async function lookupGeo(ip) {
  if (cache.has(ip)) return cache.get(ip)
  const resp = await fetch(GEO_API + encodeURIComponent(ip), {
    headers: { 'User-Agent': 'airdrop-anti-sybil/1.0' },
  })
  const json = await resp.json()
  if (json.ret !== 200) {
    // 非法 IP 或限速时降级:不参与聚类,不直接报错中断
    const fallback = { region: 'unknown', isp: 'unknown' }
    cache.set(ip, fallback)
    return fallback
  }
  const d = json.data
  const info = { region: `${d.prov}-${d.city}`, isp: d.isp }
  cache.set(ip, info)
  return info
}

// 主流程:读 wallets.jsonl(每行 {wallet, ip}),聚簇输出可疑组
const wallets = readFileSync('./wallets.jsonl', 'utf8')
  .trim().split('\n').map((line) => JSON.parse(line))

const groups = new Map() // region -> [{wallet, ip, isp}]
for (const w of wallets) {
  const geo = await lookupGeo(w.ip)
  if (!groups.has(geo.region)) groups.set(geo.region, [])
  groups.get(geo.region).push({ wallet: w.wallet, ip: w.ip, isp: geo.isp })
}

const THRESHOLD = 20
let suspicious = 0
for (const [region, members] of groups) {
  if (members.length < THRESHOLD) continue
  suspicious += members.length
  console.log(`[可疑] ${region} 共 ${members.length} 个钱包(ISP: ${members[0].isp})`)
  console.log('  ' + members.slice(0, 8).map((m) => m.wallet).join(', ') + ' ...')
}
console.log(`\n总钱包 ${wallets.length},可疑簇覆盖 ${suspicious} 个(${(suspicious / wallets.length * 100).toFixed(1)}%)`)

运行:准备一份 wallets.jsonl(每行一个 {"wallet":"0x...","ip":"1.2.3.4"}),然后 node anti-sybil.mjs。脚本会把同一省-市-运营商的 20 个以上钱包组标出来。

两个可以马上改进的点:一是免费接口只有运营商文本,isp 里出现「阿里云」「腾讯云」「数据中心」这类字样的,是机房代理的弱信号,可单独加权重;要精确区分住宅/企业/机房,用 IP9 的 VIP 版,ip_type 字段直接给出 ISP 家庭 / BUS 企业 / IDC 机房。二是阈值别写死,先跑一周看分布,按真实数据的分位数定。

四、落地注意事项 ​

  • 误伤是常态,别一发现就封。同一办公楼、同一学校、同一网吧聚簇很正常。正确姿势:可疑簇先标记,叠链上特征再裁决;实在要限制,先限制领奖资格、给申诉通道,别动钱包资产。
  • 代理池一换,规则就失效。撸毛工作室会买新代理池,同簇规则只能抓「偷懒的对手」。要抓换池的,得靠链上恒定特征(资金归集地址、创建时间聚集)兜底。
  • 机房和代理解析的边界。免费接口能给省市/运营商;IP 类型和 ASN 属于 VIP 能力,写方案时按这个边界来,别承诺免费版给不了的东西。
  • 隐私合规:记录申请 IP 属于个人信息处理,活动页面要写清楚收集用途,数据保留期限也要定。

总结 ​

空投防女巫没有银弹,但 IP 归属地聚簇是成本最低的第一道筛子:报名接口顺手记个 IP,Node 脚本几分钟就能把「同省同市同一运营商下挤着几十个钱包」的簇捞出来,再把簇交给链上验证。归属地数据用 IP9 免费接口 https://ip9.com.cn/get?ip=xxx 就行,无需注册、60 次/分钟,加个缓存基本零成本。先圈簇、再精判,比发币后追讨现实得多。官网:https://www.ip9.com.cn 。