阅读视图

发现新文章,点击刷新页面。
🔲 ☆

谁才是 Go 生态的“幕后之王”?—— 深度挖掘 4000 万个节点后的惊人发现

本文永久链接 – https://tonybai.com/2026/01/09/the-most-popular-go-dependency-is

大家好,我是Tony Bai。

在 Go 的世界里,我们每天都在引入各种 import。但你是否想过,整个 Go 生态系统中,究竟哪个包是被依赖次数最多的“基石”?

通常,我们会参考 GitHub Stars 或 Awesome 列表,但这往往带有主观偏差。为了寻找最客观的答案,开发者 Thibaut Rousseau 做了一件疯狂的事他下载了 Go Proxy 自 2019 年以来的所有模块元数据,构建了一个包含 4000 万个节点、4 亿条关系的巨大图谱。

结果令人大开眼界。

img{512x368}

从“愚公移山”到“巧用代理”

Thibaut 最初的想法很直接:从一个种子项目列表开始,递归地克隆仓库、解析 go.mod。但他很快发现这条路行不通——克隆速度太慢,且严重依赖 GitHub。

于是,他将目光转向了 Go Modules 生态系统的核心枢纽 —— Go Proxy

  • index.golang.org:提供了自 2019 年以来所有发布模块的时间流。
  • proxy.golang.org:提供了每个模块版本的 go.mod 文件。

通过这两个公开 API,他成功地将整个 Go 生态的元数据“搬”到了本地,构建了一个全量的、不可变的本地缓存。

Neo4j:点亮数据之网

面对海量的依赖关系,传统的关系型数据库显得力不从心。Thibaut 选择了图数据库 Neo4j

  • 节点 (Node):代表一个具体的 Go 模块版本(例如 github.com/gin-gonic/gin@v1.9.0)。
  • 关系 (Relationship):代表 DEPENDS_ON(依赖于)。

通过简单的 Cypher 查询语句,复杂的依赖链变得清晰可见。例如,查询一个模块的所有传递性依赖(Transitive Dependencies),在 SQL 中可能需要复杂的递归 CTE,而在 Neo4j 中只需一个简单的 *1.. 语法即可搞定。

数据揭秘:Go 生态的真实面貌

经过数天的处理和导入,这个庞大的图谱终于呈现在眼前。让我们看看数据告诉了我们什么:

1. 绝对的王者:testify

在“被直接依赖次数”的榜单上,github.com/stretchr/testify 以 259,237 次的惊人数量遥遥领先,是第二名的两倍还多。这再次印证了测试在 Go 社区中的核心地位。

紧随其后的是:

  1. github.com/google/uuid (10w+)
  2. golang.org/x/crypto (10w+)
  3. google.golang.org/grpc (9.7w+)
  4. github.com/spf13/cobra (9.3w+)
  5. … …

2. “已归档”库的生命力:pkg/errors

最令人玩味的数据来自 github.com/pkg/errors。尽管这个库多年前就已宣布“归档”(Archived)并停止维护,且 Go 1.13 已内置了类似的错误包装功能,但数据却显示了截然相反的趋势:

  • 它的使用量不降反升!
  • 2019 年仅有 3 个依赖它的模块,而到了 2025 年,这个数字飙升到了 16,001

这揭示了软件生态中一个残酷的现实:旧习惯难改,且“足够好”的库拥有极其顽强的生命力。 哪怕官方已经提供了替代方案,开发者们依然倾向于使用他们熟悉的工具。

小结

Thibaut 的这个项目不仅仅是一次有趣的数据分析,它为我们观察 Go 生态提供了一个全新的上帝视角。

  • 平均依赖数:Go 模块平均拥有 10 个直接依赖。
  • 数据开源:作者不仅开源了爬虫代码 github.com/Thiht/go-stats,还大方地通过 BitTorrent 分享了 11GB 的 Neo4j 数据库转储文件。

你可以下载这份数据,自己在本地运行 Neo4j,去挖掘更多有趣的洞见。比如,看看你最喜欢的某个小众库,究竟被谁在使用?或者,去探索一下 Go 生态中那些隐秘的“单点故障”?

在这个由 4000 万个节点构成的宇宙中,还有无数的秘密等待被发现。

资料链接:https://blog.thibaut-rousseau.com/blog/the-most-popular-go-dependency-is/


你的依赖清单

testify 的霸榜并不意外,但 pkg/errors 的顽强生命力确实让人深思。在你的 go.mod 中,是否也有那些“虽然已归档,但真的很好用”的库?或者,你有什么私藏的冷门好库推荐?

欢迎在评论区晒出你的“宝藏依赖”! 让我们一起发现更多 Go 生态的秘密。

如果这篇文章让你对 Go 生态有了全新的认识,别忘了点个【赞】和【在看】,并转发给你的 Gopher 朋友!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ⭐

在加密货币市场中如何价值投资

加密货币市场是一个充满高波动性和不确定性的领域,但即便如此,许多投资者仍然在寻找价值投资的机会。

价值投资指的是寻找那些市场价格低于其实际价值的资产进行投资。

通过基本面分析进行价值投资,我们只需要关注两个要点:

估值

流通市值 Market Cap

我们在股票或加密货币市场上普遍意义中讨论的市值和大部分行情软件、网站中的所谓市值,指的都是流通市值。

流通市值是某一代币的短期估值或实时估值的量化。

流通市值 = 流通供应量 * 当前价格

(全流通市值)总市值 Fully Diluted Market Cap/Valuation

总市值是指如果所有的代币 100% 流通后,该加密货币的市场总价值。

加密货币和股票类似,根据其代币模型的不同,存在着不同比例的锁仓、质押等情况,因此其流通市值与总市值大概率不等。

全流通市值是一个理论值,因为并不是所有的代币都已经或将会被发行。(例如交易所代币回购销毁或者人为修改上限)

然而,它仍然是一个可靠的指标,用以评估加密货币的长期潜力。

更有用的是,我们可以通过总市值的排行榜进行横向对比,以确定一个代币的价格是否被高估或低估。

总市值 = 总供应量 * 当前价格

我们也可以通过流通市值与总市值的比率计算出其流通率:

流通率 = Market Cap / FDV

低流通率的代币在解锁前后容易出现暴涨暴跌,这就是所谓的解锁牛。(例如 LINK AVAX 这类还有 50% 以上锁仓的代币,有很强的拉升出货意愿)

高流通率,尤其是 100% 流通的代币,我们称之为去中心化代币,这类代币由于已经完全派发给韭菜,一般情况下会跑输大盘。(例如上一轮牛市的表现抢眼的 MATIC,这一轮明显落后于大盘)

投资逻辑

买入同类、同板块代币中被低估的代币、卖出被高估的代币,即买入同赛道、同级别项目中总市值较低的,例如 ARB 和 OP 之间要选 OP

市值相近、赛道接近的币种,选择锁仓量更高的,而不是已经完全去中心化的(反面教材 MATIC UNI EOS)

项目热度

梅特卡夫定律

梅特卡夫定律是一个用来估计网络价值的原理,它指出网络的价值是网络中可交互用户数的平方。

在加密货币领域,这个定律可以用来估计一个项目的潜在价值,特别是那些依赖网络效应的项目,例如社交网络平台和交易平台。

用户数是衡量一个项目热度的直观指标。一个拥有大量活跃用户的项目通常被认为具有更高的价值,因为这意味着更强的用户基础和更高的网络效应。

Meme 币 社区人数

Meme 币通常依赖其社区的热情和参与度。社区人数的增长可以作为这类币种潜在价值的一个指标。

GameFi 玩家数

GameFi,结合了加密货币和区块链游戏。游戏玩家数的增长表明了该 GameFi 项目的吸引力和潜在的盈利能力。

Defi 项目或 Layer2 项目 TVL

TVL(Total Value Locked,总锁定价值)是衡量 Defi 项目或 Layer2 项目健康状况的关键指标。

TVL 表示在 Defi 协议中锁定的资金总额,是衡量项目流动性和用户信任度的重要指标。

交易所平台币 交易量

交易所总交易量反映了交易平台的活跃度。一个高交易量的平台通常意味着该平台的平台币价值较高。

投资逻辑

追逐未来的热点!

追逐热点永远是没有错的,但是一定要掌握时机,要争取做早期的投资者,而不是当后来者接盘。

热点是变幻的、流动的、时过境迁的。

例如当我们看到币安日渐式微,OKX祭出 500 人豪华团队 All in web3 的时候,就能够判断出交易所赛道未来的趋势。

同样的逻辑可以套用在 MEME 币领域,当 PEPE/ORDI 等新型 MEME 币层出不穷的时候,我们也应该注意到 DOGE 的地位会被挑战。

一个聪明的投机者应该随机应变、见风使舵,而不是坚守信仰持仓待涨。

参考资料

🔲 ☆

Gminer v3.41版本更新

  • 支持在非 ZMP 协议上的Epoch #1 进行 Zil 挖矿。

supports Zil mining on epoch #1 for non ZMP protocol

  • 显著提高IronFish算力水平(超过10%)

significant hashrate improvements on IronFish algorithm (up to +10%)

  • 修复了当 ZIL 双挖强度为 -1(-zildi -1)时, IronFish+ZIL、ETC+IronFish+ZIL、Ergo+IronFish+ZIL 和 Conflux+IronFish+ZIL 上挖矿程序崩溃的问题。

fixed miner crashes on IronFish+ZIL, ETC+IronFish+ZIL, Ergo+IronFish+ZIL and Conflux+IronFish+ZIL when ZIL dual intensity is -1 (-zildi -1)

  • 添加了适用于 Nvidia GPU 的 IronFish、IronFish+ZIL 支持
  • 添加了适用于 Nvidia GPU 的 Ergo+IronFish、Ergo+IronFish+ZIL 支持
  • 添加了适用于 Nvidia GPU 的 ETC+IronFish、ETC+IronFish+ZIL 支持
  • 添加了适用于 Nvidia GPU 的 Conflux+IronFish、Conflux+IronFish+ZIL 支持
  • 修复了 Radiant 的份额难度计算问题。

更新时间:2023/07/07

mmpOS 更新命令:

cd /tmp && wget https://github.com/develsoftware/GMinerRelease/releases/download/3.41/gminer_3_41_linux64.tar.xz && tar -xvf gminer_3_41_linux64.tar.xz && cd /opt/mmp/miners/gminer && agent-stop && cp /tmp/miner . && agent-start

Hive OS更新命令:

cd /tmp && wget https://github.com/develsoftware/GMinerRelease/releases/download/3.41/gminer_3_41_linux64.tar.xz && tar -xvf gminer_3_41_linux64.tar.xz && cd /hive/miners/gminer && miner stop && cp /tmp/miner $(ls -d1 */ | tail -1)/gminer && miner start

Gminer v3.41版本更新最先出现在Moby

🔲 ⭐

以太坊(ETH)上海升级是什么意思?

以太坊(Ethereum)是一个开源的区块链平台,它允许开发者构建去中心化应用程序(DApps)和智能合约(Smart Contract)。以太坊目前使用权益证明(PoS)共识机制。与PoW共识机制不同,PoS允许用户暂时锁定32个ETH成为验证节点,而不需要昂贵的挖矿设备。这将使更多的人能够参与到以太坊网络中,从而提高其去中心化程度和安全性。

上海升级(Shanghai Upgrade)背景

去年以太坊社群经过数年的开发和测试成功实施了重大升级,即以太坊合并。在2020年,以太坊推出采用权益证明共识机制的信标链(Beacon Chain),开始与以太坊主链并行运作。权益证明共识机制(Proof of Stake)是指通过让网络节点锁定加密货币的方式来替代矿工挖矿,从而维持区块链网络正常运作和处理交易。这种机制消除了挖矿所需的大量能源和成本,旨在将以太坊的能源消耗降低99.9%以上。

信标链成功运作一段时间后,以太坊区块链顺利合并。简单来说,以太坊合并即是将以太坊原本的执行层 (execution layer),与新的权益证明共识层(即“信标链beacon-chain”)结合起来。

在以太坊采用权益证明机制下,成为以太坊网络的验证节点并生产区块的门槛之一是要通过以太坊智能合约锁定至少32个以太币(也称为“质押”)。此外,在市场上还出现了多家质押服务提供商,它们接受用户将以太币委托给服务商进行锁定并提供奖励。在权益证明机制下,验证节点负责验证交易、保护以太坊区块链的正常运作。这种设计消除了像“工作量证明”(Proof-of-Work)挖矿模式这样需要大量能源支持的方式,旨在使以太坊的能源损耗降低99.9%以上。

以太坊合并的技术难度甚大、堪比为持续飞行中的火箭更换引擎。在2022年9月15日,以太坊顺利实行合并。完成后,以太坊就迎来下一个重大更新:上海升级 (Shanghai Upgrade),主要旨在让已质押的以太币可以开放提币。  

什么是上海升级(Shanghai Upgrade)

其中,以太坊上海升级(EIP-4895)是推进这一转变的关键步骤之一。该更新将允许抵押ETH的用户提取其资金,这将释放巨额流动性,并对以太坊市场产生积极影响。此外,该更新还带来了其他改善与变化,从整体上进行了EVM虚拟机优化以及开放了信标链的质押提币。

上海升级有什么影响

首先,在上海升级后,得益于ETH基础能力的提升,Gas 花费会减少,不管是撸空投还是卷小图片,省钱了。

上海升级允许抵押ETH的用户提取他们的资金,这释放了大量流动性,并有助于解决以太坊的流动性问题。目前通过(Lido、Kraken、Binance、Coinbase)质押数目大于1498万,占总供给量的13.23%,成为潜在的抛压,所以在上海升级后可能会导致ETH价格的波动,因为更多的ETH将被释放到市场上(每日最多解锁5.04万枚的潜在抛压),同时,由于链上活动更为频繁,ETH更加通缩,反而有可能推动币价上升。

文章说明了上海升级对普通用户的影响。抵押ETH的用户现在可以随时提取他们的资金,而不必等待整个周期结束。对于那些使用抵押产品的用户来说,他们也可以享受到这一好处。对于持有ETH的投资者来说,他们需要密切关注市场情况,尤其是当更多的ETH被释放到市场上时。

一句话总结

上海升级,依旧是作为币圈在近期市场上的最大叙事和公众预期。同时升级附近消息日鸡飞狗跳,空头被拉爆,多头被活埋可能会在同一填发生。

以太坊(ETH)上海升级是什么意思?最先出现在Moby

🔲 ⭐

Gminer v3.32版本更新

Gminer v3.32版本更新是Kaspa的单挖福音,降本增效。

  1. Kaspa+Zil双挖过程中,提高了Kaspa hashrate哈希率稳定性。
  2. 提高了单挖模式下的Kaspa哈希率(根据GPU而定,最高可提高1%),降低了功率。
  3. 支持f2pool矿池挖Conflux。
  4. 改进了双挖时双倍强度的自动检测。
  5. 修复了在Linux上使用最新的Nvidia驱动程序时,内存温度显示问题。
  6. 默认隐藏矿池效率(要启用日志,请输入–log_pool_efficiency 1)。

更新时间:2023/04/13

更新内容:

  • fixed Kaspa hashrate dip while Zil mining round
  • improved Kaspa hashrate for single mining mode (up to +1% dependent on GPU), lowered power usage
  • support f2pool for Conflux mining
  • improved auto-detection of dual intensity for dual mining
  • fixed display of memory temperature on latest Nvidia drivers under Linux
  • hide pool efficiency by default (to enable log pass –log_pool_efficiency 1)

mmpOS 更新命令:

cd /tmp && wget https://github.com/develsoftware/GMinerRelease/releases/download/3.32/gminer_3_32_linux64.tar.xz && tar -xvf gminer_3_32_linux64.tar.xz && cd /opt/mmp/miners/gminer && agent-stop && cp /tmp/miner . && agent-start

Hive OS更新命令:

cd /tmp && wget https://github.com/develsoftware/GMinerRelease/releases/download/3.32/gminer_3_32_linux64.tar.xz && tar -xvf gminer_3_32_linux64.tar.xz && cd /hive/miners/gminer && miner stop && cp /tmp/miner $(ls -d1 */ | tail -1)/gminer && miner start

Gminer的开发者费用:

algorithm fee
eth, ethash 1%
etc, etchash 1%
kawpow, rvn, ravencoin 1%
autolykos2, ergo 2%
kheavyhash, kaspa 1%
cortex 5%
beamhash 2%
equihash144_5 2%
equihash125_4 2%
equihash210_9 2%
cuckoo29, aeternity 2%

Gminer v3.32版本更新最先出现在Moby

🔲 ☆

RSA从入门到入土

RSA从入门到入土

  • 直接分解n

    p-q 很大或很小

    p-1光滑 Pollard’s p − 1 算法分解N

    from Crypto.Util.number import *
      
    def Pollard_p_1(N):
        a = 2
        while True:
            f = a
            # precompute
            for n in range(1, 80000):
                f = pow(f, n, N)
            for n in range(80000, 104729+1):
                f = pow(f, n, N)
                if n % 15 == 0:
                    d = GCD(f-1, N)
                    if 1 < d < N:
                        return d
            print(a)
            a += 1
    

    p+1光滑 Williams’s p + 1 算法分解N

    yafu

  • 小公钥指数攻击

    • 条件

      e特别小,如e = 3

    • 原理

      $c≡m^{e}\ (mod\ n)$

      $ m^{e} = c + kn $

      $ m = (c + kn)^{1/e} $

      枚举k,开e次根,直到开出整数

      def small_exponent(pubkey, cipher):
      	N = 
          e = 
      	c = 
      	i = 0
      	i = 118719488
      	while 1:
      		if(gmpy.root(c+i*N, 3)[1]==1):
      			plaintext = gmpy.root(c+i*N, 3)[0]
      			break
      		i += 1
      	plaintext = transform.int2bytes(plaintext)
      	print(plaintext)
      
  • 共模攻击

    • 条件

      用相同的n不同的e对明文m加密

    • 原理

      $c_1 ≡ m^{e_1} (mod\ n)$

      $c_2 ≡ m^{e_2} (mod\ n)$

      $gcd(e_1,e_2) = 1$

      存在$s_1,s_2$使$s_1e_1+s_2e_2 = 1$

      $c_1≡m^{e_1}\ (mod\ n)$

      $c_2≡m^{e_2}\ (mod\ n)$

      所以

      $c_1^{s_1}c_2^{s_2}≡(m^{e_1})^{s_1}(m^{e_2})^{s_2}\ (mod\ n)$

      $c_1^{s_1}c_2^{s_2}≡m^{e_1s_1+e_2s_2}\ (mod\ n)$

      $c_1^{s_1}c_2^{s_2}≡m\ (mod\ n)$

      def common_modulus_attack(cipher1,cipher2):
      	c1 = 
      	c2 = 
      	e1 = 
      	e2 = 
      	N = 
      	s = gmpy2.gcdext(e1,e2)
      	s1 = s[1]
      	s2 = -s[2]
      	c2r = gmpy2.invert(c2, N)
      	plaintext = (pow(c1,s1,N) * pow(c2r,s2,N)) % N
      	plaintext = transform.int2bytes(plaintext)
      	print(plaintext)
      
  • dp

    import gmpy2
    e = 
    n = 
    dp = 
    for x in range(1, e):
        if(e*dp%x==1):
            p=(e*dp-1)//x+1
            if(n%p!=0):
                continue
            q=n//p
            phin=(p-1)*(q-1)
            d=gmpy.invert(e, phin)
    
  • dp、dq

    import gmpy2
    p = 
    q = 
    dp = 
    dq = 
    n = p*q
    phin = (p-1)*(q-1)
    dd = gmpy2.gcd(p-1, q-1)
    d=(dp-dq)//dd * gmpy2.invert((q-1)//dd, (p-1)//dd) * (q-1) +dq
    
  • Rabin算法

    • 条件

      e = 2

    • 原理

      $c=m^{2}\ mod\ n$

      $m_p =\sqrt{c}\ mod\ p $

      $m_q =\sqrt{c}\ mod\ q $

      若$p≡q≡3\ (mod\ 4)$

      $m_p = c^{1/4(p+1)}\ mod\ p$

      $m_q = c^{1/4(q+1)}\ mod\ q$

      $y_pp+y_qq=1$

      $gcdext(p,q)$解出$y_p,y_q$

      解出四个明文

      $a = (y_ppm_q+y_qqm_p)\ mod\ n$

      $ b = n -a$

      $c = (y_ppm_q-y_qq_mp)\ mod\ n$

      $d = n - c$

      Jarvis-OJ-hard-rsa

       # coding=utf-8
       import gmpy2
       import string
       from rsa import transform
       n = 87924348264132406875276140514499937145050893665602592992418171647042491658461L
       e = 2
       p = 275127860351348928173285174381581152299
       q = 319576316814478949870590164193048041239
       c = open("flag.enc" ,'rb').read()
       c = transform.bytes2int(c)
       # 计算yp和yq
       inv_p = gmpy2.invert(p, q)
       inv_q = gmpy2.invert(q, p)
             
       # 计算mp和mq
       mp = pow(c, (p + 1) / 4, p)
       mq = pow(c, (q + 1) / 4, q)
             
       # 计算a,b,c,d
       a = (inv_p * p * mq + inv_q * q * mp) % n
       b = n - int(a)
       c = (inv_p * p * mq - inv_q * q * mp) % n
       d = n - int(c)
             
       for i in (a, b, c, d):
           print(transform.int2bytes(i))
      
  • Wiener’s attack

  • Factoring with High Bits Known

    • 条件

      已知N的一个因子的较高位

      coppersmith1

      p.bit_length() == 1024 ,p的高位需已知约576位
      p.bit_length() == 512 ,p的高位需已知约288位
      
    • 例子

      '''
      n = 110884890902749085253001083431222443088115610795940152564793628519927092107501946446399003764508722709710121804620193329162066855289179887539537634989483300155392790067446377224025966917227342075570751172611456818461296838516185655681858001119900898375522640670694604696426035782721144065487316221499661637517
      e = 65537
      c = 56d1b214082fd508567e0a4e101dcaa4f3edf262d7330cae4d75d94b874f53dfe3c9ba66d62a41b7e9331e67ae6907e3c028701e53555fea0832b2908471d04ceb98dbedf576a504902d50c3c32050fa036573de4f466f9c5de6b6bd4ad2f96bd6cd235a62c6c9555eb5ecf5b793b514f60d3e75a8307983c0f1aab746477a7b
      front = 754471047130831460574350468751127056146566410666010180184022324900851348720910487519
      backLength = 512 - front.bit_length()
      '''
          
      import binascii
      n=110884890902749085253001083431222443088115610795940152564793628519927092107501946446399003764508722709710121804620193329162066855289179887539537634989483300155392790067446377224025966917227342075570751172611456818461296838516185655681858001119900898375522640670694604696426035782721144065487316221499661637517
      cipher = 0x56c5afbc956157241f2d4ea90fd24ad58d788ca1fa2fddb9084197cfc526386d223f88be38ec2e1820c419cb3dad133c158d4b004ae0943b790f0719b40e58007ba730346943884ddc36467e876ca7a3afb0e5a10127d18e3080edc18f9fbe590457352dca398b61eff93eec745c0e49de20bba1dd77df6de86052ffff41247d
      e2 = 0x10001
      pbits = 512
      for i in range(0,4095):
        p4 = 0x636c1b2209b27268ad05ff5d64802c40d509cefccd92953227264dab0f27187dea4fdf000
        p4 = p4 + int(hex(i),16)
        kbits = pbits - p4.nbits() 
        p4 = p4 << kbits 
        PR.<x> = PolynomialRing(Zmod(n))
        f = x + p4
        roots = f.small_roots(X=2^kbits, beta=0.4) 
        if roots: 
          p = p4+int(roots[0])
          assert n % p == 0
          q = n/int(p)
          phin = (p-1)*(q-1)
          d = inverse_mod(e2,phin)
          print(d)
          cipher = 0x56d1b214082fd508567e0a4e101dcaa4f3edf262d7330cae4d75d94b874f53dfe3c9ba66d62a41b7e9331e67ae6907e3c028701e53555fea0832b2908471d04ceb98dbedf576a504902d50c3c32050fa036573de4f466f9c5de6b6bd4ad2f96bd6cd235a62c6c9555eb5ecf5b793b514f60d3e75a8307983c0f1aab746477a7b
          flag = pow(cipher,d,n)
          flag = hex(int(flag))[2:-1]
          print (binascii.unhexlify(flag))
      #https://sagecell.sagemath.org/
      
  • Known High Bits Message Attack

    • 例子
      /*
      [+]n=0x7c3139d3be9a691abdf3ff49c712fcb84ba39bbd2189bb98d04e04d2d7cc086c9d31b06fdf828aaeeb3765e1ab8ea41a3f1b8c73b80a498f1e2eaad42a1ac7b8e54e705cd1e3e4a39940f9bdcd16d4b42ab71a826955cc78450d6915663c82ae80fd2f64b7e3a70f2b188b85a738759eeb0688dfa22525bbbe92d7934763445L
      [+]e=3
      [+]m=random.getrandbits(512)
      [+]c=pow(m,e,n)=0x20084d9c4fa81d903437a9fabea4a2ad025a00ddc961e4fcd0f52ff9ec750702c109ce0188ae96e540a5c3dcf55013ced9ee37ad9547240fc8773f81fbb509b0b8ab24ed0288a6e1f997b5c0b196236bc8da2df9cce77c559492963eeafbbe4f5a9cb18098bfac87a1e179b26f60948fb72327acc0675890009a04697b76073L
      [+]((m>>72)<<72)=0xb90f972f73ebb3952b3a8e50233f783732478d874795b44c33f685caf7637f4cd0c90cf3a599e1a01e84a28459220b31a490fd1892df58000000000000000000L
      */
          
      import time
      def matrix_overview(BB, bound):
          for ii in range(BB.dimensions()[0]):
              a = ('%02d ' % ii)
              for jj in range(BB.dimensions()[1]):
                  a += '0' if BB[ii,jj] == 0 else 'X'
                  a += ' '
              if BB[ii, ii] >= bound:
                  a += '~'
              print a
      def coppersmith_howgrave_univariate(pol, modulus, beta, mm, tt, XX):
          
          dd = pol.degree()
          nn = dd * mm + tt
          
          if not 0 < beta <= 1:
              raise ValueError("beta should belongs in (0, 1]")
          
          if not pol.is_monic():
              raise ArithmeticError("Polynomial must be monic.")
             
          polZ = pol.change_ring(ZZ)
          x = polZ.parent().gen()
          
          # compute polynomials
          gg = []
          for ii in range(mm):
              for jj in range(dd):
                  gg.append((x * XX)**jj * modulus**(mm - ii) * polZ(x * XX)**ii)
          for ii in range(tt):
              gg.append((x * XX)**ii * polZ(x * XX)**mm)
          
          BB = Matrix(ZZ, nn)
          
          for ii in range(nn):
              for jj in range(ii+1):
                  BB[ii, jj] = gg[ii][jj]
          
          # display basis matrix
          if debug:
              matrix_overview(BB, modulus^mm)
          
          # LLL
          BB = BB.LLL()
          
          # transform shortest vector in polynomial    
          new_pol = 0
          for ii in range(nn):
              new_pol += x**ii * BB[0, ii] / XX**ii
          
          # factor polynomial
          potential_roots = new_pol.roots()
          print "potential roots:", potential_roots
          
          # test roots
          roots = []
          for root in potential_roots:
              if root[0].is_integer():
                  result = polZ(ZZ(root[0]))
                  if gcd(modulus, result) >= modulus^beta:
                      roots.append(ZZ(root[0]))
          return roots
          
          
      # RSA gen options (for the demo)
      length_N = 1024  # size of the modulus
      Kbits = 72      # size of the root
      e = 3
      N = 0x7c3139d3be9a691abdf3ff49c712fcb84ba39bbd2189bb98d04e04d2d7cc086c9d31b06fdf828aaeeb3765e1ab8ea41a3f1b8c73b80a498f1e2eaad42a1ac7b8e54e705cd1e3e4a39940f9bdcd16d4b42ab71a826955cc78450d6915663c82ae80fd2f64b7e3a70f2b188b85a738759eeb0688dfa22525bbbe92d7934763445L
      ZmodN = Zmod(N);
          
      C = 0x20084d9c4fa81d903437a9fabea4a2ad025a00ddc961e4fcd0f52ff9ec750702c109ce0188ae96e540a5c3dcf55013ced9ee37ad9547240fc8773f81fbb509b0b8ab24ed0288a6e1f997b5c0b196236bc8da2df9cce77c559492963eeafbbe4f5a9cb18098bfac87a1e179b26f60948fb72327acc0675890009a04697b76073L
      msg = 0xb90f972f73ebb3952b3a8e50233f783732478d874795b44c33f685caf7637f4cd0c90cf3a599e1a01e84a28459220b31a490fd1892df58000000000000000000
      P.<x> = PolynomialRing(ZmodN) #, implementation='NTL')
      pol = (msg + x)^e - C
      dd = pol.degree()
          
      # Tweak those
      beta = 1                                # b = N
      epsilon = beta / 7                      # <= beta / 7
      mm = ceil(beta**2 / (dd * epsilon))     # optimized value
      tt = floor(dd * mm * ((1/beta) - 1))    # optimized value
      XX = ceil(N**((beta**2/dd) - epsilon))  # optimized value
          
      # Coppersmith
      start_time = time.time()
      roots = coppersmith_howgrave_univariate(pol, N, beta, mm, tt, XX)
          
      # output
      print "\n# Solutions"
      print "we found:", str(roots)
      print("in: %s seconds " % (time.time() - start_time))
      print "\n"
          
      
  • 部分私钥泄露

    • 例子
      /*
      [+]n=0x291b24eae63660849a91b7122663814918ae91d62e3431163c4f47ecdbf92c59c9c430bbcc9443e4ff3dedbe60b1c06f383771bf628cdd36e649aa0c96db4addac4885071b651d2b1ae4e131ae3c115f1a59b828999ca7af8f235b75ad5b757680249eaa9b531ec1edbf9204417f17df08ec550893ed36523fcfef7fb4b2415dL
      [+]e=3
      [+]m=random.getrandbits(512)
      [+]c=pow(m,e,n)=0x623dc16f9047da92278d94fe3cabbd89db4f8c4c612ac55a439df31e368133d697cb08a571e2aad2a194800a433bc00940967441bb7e0d30bfc0599c55aeefc4af8be67ffaac307b65a2096863ca87c6aad615535814758212baae7328ac1ae9bce9f39a52456852c4c0b9779edbb19016872f516e2be9fab463f3b405e25beL
      [+]d=invmod(e,(p-1)*(q-1))
      [+]d&((1<<512)-1)=0x91d03d35338acebcf703991efd4b3f9c88e2f022568c31a410a33062d3e3e24571dc3537e21741e6b1c9eba127db0a768842d79a3197dca5b86e2cd509cd3b93L
      */
          
      known_bits = 512
      e = 3
      X = var('X')
      N=0x291b24eae63660849a91b7122663814918ae91d62e3431163c4f47ecdbf92c59c9c430bbcc9443e4ff3dedbe60b1c06f383771bf628cdd36e649aa0c96db4addac4885071b651d2b1ae4e131ae3c115f1a59b828999ca7af8f235b75ad5b757680249eaa9b531ec1edbf9204417f17df08ec550893ed36523fcfef7fb4b2415d
      d0 = 0x91d03d35338acebcf703991efd4b3f9c88e2f022568c31a410a33062d3e3e24571dc3537e21741e6b1c9eba127db0a768842d79a3197dca5b86e2cd509cd3b93
      P.<x> = PolynomialRing(Zmod(N))
      for k in xrange(1, e+1):
          results = solve_mod([e * d0 * X - k * X * (N - X + 1) + k * N == X], 2 ** 512)
          
          for m in results:
              f = x * 2 ** known_bits + ZZ(m[0])
              f = f.monic()
              roots = f.small_roots(X = 1, beta=0.3)
          
              if roots:
                  x0 = roots[0]
                  p = gcd(2 ** known_bits * x0 + ZZ(m[0]), N)
                  print '[+] Found factorization!'
                  print 'p =', ZZ(p)
                  print 'q =', N / ZZ(p)
                  break
      n=0x291b24eae63660849a91b7122663814918ae91d62e3431163c4f47ecdbf92c59c9c430bbcc9443e4ff3dedbe60b1c06f383771bf628cdd36e649aa0c96db4addac4885071b651d2b1ae4e131ae3c115f1a59b828999ca7af8f235b75ad5b757680249eaa9b531ec1edbf9204417f17df08ec550893ed36523fcfef7fb4b2415d
      p = 4369408607185874842987791687972458181281635894126489505104950532427588844072160412371716367300194382551703650763840526184308340422066926730909955032516327
      q = 6606303039610996668393006981258443682930645126368365403476569537191826726070101133886617447839914797821331788273937106747740172175833966059802001454391579
      e = 3
      phin = (p-1)*(q-1)
      d = inverse_mod(e,phin)
      cipher = 0x623dc16f9047da92278d94fe3cabbd89db4f8c4c612ac55a439df31e368133d697cb08a571e2aad2a194800a433bc00940967441bb7e0d30bfc0599c55aeefc4af8be67ffaac307b65a2096863ca87c6aad615535814758212baae7328ac1ae9bce9f39a52456852c4c0b9779edbb19016872f516e2be9fab463f3b405e25be
      print(d)
      flag = pow(cipher,d,n)
      flag = hex(int(flag))[2:-1]
      print(flag)
          
      
  • Coppersmith’s short-pad attack

    • 例子
      [+]n=0xc9f2c02d0ce22b192b5a046f8311b3eb470394ef228bbe8bc31f2939e3d7472a62eea2468c06b7d7de3a155a2e5a10c98143ede2fdf2f60fe5d65c9ba9fa26f5f7d05591201c76765599fb35f13e00a5b089fd4215c57b1453aaefc911a73c9f39003153af5e4a2e882a1c6c02d0024a6b0dede6c159a65b0bfe5c57b616127L
      [+]e=3
      [+]m=random.getrandbits(512)
      [+]c=pow(m,e,n)=0xb6046b56183fcc80d8a7c5dbc1f39176e736e2054255002abe1947a6e51fb7c37bdd689235613aec0e2a2651fade4837b968d4d6396b908a407f35e742065a773499f3bcd6111f2a1d8b65a3c79c9d3b20d681b9bf8cb2f26d2c528bca82e76d45ec734647cb13ca1a327e88173a64839bd4d8e576427600c86e7bc7224832cL
      [+]x=pow(m+1,e,n)=0xb590cc6da005f5bae916d26dca52f3f8e4c6c77d3d24df9f1f6e4e1ef1e58dc3b2bb0571810f5f27b019be2a768a392057c83006cbb12363b9661089d3fae650017c64d218ebe2b48b2ae91128d7613e6e51fabb94e7aaaba01d711d40ddac122683060ca5416ff0a00fa7f043f834d3989f8240b677a0cdda107832abe56c4L
          
      import gmpy2
      def getM2(a,b,c1,c2,n):
          a3 = pow(a,3,n)
          b3 = pow(b,3,n)
          first = c1-a3*c2+2*b3
          first = first % n
          second = 3*b*(a3*c2-b3)
          second = second % n
          third = second*gmpy2.invert(first,n)
          third = third % n
          fourth = (third+b)*gmpy2.invert(a,n)
          return fourth % n
      n=0xc9f2c02d0ce22b192b5a046f8311b3eb470394ef228bbe8bc31f2939e3d7472a62eea2468c06b7d7de3a155a2e5a10c98143ede2fdf2f60fe5d65c9ba9fa26f5f7d05591201c76765599fb35f13e00a5b089fd4215c57b1453aaefc911a73c9f39003153af5e4a2e882a1c6c02d0024a6b0dede6c159a65b0bfe5c57b616127L
      e=3
      c1=0xb6046b56183fcc80d8a7c5dbc1f39176e736e2054255002abe1947a6e51fb7c37bdd689235613aec0e2a2651fade4837b968d4d6396b908a407f35e742065a773499f3bcd6111f2a1d8b65a3c79c9d3b20d681b9bf8cb2f26d2c528bca82e76d45ec734647cb13ca1a327e88173a64839bd4d8e576427600c86e7bc7224832cL
      c2=0xb590cc6da005f5bae916d26dca52f3f8e4c6c77d3d24df9f1f6e4e1ef1e58dc3b2bb0571810f5f27b019be2a768a392057c83006cbb12363b9661089d3fae650017c64d218ebe2b48b2ae91128d7613e6e51fabb94e7aaaba01d711d40ddac122683060ca5416ff0a00fa7f043f834d3989f8240b677a0cdda107832abe56c4L
      padding1 = 0
      padding2 = 1
      m = getM2(1,padding1-padding2,c1,c2,n)-padding2
      print(hex(m))
          
      
  • Boneh and Durfee attack

    • 例子
      /*
      [+]n=0xbadd260d14ea665b62e7d2e634f20a6382ac369cd44017305b69cf3a2694667ee651acded7085e0757d169b090f29f3f86fec255746674ffa8a6a3e1c9e1861003eb39f82cf74d84cc18e345f60865f998b33fc182a1a4ffa71f5ae48a1b5cb4c5f154b0997dc9b001e441815ce59c6c825f064fdca678858758dc2cebbc4d27L
      [+]d=random.getrandbits(1024*0.270)
      [+]e=invmod(d,phin)
      [+]hex(e)=0x11722b54dd6f3ad9ce81da6f6ecb0acaf2cbc3885841d08b32abc0672d1a7293f9856db8f9407dc05f6f373a2d9246752a7cc7b1b6923f1827adfaeefc811e6e5989cce9f00897cfc1fc57987cce4862b5343bc8e91ddf2bd9e23aea9316a69f28f407cfe324d546a7dde13eb0bd052f694aefe8ec0f5298800277dbab4a33bbL
      [+]m=random.getrandbits(512)
      [+]c=pow(m,e,n)=0xe3505f41ec936cf6bd8ae344bfec85746dc7d87a5943b3a7136482dd7b980f68f52c887585d1c7ca099310c4da2f70d4d5345d3641428797030177da6cc0d41e7b28d0abce694157c611697df8d0add3d900c00f778ac3428f341f47ecc4d868c6c5de0724b0c3403296d84f26736aa66f7905d498fa1862ca59e97f8f866cL
      */
          
      import time
          
      strict = False
          
      helpful_only = True
      dimension_min = 7 # stop removing if lattice reaches that dimension
          
      def helpful_vectors(BB, modulus):
          nothelpful = 0
          for ii in range(BB.dimensions()[0]):
              if BB[ii,ii] >= modulus:
                  nothelpful += 1
          
          print nothelpful, "/", BB.dimensions()[0], " vectors are not helpful"
          
      # display matrix picture with 0 and X
      def matrix_overview(BB, bound):
          for ii in range(BB.dimensions()[0]):
              a = ('%02d ' % ii)
              for jj in range(BB.dimensions()[1]):
                  a += '0' if BB[ii,jj] == 0 else 'X'
                  if BB.dimensions()[0] < 60:
                      a += ' '
              if BB[ii, ii] >= bound:
                  a += '~'
              print a
          
      # tries to remove unhelpful vectors
      # we start at current = n-1 (last vector)
      def remove_unhelpful(BB, monomials, bound, current):
          # end of our recursive function
          if current == -1 or BB.dimensions()[0] <= dimension_min:
              return BB
          
          # we start by checking from the end
          for ii in range(current, -1, -1):
              # if it is unhelpful:
              if BB[ii, ii] >= bound:
                  affected_vectors = 0
                  affected_vector_index = 0
                  # let's check if it affects other vectors
                  for jj in range(ii + 1, BB.dimensions()[0]):
                      # if another vector is affected:
                      # we increase the count
                      if BB[jj, ii] != 0:
                          affected_vectors += 1
                          affected_vector_index = jj
          
                  # level:0
                  # if no other vectors end up affected
                  # we remove it
                  if affected_vectors == 0:
                      print "* removing unhelpful vector", ii
                      BB = BB.delete_columns([ii])
                      BB = BB.delete_rows([ii])
                      monomials.pop(ii)
                      BB = remove_unhelpful(BB, monomials, bound, ii-1)
                      return BB
          
                  # level:1
                  # if just one was affected we check
                  # if it is affecting someone else
                  elif affected_vectors == 1:
                      affected_deeper = True
                      for kk in range(affected_vector_index + 1, BB.dimensions()[0]):
                          # if it is affecting even one vector
                          # we give up on this one
                          if BB[kk, affected_vector_index] != 0:
                              affected_deeper = False
                      # remove both it if no other vector was affected and
                      # this helpful vector is not helpful enough
                      # compared to our unhelpful one
                      if affected_deeper and abs(bound - BB[affected_vector_index, affected_vector_index]) < abs(bound - BB[ii, ii]):
                          print "* removing unhelpful vectors", ii, "and", affected_vector_index
                          BB = BB.delete_columns([affected_vector_index, ii])
                          BB = BB.delete_rows([affected_vector_index, ii])
                          monomials.pop(affected_vector_index)
                          monomials.pop(ii)
                          BB = remove_unhelpful(BB, monomials, bound, ii-1)
                          return BB
          # nothing happened
          return BB
          
      """ 
      Returns:
      * 0,0   if it fails
      * -1,-1 if `strict=true`, and determinant doesn't bound
      * x0,y0 the solutions of `pol`
      """
      def boneh_durfee(pol, modulus, mm, tt, XX, YY):
          """
          Boneh and Durfee revisited by Herrmann and May
              
          finds a solution if:
          * d < N^delta
          * |x| < e^delta
          * |y| < e^0.5
          whenever delta < 1 - sqrt(2)/2 ~ 0.292
          """
          
          # substitution (Herrman and May)
          PR.<u, x, y> = PolynomialRing(ZZ)
          Q = PR.quotient(x*y + 1 - u) # u = xy + 1
          polZ = Q(pol).lift()
          
          UU = XX*YY + 1
          
          # x-shifts
          gg = []
          for kk in range(mm + 1):
              for ii in range(mm - kk + 1):
                  xshift = x^ii * modulus^(mm - kk) * polZ(u, x, y)^kk
                  gg.append(xshift)
          gg.sort()
          
          # x-shifts list of monomials
          monomials = []
          for polynomial in gg:
              for monomial in polynomial.monomials():
                  if monomial not in monomials:
                      monomials.append(monomial)
          monomials.sort()
              
          # y-shifts (selected by Herrman and May)
          for jj in range(1, tt + 1):
              for kk in range(floor(mm/tt) * jj, mm + 1):
                  yshift = y^jj * polZ(u, x, y)^kk * modulus^(mm - kk)
                  yshift = Q(yshift).lift()
                  gg.append(yshift) # substitution
              
          # y-shifts list of monomials
          for jj in range(1, tt + 1):
              for kk in range(floor(mm/tt) * jj, mm + 1):
                  monomials.append(u^kk * y^jj)
          
          # construct lattice B
          nn = len(monomials)
          BB = Matrix(ZZ, nn)
          for ii in range(nn):
              BB[ii, 0] = gg[ii](0, 0, 0)
              for jj in range(1, ii + 1):
                  if monomials[jj] in gg[ii].monomials():
                      BB[ii, jj] = gg[ii].monomial_coefficient(monomials[jj]) * monomials[jj](UU,XX,YY)
          
          # Prototype to reduce the lattice
          if helpful_only:
              # automatically remove
              BB = remove_unhelpful(BB, monomials, modulus^mm, nn-1)
              # reset dimension
              nn = BB.dimensions()[0]
              if nn == 0:
                  print "failure"
                  return 0,0
          
          # check if vectors are helpful
          if debug:
              helpful_vectors(BB, modulus^mm)
              
          # check if determinant is correctly bounded
          det = BB.det()
          bound = modulus^(mm*nn)
          if det >= bound:
              print "We do not have det < bound. Solutions might not be found."
              print "Try with highers m and t."
              if debug:
                  diff = (log(det) - log(bound)) / log(2)
                  print "size det(L) - size e^(m*n) = ", floor(diff)
              if strict:
                  return -1, -1
          else:
              print "det(L) < e^(m*n) (good! If a solution exists < N^delta, it will be found)"
          
          # display the lattice basis
          if debug:
              matrix_overview(BB, modulus^mm)
          
          # LLL
          if debug:
              print "optimizing basis of the lattice via LLL, this can take a long time"
          
          BB = BB.LLL()
          
          if debug:
              print "LLL is done!"
          
          # transform vector i & j -> polynomials 1 & 2
          if debug:
              print "looking for independent vectors in the lattice"
          found_polynomials = False
              
          for pol1_idx in range(nn - 1):
              for pol2_idx in range(pol1_idx + 1, nn):
                  # for i and j, create the two polynomials
                  PR.<w,z> = PolynomialRing(ZZ)
                  pol1 = pol2 = 0
                  for jj in range(nn):
                      pol1 += monomials[jj](w*z+1,w,z) * BB[pol1_idx, jj] / monomials[jj](UU,XX,YY)
                      pol2 += monomials[jj](w*z+1,w,z) * BB[pol2_idx, jj] / monomials[jj](UU,XX,YY)
          
                  # resultant
                  PR.<q> = PolynomialRing(ZZ)
                  rr = pol1.resultant(pol2)
          
                  # are these good polynomials?
                  if rr.is_zero() or rr.monomials() == [1]:
                      continue
                  else:
                      print "found them, using vectors", pol1_idx, "and", pol2_idx
                      found_polynomials = True
                      break
              if found_polynomials:
                  break
          
          if not found_polynomials:
              print "no independant vectors could be found. This should very rarely happen..."
              return 0, 0
              
          rr = rr(q, q)
          
          # solutions
          soly = rr.roots()
          
          if len(soly) == 0:
              print "Your prediction (delta) is too small"
              return 0, 0
          
          soly = soly[0][0]
          ss = pol1(q, soly)
          solx = ss.roots()[0][0]
          
          #
          return solx, soly
          
      def example():
          #
          # The problem to solve (edit the following values)
          #
          
          # the modulus
          N = 0xbadd260d14ea665b62e7d2e634f20a6382ac369cd44017305b69cf3a2694667ee651acded7085e0757d169b090f29f3f86fec255746674ffa8a6a3e1c9e1861003eb39f82cf74d84cc18e345f60865f998b33fc182a1a4ffa71f5ae48a1b5cb4c5f154b0997dc9b001e441815ce59c6c825f064fdca678858758dc2cebbc4d27
          # the public exponent
          e = 0x11722b54dd6f3ad9ce81da6f6ecb0acaf2cbc3885841d08b32abc0672d1a7293f9856db8f9407dc05f6f373a2d9246752a7cc7b1b6923f1827adfaeefc811e6e5989cce9f00897cfc1fc57987cce4862b5343bc8e91ddf2bd9e23aea9316a69f28f407cfe324d546a7dde13eb0bd052f694aefe8ec0f5298800277dbab4a33bb
          
          # the hypothesis on the private exponent (the theoretical maximum is 0.292)
          delta = .18 # this means that d < N^delta
          
          #
          # Lattice (tweak those values)
          #
          
          # you should tweak this (after a first run), (e.g. increment it until a solution is found)
          m = 4 # size of the lattice (bigger the better/slower)
          
          # you need to be a lattice master to tweak these
          t = int((1-2*delta) * m)  # optimization from Herrmann and May
          X = 2*floor(N^delta)  # this _might_ be too much
          Y = floor(N^(1/2))    # correct if p, q are ~ same size
          
          #
          # Don't touch anything below
          #
          
          # Problem put in equation
          P.<x,y> = PolynomialRing(ZZ)
          A = int((N+1)/2)
          pol = 1 + x * (A + y)
          
          #
          # Find the solutions!
          #
          
          # Checking bounds
          if debug:
              print "=== checking values ==="
              print "* delta:", delta
              print "* delta < 0.292", delta < 0.292
              print "* size of e:", int(log(e)/log(2))
              print "* size of N:", int(log(N)/log(2))
              print "* m:", m, ", t:", t
          
          # boneh_durfee
          if debug:
              print "=== running algorithm ==="
              start_time = time.time()
          
          solx, soly = boneh_durfee(pol, e, m, t, X, Y)
          
          # found a solution?
          if solx > 0:
              print "=== solution found ==="
              if False:
                  print "x:", solx
                  print "y:", soly
          
              d = int(pol(solx, soly) / e)
              print "private key found:", d
          else:
              print "=== no solution was found ==="
          
          if debug:
              print("=== %s seconds ===" % (time.time() - start_time))
          
      if __name__ == "__main__":
          example()
          
      
>
❌