热点、深度、趋势全掌握,尽在BTC区块圈

深入Sui CLMM:tick_math中的确定性艺术(下)——反向计算Tick的对数魔法

从幂到对数,逆向工程的挑战

在上一篇文章《tick_math中的确定性艺术(上)》中,我们深入剖析了 Cetus CLMM 协议如何通过二进制分解和精巧的定点数“双精度魔法”,将一个离散的tick(价格刻度)确定性地转换为一个精确的sqrt_price(价格的平方根)。其核心是幂运算:

$ sqrt{P} = (1.0001)^{frac{text{tick}}{2}} $

我们看到,get_sqrt_price_at_tick函数通过一系列高效的乘法,在链上完美地实现了这一过程,最终输出一个标准的 Q64.64 格式定点数。

然而,一个完整的价格系统必须是双向的。池子在进行交易互换(Swap)时,价格会发生连续变化。当一个新的sqrt_price产生后,协议需要能准确地反向计算出它落在了哪个tick上,以便知道当前活跃的流动性范围。这就引出了我们的新问题:

给定一个 Q64.64 格式的 **sqrt_price**,我们如何反推出它对应的 **tick**?

从数学上看,这需要求解一个对数方程。对上面的公式两边取以 $ sqrt{1.0001} $ 为底的对数,我们得到:

$ text{tick} = log_sqrt{1.0001}(sqrt{P}) $

这是一个巨大的挑战。如果说在智能合约中实现幂运算已经“价格不菲”,那么直接计算对数,尤其是一个以非整数为底的对数,在Sui Move这样原生不支持浮点数的纯整数环境中,几乎是“天方夜谭”。挑战不仅在于模拟这些复杂数学函数所带来的高昂Gas成本,更在于如何仅通过整数运算(即定点数算术)来实现一个既精确又高效的对数算法,这本身就是一项艰巨的工程。

那么,tick_math模块中的get_tick_at_sqrt_price函数,是如何在没有log函数的蛮荒之地,施展出它的“对数魔法”的呢?本文将带您一探究竟。

get_tick_at_sqrt_price 的三步炼金术

面对看似无解的对数计算,Cetus的工程师们没有退缩,而是像古代的炼金术士一样,通过一系列精妙的步骤,将复杂问题分解、转化,最终“炼”出了我们需要的tick。这个过程可以概括为三步炼金术。

第一步:基底转换 —— 万物归于$ log_2 $

函数收到的输入sqrt_price,是我们已经非常熟悉的Q64.64格式。第一步,也是最关键的一步,是利用对数换底公式,将我们棘手的目标,转换为一个更容易处理的问题:

$ text{tick} = log_sqrt{1.0001}(sqrt{P}) = frac{log_2(sqrt{P})}{log_2(sqrt{1.0001})} $

这个公式告诉我们,只要我们能用某种方法求出任意价格的二进制对数(log2),再除以一个预先计算好的常量$ log_2(sqrt{1.0001}) $,就能得到最终的tick。这使得我们的核心任务从求解一个任意底的对数,简化为了求解一个标准化的二进制对数。

第二步:巧算$ log_2(sqrt{P}) $—— MSB 与迭代平方

现在,问题变成了如何在链上高效地计算一个Q64.64定点数的$ log_2 $值。get_tick_at_sqrt_price函数再次展示了二进制计算的强大威力。

整数部分的粗略估计 (MSB)

代码首先通过一系列的位移和比较,快速地找到了输入sqrt_price的最高有效位(Most Significant Bit, MSB)。

let r = sqrt_price;
let msb = 0;
let f: u8 = as_u8(r >= 0x10000000000000000) << 6; // If r >= 2^64, f = 64 else 0
msb = msb | f;
r = r >> f;
// ... (repeated for 2^32, 2^16, 2^8, etc.)

寻找 MSB 的位置,是计算$ log_2 $整数部分最快的方法。例如,如果一个数在$

2^5, 2^6) $区间内,那么它的$ log_2 $值就在$

5, 6) $区间内,整数部分就是5。msb变量记录的正是这个整数。由于输入是Q64.64格式(即实际值 $ V $ 被存储为整数$ V times 2^{64} $ ),所以代码计算出的msb是针对这个放大后的整数的。要得到原始值$ V $的$ log_2 $的整数部分,就需要从msb中减去64。

小数部分的精确求解 (迭代平方)

在求解了对数的整数部分后,我们需要进一步精确计算其小数部分。迭代平方算法是实现这一目标的关键,但它要求输入值必须在一个特定的标准化区间内。

r = if (msb >= 64) {
    sqrt_price >> (msb - 63)
} else {
    sqrt_price << (63 - msb)
};

此操作的核心目的,是将原始价格 V(sqrt_price所代表的真实值)的小数部分,即 $ frac{V}{2^{lfloor log_2(V) rfloor}} $,重新表示为一个在特定定点数格式下,值在$

1, 2) $区间内的数。 具体来说,这段代码将 sqrt_price(一个Q64.64数)转换为了一个新的数 r,这个 r 是一个Q63.63格式的定点数,它所代表的真实值 vr 被精确地缩放到了 $

1, 2) $ 区间内。这个规格化步骤是后续迭代得以正确进行的前提。

let shift = 31;
while (shift >= 18) {
    r = ((r * r) >> 63); // 核心迭代
    f = ((r >> 64) as u8); // 检查结果是否 >= 2
    log_2_x32 = i128::or(log_2_x32, i128::shl(i128::from((f as u128)), shift));
    r = r >> f; // 如果 r >= 2, 将其除以2,重新规格化
    shift = shift - 1;
};

这个循环是定点数算术的典范

第三步:终点校准 —— 确保完美逆反

经过前两步,我们已经有了一个高精度的$ log_2(sqrt{P}) $值,存储在log_2_x32中。现在,我们需要完成从的$ log_2(sqrt{P}) $到最终tick的转换,并处理定点数运算中固有的舍入误差。

let log_sqrt_10001 = i128::mul(log_2_x32, i128::from(59543866431366u128));

log_2_x32 是我们计算出的 $ log_2(sqrt{P}) $的高精度定点数表示。常量 59543866431366u128 则是预先计算好的 $ frac1{log_2(sqrt{P})} $ 的高精度定点数表示。通过一次乘法,log_sqrt_10001 就得到了 $ log_{sqrt{1.0001}}(sqrt{P}) $的高精度定点数结果,这在数学上等价于我们的目标 tick 值。

然而,log_sqrt_10001 仍然是一个包含大量小数位的高精度定点数。当我们将它转换为一个整数tick时,简单的截断(向下取整)可能会因为之前累积的微小舍入误差而导致“差一错误”(off-by-one error)。为了得到绝对正确的tick,函数采用了一种“候选与验证”的策略。

首先,代码通过添加或减去一个微小的修正值,计算出两个最有可能的tick候选者:

let tick_low = i128::as_i32(i128::shr(i128::sub(log_sqrt_10001, i128::from(184467440737095516u128)), 64));
let tick_high = i128::as_i32(i128::shr(i128::add(log_sqrt_10001, i128::from(15793534762490258745u128)), 64));

这两行代码计算出了两个候选tick:

if (i32::eq(tick_low, tick_high)) {
    return tick_low
} else if (get_sqrt_price_at_tick(tick_high) <= sqrt_price) {
    return tick_high
} else {
    return tick_low
}

这最后的if-else结构是整个函数的点睛之笔,它构成了完美的闭环验证:

如果 tick_low 和 tick_high 相等,那么毫无疑问,这就是正确的tick。如果它们不相等,代码会调用我们在上一篇文章中深入分析过的 get_sqrt_price_at_tick 函数,用 tick_high 这个候选值进行一次正向计算,得到其对应的价格 sqrt_price(tick_high)。

然后,它将 sqrt_price(tick_high) 与我们最初的输入 sqrt_price 进行比较。如果 sqrt_price(tick_high) 小于或等于我们输入的 sqrt_price,这意味着 tick_high 并没有“越界”,它就是正确的tick。反之,如果 sqrt_price(tick_high) 已经超过了输入的 sqrt_price,那就说明正确的 tick 应该是更小的那个,即 tick_low。

通过这种方式,函数确保了从 sqrt_price 到 tick 的转换是绝对精确且可逆的,消除了所有因定点数算术舍入而产生的歧义。

结论:确定性的炼金术与互为镜像的数学艺术

如果说 get_sqrt_price_at_tick 是一门“指数增长的艺术”,那么 get_tick_at_sqrt_price 就是一门“对数收敛的艺术”。两者互为镜像,共同构成了 Cetus CLMM 协议中价格与刻度之间精确、稳固的数学桥梁。

get_tick_at_sqrt_price 的实现,是智能合约工程领域一项堪称典范的杰作。面对在纯整数环境中计算对数这一看似不可能的任务,Cetus 的工程师们没有选择妥协或寻求近似的捷径。相反,他们向我们展示了一场精彩的“确定性炼金术”:

化繁为简的智慧:他们没有硬碰硬地去模拟一个对数函数,而是回归数学的第一性原理,通过换底公式将复杂问题巧妙地转化为一个更易于处理的二进制对数问题。化运算为位移的魔法:他们将对数计算的核心,通过精巧的迭代平方算法,彻底转化为了一系列对于虚拟机而言极为高效的整数乘法和二进制位移操作。这正是“链上思维”的极致体现——在最小的计算单元上追求最高的效率。

滴水不漏的严谨:在所有计算的终点,他们没有忽视定点数运算固有的舍入误差,而是通过一个最终的闭环验证——调用正向函数进行校准——来确保每一次从价格到刻度的转换都完美无瑕,毫厘不差。

这不仅仅是代码,它更是一种在严苛的限制(无浮点数、Gas成本)下,追求数学确定性与工程优雅性的艺术。它告诉我们,最卓越的解决方案,往往不是依赖于更强大的工具,而是源于对基本规则最深刻的理解和最富创造力的应用。

参考仓库感谢社区

HOH.png

  HOH水分子公众号

使用本文
0
共享
上一篇

SEC降低对加密货币监管的合法性,监管机构发出警告

下一篇

如果 XRP 即将达到 8,000 美元并取代 SWIFT,Ripple 为什么不采用 XRP 储备策略:Gary Cardone

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

阅读下一页