Day 9

预言机系统

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

学习目标
  • 理解区块链预言机的作用
  • 掌握预言机完整生命周期
  • 实现预言机注册和扩展
  • 发送和响应预言机查询
  • 设计链上链下数据交互
预言机概述
什么是预言机

预言机(Oracle)是区块链与外部世界之间的桥梁,负责将链外数据安全地传输到链上。

为什么需要预言机?

区块链是封闭系统,无法直接访问外部数据。智能合约需要外部数据做决策(如价格、天气、比赛结果),预言机提供可信的数据输入机制。

应用场景
场景数据类型示例
DeFi价格数据ETH/USD 汇率
保险事件数据航班延误、自然灾害
博彩结果数据比赛分数、选举结果
供应链追踪数据物流状态、温度记录
IoT传感器数据环境监测、设备状态
Aeternity 预言机特点
  • 原生支持:预言机作为一级公民内置于协议
  • 专用交易类型:独立的交易类型和状态管理
  • 简单高效:无需复杂的合约实现
                      区块链
  ┌───────────────────────────────────────────────┐
  │  ┌───────────┐   ┌───────────┐   ┌───────────┐ │
  │  │  预言机A  │   │  预言机B  │   │  预言机C  │ │
  │  │  (价格)   │   │  (天气)   │   │  (体育)   │ │
  │  └─────┬─────┘   └─────┬─────┘   └─────┬─────┘ │
  │        │               │               │        │
  │  ┌─────┴───────────────┴───────────────┴─────┐ │
  │  │              智能合约层                    │ │
  │  └───────────────────────────────────────────┘ │
  └───────────────────────────────────────────────┘
           ↑                ↑                ↑
    ┌──────┴──────┐  ┌──────┴──────┐  ┌──────┴──────┐
    │  价格 API   │  │  天气 API   │  │  体育 API   │
    └─────────────┘  └─────────────┘  └─────────────┘
预言机生命周期
状态转换
┌─────────┐  register  ┌──────────┐  extend  ┌──────────┐
│ 不存在  │──────────→│   活跃   │←────────→│   活跃   │
└─────────┘           └────┬─────┘          └──────────┘
                           │
                           │ TTL 到期
                           ↓
                      ┌──────────┐
                      │  过期    │
                      └──────────┘
核心操作
操作说明
Register将账户注册为预言机,定义查询/响应格式和费用
Extend延长预言机有效期
Query任何人可向预言机发送查询,支付查询费用
Respond预言机所有者回复查询,获得费用
费用结构
操作费用类型支付方接收方
注册交易费预言机矿工
查询交易费 + 查询费查询者矿工 + 预言机
响应交易费预言机矿工
扩展交易费预言机矿工
实践任务
任务 9.1: 注册预言机
from aeternity.node import NodeClient, Config
from aeternity.signing import Account
from aeternity import oracles

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

# 创建预言机账户(需要有测试币)
oracle_account = Account.generate()
print(f"预言机账户: {oracle_account.get_address()}")

# 定义预言机参数
query_format = "string"      # 查询格式
response_format = "string"   # 响应格式
query_fee = 100000           # 查询费用 (aettos)
oracle_ttl = 1000            # 预言机 TTL (区块数)

print(f"\n预言机配置:")
print(f"  查询格式: {query_format}")
print(f"  响应格式: {response_format}")
print(f"  查询费用: {query_fee} aettos")
print(f"  有效期: {oracle_ttl} 区块")

# 注册预言机
try:
    register_tx = client.oracle_register(
        account=oracle_account,
        query_format=query_format,
        response_format=response_format,
        query_fee=query_fee,
        oracle_ttl=oracle_ttl
    )
    
    print(f"\n注册成功:")
    print(f"  交易哈希: {register_tx.hash}")
    print(f"  预言机 ID: {register_tx.oracle_id}")
    
    # 预言机 ID 格式: ok_{account_pubkey}
    oracle_id = f"ok_{oracle_account.get_address()[3:]}"
    print(f"  预言机地址: {oracle_id}")
except Exception as e:
    print(f"\n注册失败: {e}")
任务 9.2: 查询预言机状态
# 预言机 ID(替换为实际 ID)
oracle_id = "ok_xxx..."

print(f"查询预言机: {oracle_id}")

try:
    oracle_info = client.get_oracle_by_pubkey(oracle_id)
    current_height = client.get_current_key_block_height()
    
    print(f"\n预言机信息:")
    print(f"  ID: {oracle_info.id}")
    print(f"  查询格式: {oracle_info.query_format}")
    print(f"  响应格式: {oracle_info.response_format}")
    print(f"  查询费用: {oracle_info.query_fee} aettos")
    print(f"  TTL: 区块 {oracle_info.ttl}")
    
    remaining = oracle_info.ttl - current_height
    if remaining > 0:
        print(f"  剩余有效期: {remaining} 区块")
        print(f"  状态: ✅ 活跃")
    else:
        print(f"  状态: ❌ 已过期")
except Exception as e:
    print(f"\n查询失败: {e}")
任务 9.3: 发送预言机查询
# 查询者账户
querier = Account.generate()

# 目标预言机
oracle_id = "ok_xxx..."

# 获取预言机信息
oracle_info = client.get_oracle_by_pubkey(oracle_id)
print(f"预言机查询费用: {oracle_info.query_fee} aettos")

# 构建查询
query_data = "ETH/USD"  # 查询内容
query_fee = oracle_info.query_fee
query_ttl = 50   # 查询有效期
response_ttl = 50  # 响应等待期

print(f"\n发送查询:")
print(f"  内容: {query_data}")
print(f"  费用: {query_fee} aettos")

try:
    query_tx = client.oracle_query(
        account=querier,
        oracle_id=oracle_id,
        query=query_data,
        query_fee=query_fee,
        query_ttl=query_ttl,
        response_ttl=response_ttl
    )
    
    print(f"\n查询成功:")
    print(f"  交易哈希: {query_tx.hash}")
    print(f"  查询 ID: {query_tx.query_id}")
except Exception as e:
    print(f"\n查询失败: {e}")
任务 9.4: 响应预言机查询
import time

# 预言机账户
oracle_account = Account.from_secret_key_string("你的私钥")
oracle_id = f"ok_{oracle_account.get_address()[3:]}"

# 获取待处理的查询
def get_pending_queries(client, oracle_id):
    """获取预言机的待处理查询"""
    queries = []
    try:
        result = client.get_oracle_queries_by_pubkey(oracle_id)
        for query in result.oracle_queries:
            if not query.response:  # 未响应的查询
                queries.append(query)
    except Exception as e:
        print(f"获取查询失败: {e}")
    return queries

# 检查待处理查询
pending = get_pending_queries(client, oracle_id)
print(f"待处理查询: {len(pending)} 个")

for query in pending:
    print(f"\n查询详情:")
    print(f"  查询 ID: {query.id}")
    print(f"  内容: {query.query}")
    print(f"  费用: {query.fee} aettos")
    
    # 处理查询(模拟外部数据获取)
    query_content = query.query
    if "ETH/USD" in query_content:
        response_data = "1850.00"
    elif "BTC/USD" in query_content:
        response_data = "43500.00"
    else:
        response_data = "UNKNOWN"
    
    # 发送响应
    try:
        response_tx = client.oracle_respond(
            account=oracle_account,
            oracle_id=oracle_id,
            query_id=query.id,
            response=response_data
        )
        print(f"  响应: {response_data}")
        print(f"  交易哈希: {response_tx.hash}")
    except Exception as e:
        print(f"  响应失败: {e}")
任务 9.5: 完整预言机服务
class PriceOracle:
    """价格预言机服务"""
    
    def __init__(self, client: NodeClient, account: Account):
        self.client = client
        self.account = account
        self.oracle_id = None
        
    def register(self, query_fee=100000, oracle_ttl=5000):
        """注册预言机"""
        print("注册价格预言机...")
        
        tx = self.client.oracle_register(
            account=self.account,
            query_format="string",
            response_format="string",
            query_fee=query_fee,
            oracle_ttl=oracle_ttl
        )
        
        self.oracle_id = tx.oracle_id
        print(f"注册成功: {self.oracle_id}")
        return self.oracle_id
    
    def get_price(self, pair: str) -> str:
        """获取价格数据(模拟外部 API)"""
        mock_prices = {
            "ETH/USD": "1850.00",
            "BTC/USD": "43500.00",
            "AE/USD": "0.085",
        }
        return mock_prices.get(pair, "N/A")
    
    def process_queries(self):
        """处理所有待处理查询"""
        if not self.oracle_id:
            print("预言机未注册")
            return
        
        result = self.client.get_oracle_queries_by_pubkey(self.oracle_id)
        pending = [q for q in result.oracle_queries if not q.response]
        
        print(f"待处理查询: {len(pending)} 个")
        
        for query in pending:
            pair = query.query
            price = self.get_price(pair)
            
            print(f"  处理: {pair} → {price}")
            
            self.client.oracle_respond(
                account=self.account,
                oracle_id=self.oracle_id,
                query_id=query.id,
                response=price
            )
    
    def run_service(self, poll_interval=10):
        """运行预言机服务"""
        print(f"\n启动预言机服务...")
        print(f"预言机 ID: {self.oracle_id}")
        print("按 Ctrl+C 停止\n")
        
        try:
            while True:
                self.process_queries()
                time.sleep(poll_interval)
        except KeyboardInterrupt:
            print("\n服务已停止")

# 使用示例
oracle = PriceOracle(client, oracle_account)
oracle.register(query_fee=50000, oracle_ttl=10000)
oracle.run_service(poll_interval=15)
智能合约中使用预言机
Sophia 合约示例
@compiler >= 6

contract OracleConsumer =

    // 预言机相关类型
    type oracle_id = oracle(string, string)
    type query_id = oracle_query(string, string)
    
    // 合约状态
    record state = {
        owner       : address,
        oracle      : option(oracle_id),
        last_price  : option(string),
        last_query  : option(query_id)
    }
    
    // 设置预言机
    stateful entrypoint set_oracle(oracle_addr : oracle_id) =
        require(Call.caller == state.owner, "Only owner")
        put(state{ oracle = Some(oracle_addr) })
    
    // 查询价格
    stateful payable entrypoint query_price(pair : string) : query_id =
        require(is_some(state.oracle), "Oracle not set")
        
        let oracle = Option.force(state.oracle)
        let fee = Oracle.query_fee(oracle)
        require(Call.value >= fee, "Insufficient query fee")
        
        // 发送查询
        let q = Oracle.query(
            oracle,
            pair,
            fee,
            RelativeTTL(50),   // 查询 TTL
            RelativeTTL(50)    // 响应 TTL
        )
        
        put(state{ last_query = Some(q) })
        q
    
    // 获取响应
    stateful entrypoint get_response() : option(string) =
        require(is_some(state.oracle), "Oracle not set")
        require(is_some(state.last_query), "No pending query")
        
        let oracle = Option.force(state.oracle)
        let query = Option.force(state.last_query)
        
        switch(Oracle.get_answer(oracle, query))
            None => None
            Some(response) =>
                put(state{ last_price = Some(response) })
                Some(response)
知识检查点

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