Day 8

AENS 域名服务

高级阶段 · 预计学习时间 120 分钟

学习目标
  • 理解 AENS 域名系统
  • 掌握域名注册完整流程
  • 熟悉域名状态和生命周期
  • 实现域名指针配置
  • 完成域名转让和撤销
AENS 概述
什么是 AENS

AENS(Aeternity Naming System)是 Aeternity 区块链的域名系统,将人类可读的名称映射到区块链地址,类似于互联网的 DNS 系统。

示例:
alice.chain → ak_2gx9MEFxKvY9vMG5YnqnXWv1hCsX7rgnfvBLJS4aQurustR1rt
mytoken.chain → ct_2AfnEfCSZCTEkxL5Yoi4Yfq6fF7YapHRaFKDJK3THMXMBspp5z
域名命名规则
规则说明
后缀必须以 .chain 结尾
长度最少 1 个字符(不含后缀)
字符小写字母、数字、连字符
限制不能以连字符开头或结尾
✅ 有效示例
alice.chain
my-wallet.chain
token123.chain
❌ 无效示例
Alice.chain (大写)
my_wallet.chain (下划线)
-start.chain (连字符开头)
域名状态与生命周期
域名状态
状态说明可执行操作
AVAILABLE可用,未被注册preclaim
PRECLAIMED已预声明claim
CLAIMED已注册update, transfer, revoke
REVOKED已撤销preclaim(重新注册)
注册流程
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│  AVAILABLE  │────→│  PRECLAIMED │────→│   CLAIMED   │
└─────────────┘     └─────────────┘     └─────────────┘
      ↑               preclaim            claim
      │
      │  ┌─────────────┐
      └──│   REVOKED   │
         └─────────────┘
              revoke
为什么需要两步注册?

防止抢注攻击。Preclaim 提交承诺(commitment),不暴露域名;Claim 揭示域名,完成注册。

费用计算
域名长度费用(约)说明
1 字符~570 AE极高
2 字符~352 AE很高
3 字符~218 AE
4 字符~135 AE中等
5+ 字符~83 AE基础
实践任务
任务 8.1: 检查域名可用性
from aeternity.node import NodeClient, Config
from aeternity import aens

# 连接测试网
config = Config(external_url='https://testnet.aeternity.io')
client = NodeClient(config)

print(f"当前区块高度: {client.get_current_key_block_height()}")

# 检查域名
domains_to_check = [
    "test-domain-12345.chain",
    "aeternity.chain",
    "my-unique-name.chain"
]

for domain in domains_to_check:
    print(f"\n检查域名: {domain}")
    
    try:
        name_info = client.get_name_entry_by_name(domain)
        
        print(f"  状态: 已注册")
        print(f"  所有者: {name_info.owner}")
        print(f"  TTL: 区块 {name_info.ttl}")
        
        if name_info.pointers:
            print(f"  指针:")
            for pointer in name_info.pointers:
                print(f"    {pointer.key}: {pointer.id}")
    except Exception as e:
        if "not found" in str(e).lower() or "404" in str(e):
            print(f"  状态: 可用(未注册)")
        else:
            print(f"  查询失败: {e}")
任务 8.2: 域名注册 - Preclaim
from aeternity.signing import Account
import secrets

# 创建或加载账户(需要有测试币)
account = Account.generate()
print(f"账户地址: {account.get_address()}")

# 生成唯一域名
random_suffix = secrets.token_hex(4)
domain_name = f"mytest-{random_suffix}.chain"
print(f"\n准备注册域名: {domain_name}")

# 生成 commitment(承诺)
salt = aens.generate_commitment_salt()
commitment = aens.compute_commitment(domain_name, salt)

print(f"\nPreclaim 信息:")
print(f"  域名: {domain_name}")
print(f"  Salt: {salt.hex()}")
print(f"  Commitment: {commitment}")

# 发送 Preclaim 交易
try:
    preclaim_tx = client.name_preclaim(
        account=account,
        commitment_id=commitment
    )
    
    print(f"\nPreclaim 交易:")
    print(f"  交易哈希: {preclaim_tx.hash}")
    print(f"  状态: 成功")
    print(f"\n⚠️ 保存以下信息用于 Claim:")
    print(f"  域名: {domain_name}")
    print(f"  Salt: {salt.hex()}")
except Exception as e:
    print(f"\nPreclaim 失败: {e}")
任务 8.3: 域名注册 - Claim
import time

# Preclaim 时保存的信息
domain_name = "mytest-xxxxx.chain"  # 替换为实际域名
salt = bytes.fromhex("xxx...")      # 替换为实际 salt

print(f"准备 Claim 域名: {domain_name}")

# 等待 Preclaim 确认(至少 1 个区块)
print("\n等待 Preclaim 确认...")
time.sleep(5)  # 测试网约 3 秒一个区块

# 发送 Claim 交易
try:
    claim_tx = client.name_claim(
        account=account,
        name=domain_name,
        name_salt=salt
    )
    
    print(f"\nClaim 交易:")
    print(f"  交易哈希: {claim_tx.hash}")
    print(f"  域名 ID: {claim_tx.name_id}")
    print(f"  状态: 成功")
    
    # 查询注册结果
    name_info = client.get_name_entry_by_name(domain_name)
    print(f"\n域名信息:")
    print(f"  所有者: {name_info.owner}")
    print(f"  TTL: 区块 {name_info.ttl}")
except Exception as e:
    print(f"\nClaim 失败: {e}")
任务 8.4: 完整注册流程(一站式)
class AENSManager:
    """AENS 域名管理器"""
    
    def __init__(self, client: NodeClient, account: Account):
        self.client = client
        self.account = account
    
    def is_available(self, name: str) -> bool:
        """检查域名是否可用"""
        try:
            self.client.get_name_entry_by_name(name)
            return False
        except:
            return True
    
    def register(self, name: str, wait_blocks: int = 2) -> dict:
        """注册域名(完整流程)"""
        print(f"开始注册域名: {name}")
        
        if not self.is_available(name):
            raise ValueError(f"域名 {name} 已被注册")
        print("  ✓ 域名可用")
        
        # Step 1: Preclaim
        salt = aens.generate_commitment_salt()
        commitment = aens.compute_commitment(name, salt)
        
        preclaim_tx = self.client.name_preclaim(
            account=self.account,
            commitment_id=commitment
        )
        print(f"  ✓ Preclaim 完成: {preclaim_tx.hash}")
        
        # 等待确认
        print(f"  ⏳ 等待 {wait_blocks} 个区块确认...")
        initial_height = self.client.get_current_key_block_height()
        while True:
            current_height = self.client.get_current_key_block_height()
            if current_height >= initial_height + wait_blocks:
                break
            time.sleep(2)
        
        # Step 2: Claim
        claim_tx = self.client.name_claim(
            account=self.account,
            name=name,
            name_salt=salt
        )
        print(f"  ✓ Claim 完成: {claim_tx.hash}")
        
        name_info = self.client.get_name_entry_by_name(name)
        return {
            "name": name,
            "name_id": claim_tx.name_id,
            "owner": name_info.owner,
            "ttl": name_info.ttl
        }

# 使用示例
manager = AENSManager(client, account)
result = manager.register(f"demo-{secrets.token_hex(4)}.chain")
print(f"\n🎉 域名注册成功!")
print(f"  域名: {result['name']}")
任务 8.5: 更新域名指针
# 准备指针
pointers = [
    {"key": "account_pubkey", "id": account.get_address()},
    # 也可以指向合约
    # {"key": "contract_pubkey", "id": "ct_xxx..."}
]

# 发送更新交易
update_tx = client.name_update(
    account=account,
    name=domain_name,
    pointers=pointers,
    name_ttl=50000,      # 域名 TTL
    client_ttl=500       # 指针缓存 TTL
)

print(f"更新交易: {update_tx.hash}")

# 验证更新
name_info = client.get_name_entry_by_name(domain_name)
print(f"\n更新后的指针:")
for pointer in name_info.pointers:
    print(f"  {pointer.key}: {pointer.id}")
任务 8.6: 域名解析
def resolve_name(name: str, pointer_key: str = "account_pubkey") -> str:
    """解析域名到地址"""
    try:
        name_info = client.get_name_entry_by_name(name)
        
        for pointer in name_info.pointers:
            if pointer.key == pointer_key:
                return pointer.id
        return None
    except Exception as e:
        raise ValueError(f"无法解析域名 {name}: {e}")

# 测试域名解析
domains_to_resolve = ["aeternity.chain", "test.chain"]

for domain in domains_to_resolve:
    try:
        address = resolve_name(domain)
        if address:
            print(f"{domain} → {address}")
        else:
            print(f"{domain} → 无 account_pubkey 指针")
    except ValueError:
        print(f"{domain} → 未注册")
任务 8.7: 转让域名
# 新所有者
new_owner = Account.generate()

# 发送转让交易
transfer_tx = client.name_transfer(
    account=current_owner,
    name=domain_name,
    recipient=new_owner.get_address()
)

print(f"转让交易: {transfer_tx.hash}")

# 验证转让
name_info = client.get_name_entry_by_name(domain_name)
print(f"新所有者: {name_info.owner}")
print(f"转让成功: {name_info.owner == new_owner.get_address()}")
域名指针类型
指针类型说明示例
account_pubkey账户地址ak_xxx...
contract_pubkey合约地址ct_xxx...
oracle_pubkey预言机地址ok_xxx...
channel状态通道ch_xxx...
指针用途
# 转账时使用域名
client.spend(account, "alice.chain", "1AE")  # 而非 ak_xxx...

# 调用合约
manager.at_address("mycontract.chain")  # 而非 ct_xxx...
知识检查点

完成 Day 8 后,你应该能够: