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

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

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

在上一篇文章《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函数再次展示了二进制计算的强大威力。

  1. 整数部分的粗略估计 (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。

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

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

  • 迭代前的准备 - 规格化 **r**

    在进入核心的迭代循环之前,代码执行了一个关键的预处理步骤,其目的是将我们的计算对象 sqrt_price 转换为一个适合迭代算法的初始值 r

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) $ 区间内。这个规格化步骤是后续迭代得以正确进行的前提。

  • 核心迭代 - 逐位提取小数

    现在,r 已经准备就绪,函数进入了一个while循环,这是整个对数魔法的核心:

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;
};

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

  • 核心迭代r = ((r * r) >> 63) 这一行计算了 vr 的平方。当一个Q63.63的数 r 自乘时,其结果会变成一个Q126.126格式的数。通过右移63位,结果被重新调整回Q63.63格式,并代表 $ v_r^{2} $。
  • 检查与提取:数学上,如果 $ \log_2(v_r) = 0.b_1b_2b_3..._2 $(二进制小数),那么 $ \log_2(v_r^{2}) = 2 \times \log_2(v_r) = b_1.b_2b_3..._2 $。因此,我们只需检查$ v_r^{2} $是否大于等于2,就能确定$ \log_2(v_r) $的首位小数 $ b_1 $。代码 f = ((r >> 64) as u8) 巧妙地完成了这个任务。因为 r 代表$ v_r^{2} \times 2^{63} $ ,所以 r 的值会大于等于 $ 2\times 2^{63} = 2^{64} $ 当且仅当 $ v_r^{2} \geq 2 $。这是一个极为高效的比较。
  • 规格化:如果 $ b_1=1 $(即f=1),r = r >> f 会将 r 右移一位,相当于将其代表的数值除以 2,使其重新回到$ [1, 2) $的范围内,为下一次迭代做好准备。

    通过这个循环,函数逐位“提取”出 $ \log_2(v_r) $ 的二进制小数位,并将其记录在log_2_x32中,最终将复杂的对数运算完全转化为了一系列高效的整数乘法和位移。

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

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

  • 换底运算

    首先,代码执行了换底公式中的最后一步:除以 $ \log_2(\sqrt{1.0001}) $。由于除法在链上成本高昂且易于损失精度,这里巧妙地将其转化为乘法。

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

  • tick_low:通过从log_sqrt_10001中减去一个小的修正值再右移64位(即除以$ 2^{64} $并取整),得到了一个向下取整的tick候选。
  • tick_high:通过向log_sqrt_10001中增加一个修正值再右移64位,得到了一个向上取整的tick候选。

    这两个候选值 tick_lowtick_high 通常是相等或相差为1的。现在,终极问题是:哪一个才是真正代表输入sqrt_pricetick?最终的裁定通过一个完美的闭环验证来完成:

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结构是整个函数的点睛之笔,它构成了完美的闭环验证:

  1. 如果 tick_lowtick_high 相等,那么毫无疑问,这就是正确的tick
  2. 如果它们不相等,代码会调用我们在上一篇文章中深入分析过的 get_sqrt_price_at_tick 函数,用 tick_high 这个候选值进行一次正向计算,得到其对应的价格 sqrt_price(tick_high)
  3. 然后,它将 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_pricetick 的转换是绝对精确且可逆的,消除了所有因定点数算术舍入而产生的歧义。

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

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

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

  1. 化繁为简的智慧:他们没有硬碰硬地去模拟一个对数函数,而是回归数学的第一性原理,通过换底公式将复杂问题巧妙地转化为一个更易于处理的二进制对数问题。
  2. 化运算为位移的魔法:他们将对数计算的核心,通过精巧的迭代平方算法,彻底转化为了一系列对于虚拟机而言极为高效的整数乘法和二进制位移操作。这正是“链上思维”的极致体现——在最小的计算单元上追求最高的效率。
  3. 滴水不漏的严谨:在所有计算的终点,他们没有忽视定点数运算固有的舍入误差,而是通过一个最终的闭环验证——调用正向函数进行校准——来确保每一次从价格到刻度的转换都完美无瑕,毫厘不差。

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

参考仓库

<!--StartFragment-->

感谢社区

HOH.png\ 💧  HOH水分子公众号

🌊  HOH水分子X账号

📹  课程B站账号

<!--EndFragment-->

  • 原创
  • 学分: 11
  • 分类: Move
  • 标签:
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
StarryDeserts
StarryDeserts
0xB14f...49CD
江湖只有他的大名,没有他的介绍。