Diem 区块链认证数据结构规范:Merkle 累加器、稀疏 Merkle 树与客户端验证实战
2026/9/23 1:20:34 网站建设 项目流程
  • 区块链
  • 金融科技

【免费下载链接】diem

Diem’s mission is to build a trusted and innovative financial network that empowers people and businesses around the world.

项目地址:https://gitcode.com/gh_mirrors/di/diem
点击查看免费下载

本文是 Diem(LPN,Diem Payment Network)协议认证数据结构(Authenticated Data Structure)的完整技术规范解读。它详细说明 Diem 区块链中"账本状态(Ledger State)"与"交易输出"如何组织成不可篡改的历史,以及客户端如何利用 Merkle 树根哈希作为认证器(Authenticator),在不可信服务器上安全地查询交易、账户状态与事件。读完本文,你将掌握 Merkle 累加器与稀疏 Merkle 树的精确定义、证明格式与验证算法,并理解TransactionInfoLedgerInfo等核心数据结构在客户端验证链路中的实际用法。全文以 authenticated_data_structures.md 为主干,并结合仓库源码(如 storage/accumulator、types/src/proof)进行纵深补充。

概述:版本化数据库与认证模型

Diem 区块链的所有数据都存储在一个**单一版本化数据库(single versioned database)**中。版本号是一个无符号 64 位整数(u64),其数值等于系统已执行的交易数量。在每个版本i,数据库保存一个三元组(Tᵢ, Oᵢ, Sᵢ),分别代表交易(Transaction)、交易输出(Transaction Output)与账本状态(Ledger State)。

给定一个确定性的执行函数Apply,三元组的含义是:针对账本状态Sᵢ₋₁执行交易Tᵢ,产生输出Oᵢ和新的账本状态Sᵢ,即:

Apply(Sᵢ₋₁, Tᵢ) -> ⟨Oᵢ, Sᵢ⟩

创世交易T₀在空数据库上执行,产生创世状态S₀。这一版本化模型是整个认证数据结构的语义基础:每个版本都对应一棵完整的状态树快照,而历史则通过累积的方式被不可篡改地记录下来。

本规范文档主要回答三个问题:

  1. 账本状态与交易输出是什么、如何表示;
  2. 它们如何构成不可变的账本历史;
  3. 共识协议提交(commit)之后,客户端在查询服务器时如何验证响应。

交易的具体格式与Apply函数的完整定义由 Move 虚拟机规范另行描述(原文档以 "XXX" 占位待补),本文聚焦于认证数据结构本身。

Merkle 树与证明基础

Diem 协议中的认证数据结构全部基于 Merkle 树。与所有 Merkle 树一样,树的根哈希是一个短小的认证器(authenticator),它构成对整个大树(承载大量数据)的绑定承诺(binding commitment)。当客户端向不可信服务器查询树的某一部分时,服务器返回结果并附带一个短小的证明(proof),客户端用证明与已知的根哈希即可验证结果的正确性。

哈希函数约定

本文使用的哈希函数遵循 Diem 的 密码学规范。该规范明确:Diem 内部哈希统一使用SHA3-256作为唯一算法,用于所有内部数据结构的哈希计算;同时引入**域分离(domain separation)**机制——每个 Diem 数据结构在哈希时都会拼接以其类型对应的域分隔符(由 ASCII 前缀"DIEM::"与类型的序列化名称组成),以避免序列化字节串的歧义和碰撞攻击。

在 crates/diem-crypto/src/hash.rs 中可以看到占位符哈希的生成方式:

fn create_literal_hash(word: &str) -> HashValue { let mut s = word.as_bytes().to_vec(); assert!(s.len() <= HashValue::LENGTH); s.resize(HashValue::LENGTH, 0); HashValue::from_slice(&s).expect("Cannot fail") }

即:将字符串字节拼接补零至 32 字节,得到一个没有已知原像(pre-image)的HashValue。Merkle 累加器与稀疏 Merkle 树的占位符哈希正是由此生成(hash.rs)。

Merkle 累加器(Merkle Accumulator)

定义

Merkle 累加器是一种只追加(append-only)的二叉树,用于存储对象列表。其规则如下:

  • 每个叶子的值是一个HashValue,等于对应对象的密码学哈希;
  • 每个内部节点的值是一个HashValue,等于其两个子节点哈希拼接后的哈希;
  • 树的根哈希即根节点的值。

树的确切形态由已追加的对象总数决定。对于存储k个对象的累加器,令n为大于等于k的最小 2 的幂。累加器的结构等价于一棵具有n片叶子的完美二叉树,其中最左边k片叶子对应所有对象,最右边n-k片叶子是占位节点;任何只含占位节点的子树都会被压缩成单个占位节点

占位节点的值定义为:将字节数组b"ACCUMULATOR_PLACEHOLDER_HASH"补零至 32 字节后作为HashValue。该哈希没有已知原像。上图展示了累加器随对象不断追加而演变的形态(0 个到 5 个对象,占位节点逐步被真实子树替换)。

从源码看,累加器实现区分了冻结节点(Frozen Node)与非冻结节点(Non-frozen Node)(storage/accumulator/src/lib.rs):当一片叶子的所有后代都是非占位节点时,该节点即"冻结",其哈希值在后续追加中不再改变。存储层只持久化冻结节点,非冻结节点在访问时动态生成,从而保证物理存储也是纯追加的(append-only)。

API

Merkle 累加器支持以下操作(原文档的 trait 定义):

trait MerkleAccumulator<T> { /// Appends next object to the existing tree. fn append(&self, object: T); /// Queries the `index`-th object. Returns the object with a proof if the /// object exists in the tree. Errors if the tree has less than `index+1` /// objects in total. fn get_with_proof(&self, index: usize) -> Result<(T, AccumulatorProof)>; /// Similar to get_with_proof but queries a range of objects starting from /// `first_index`. fn get_range_with_proof( &self, first_index: usize, num_objects: usize, ) -> Result<(Vec<T>, AccumulatorRangeProof)>; }

仓库中对应的实际实现位于 storage/accumulator/src/lib.rs,提供了appendget_proof(单叶包含证明)、get_range_proof(范围证明)以及get_consistency_proof(一致性证明,用于证明完整累加器与较小累加器的一致性,供状态同步场景使用)。追加过程的关键实现是append_one:把新叶子压入冻结子树根列表后,按照叶子数二进制中尾部 1 的个数执行若干次"合并最右侧两个子树"操作(storage/accumulator/src/lib.rs)。

AccumulatorProof 的格式与验证
格式

对于树中的任意对象,从该对象到根节点存在一条路径。对象的AccumulatorProof由路径上的所有兄弟节点组成。兄弟节点从下到上排列,即靠近底部的兄弟位于列表开头。

struct AccumulatorProof { siblings: Vec<HashValue>, }

例如,查询索引为 3 的对象O时,兄弟列表为[A, B, C],其结构见下图:

在仓库中,AccumulatorProof的定义位于 types/src/proof/definition.rs,并有常量约束:

pub const MAX_ACCUMULATOR_PROOF_DEPTH: usize = 63; pub const MAX_ACCUMULATOR_LEAVES: LeafCount = 1 << MAX_ACCUMULATOR_PROOF_DEPTH;

即累加器最大叶子数被硬性限制为 2⁶³,任何超过 63 个兄弟节点的证明都会被拒绝(见下文验证第 1 步)。

验证流程

验证时假定:

  1. 客户端知道累加器共有k个对象;
  2. 客户端已获得该累加器的根哈希H
  3. 客户端查询第i个对象(i < k)。

服务器返回对象O与证明(siblings)后,客户端按以下步骤验证:

  1. 确保证明中兄弟节点的数量不超过 63;
  2. 利用索引i,判断每个兄弟节点是其父节点的左孩子还是右孩子;
  3. 对对象O求哈希,得到对应叶子的值;
  4. 将叶子与证明中的兄弟节点逐层合并,重建根节点并得到根哈希;
  5. 将计算出的根哈希与已知根哈希H比较:当且仅当两者相等时,对象与证明才被信任

验证伪代码如下:

fn verify_accumulator_proof( index: usize, known_root_hash: HashValue, obj: Object, proof: AccumulatorProof, ) -> Result<()> { // We assume that the accumulator will never have more than 2^63 leaves, // which should be reasonable in real world scenarios. We do a sanity check // here to ensure that if the proof is too long, it gets rejected // immediately to avoid wasting further computation. ensure!(proof.siblings.len() <= 63); let mut current_hash = hash(obj); for sibling in proof.siblings { if sibling.is_left_child(index) { current_hash = hash(sibling || current_hash); } else { current_hash = hash(current_hash || sibling); } } ensure!(current_hash == known_root_hash); Ok(()) }

注意sibling.is_left_child(index)的判断贯穿整个循环:index的二进制位从低位到高位依次决定了每一层兄弟节点位于当前节点左侧还是右侧,这也是证明验证无需任何树结构元数据、仅凭索引即可完成的原因。

稀疏 Merkle 树(Sparse Merkle Tree)

定义

Diem 协议中的稀疏 Merkle 树用于存储键值映射:所有键都是 256 位字节数组,值可以是任意可序列化为二进制 blob 的对象。稀疏 Merkle 树同样是二叉树,每片叶子对应映射中的一个条目。与只追加、不可变的 Merkle 累加器不同,稀疏 Merkle 树支持更新已有条目、添加新条目和删除条目

给定一个键值映射,稀疏 Merkle 树按前缀树(prefix tree)方式构造,理论上可表示 2²⁵⁶ 个条目。为了直观,文档使用 4 位键举例。下图展示了一棵含 3 个条目(键为0b01000b10000b1011)的树:

一棵 2²⁵⁶ 规模的树显然无法直接表示,因此采用两项优化(假设实际场景中树总是极端稀疏的):

  1. 占位节点压缩:完全由空节点构成的子树被替换为一个占位节点(见下图);结合下文可知,占位节点的值由字节串b"SPARSE_MERKLE_PLACEHOLDER_HASH"补零至 32 字节得到。

  1. 单叶子树压缩:只含一片叶子的子树被直接替换为对应的叶子节点(见下图)。

叶子节点的值定义为SparseMerkleLeafNode对象的哈希:

struct SparseMerkleLeafNode { /// The 256-bit key is usually generated by hashing the original key, for /// the purpose of making sure the keys are evenly distributed, so a /// HashValue is used here. key: HashValue, /// Hash of the object. The reason of this being the hash of the value /// instead of the value itself is that, in some cases for a non-membership /// proof, this needs to be presented to the client. Using a hash avoids the /// need to present the client the entire blob, which could potentially be /// large. See later section on proof format for more details. value_hash: HashValue, }

与 Merkle 累加器一致:内部节点的值是左右子节点哈希拼接后的哈希,根节点的值即树的根哈希。

API

稀疏 Merkle 树支持以下操作:

trait SparseMerkleTree<T> { /// Inserts a new key-value pair into the tree if the key did not exist, /// otherwise update the value corresponding to the key. fn put(&self, key: HashValue, value: T); /// Queries the object indexed by the key. Returns a tuple where the first /// element is the object, None if the key does not exist. The second /// element is the proof. fn get_with_proof(&self, key: HashValue) -> (Option<T>, SparseMerkleProof); /// Similar to get_with_proof but queries a range of objects. fn get_range_with_proof(&self, start_key: HashValue, end_key: HashValue) -> (Vec<T>, SparseMerkleRangeProof); }

原文档同时指出:虽然支持从树中删除条目,但Diem 协议目前并不使用删除功能

SparseMerkleProof 的格式与验证
格式

稀疏 Merkle 树同时支持成员证明(membership proof)非成员证明(non-membership proof)

struct SparseMerkleProof { leaf: Option<SparseMerkleLeafNode>, siblings: Vec<HashValue>, }

成员证明场景:当被查询的键存在于树中时,证明与AccumulatorProof非常相似——siblings是从对象到根节点路径上的所有兄弟节点;leafSome(node),其中 node 正是被查询的叶子节点。严格来说此时leaf并非必需(键已知,值哈希可由对象算出),但保留它可以使成员证明与非成员证明的格式统一。

仍以上面那棵 3 条目的树为例:若客户端查询键0b1011,服务器返回对象O,证明中的leafSome(0b1011, Hash(O))siblings[A, B, C],如下图所示:

非成员证明场景:当被查询的键不存在时,从根节点沿着键指定的路径向下,根据最终遇到的叶子节点有两种情况:

  1. 若查询的键是0b1100,沿路径最终到达节点B(一个占位节点)。这说明树中没有任何键具有0b11前缀,也就意味着0b1100不存在。此时leafNonesiblings[X, C]
  2. 若查询的键是0b1010,沿路径最终到达节点O(有数据,键为0b1011)。这说明0b1011是该子树中唯一的键,因此0b1010不存在——否则此位置会出现一个内部节点而非叶子O。此时leafSome(0b1011, Hash(O))siblings[A, B, C]。注意这里使用的是对象O哈希而非O本身,这可以在对象很大时显著减小证明体积。
验证流程

验证时假定:

  1. 客户端已获得该稀疏 Merkle 树的根哈希H
  2. 客户端查询键K

服务器返回对象O(或None)与证明后,客户端按以下步骤验证:

  1. 若服务器声称键存在proof.leaf必须是Some。检查leaf.key是否等于Kleaf.value_hash是否等于对象的哈希,然后以Hash(leaf)作为叶子节点的值。
  2. 若服务器声称键不存在
    • a) 若proof.leafSome:检查leaf.key不等于K,并检查Kleaf.key公共前缀位长度不小于证明中兄弟节点的数量(这保证了该叶子确实位于键K的搜索路径末端,而非被任意修剪);然后以Hash(leaf)作为叶子节点的值。
    • b) 若proof.leafNone:以占位节点的值作为叶子节点的值。
  3. 利用键K,判断每个兄弟节点是父节点的左孩子还是右孩子;
  4. 有了叶子节点值后,将其与兄弟节点逐层合并,重建根节点得到根哈希;
  5. 将计算出的根哈希与已知根哈希H比较:当且仅当两者相等时,对象与证明才被信任

验证伪代码如下:

fn verify_sparse_merkle_proof( key: HashValue, known_root_hash: HashValue, object: Option<Object>, proof: SparseMerkleProof, ) -> Result<()> { // Since the depth of the sparse Merkle tree is at most 256 (the length of // `HashValue` in bits), the proof should not consist of more than 256 // siblings. Reject very long proofs immediately to avoid wasting further // computation. ensure!(proof.siblings.len() <= 256); let leaf = match object { Some(obj) => { match proof.leaf { Some(leaf_node) => { ensure!(leaf_node.key == key); ensure!(leaf_node.value_hash == hash(obj)); hash(leaf_node) } None => bail!("Invalid proof."), } } None => { match proof.leaf { Some(leaf_node) => { ensure!(leaf_node.key != key); ensure!(common_prefix_bits_len(key, leaf_node.key) >= proof.siblings.len()); hash(leaf_node) } None => SPARSE_MERKLE_PLACEHOLDER_HASH, } } }; let mut current_hash = leaf; for sibling in proof.siblings { if sibling.is_left_child(key) { current_hash = hash(sibling || current_hash); } else { current_hash = hash(current_hash || sibling); } } ensure!(current_hash == known_root_hash); Ok(()) }

由于HashValue长度为 256 位(即 32 字节),稀疏 Merkle 树深度最多为 256,因此证明中兄弟节点数超过 256 会被直接拒绝。

Diem 数据结构:由 Merkle 原语构建 LPN

上一节描述了构成 LPN(Diem Payment Network)认证数据结构的两大基本构件。本节说明如何用它们构建 LPN 的具体对象。这些对象的类型定义位于 types/src 下的 Rust 代码中(例如AccountStateBlobTransactionInfo等),并通过 BCS 序列化与上述哈希约定参与认证。

账本状态(Ledger State)

账本状态代表 Diem 生态的事实真相,包括在给定版本下每个用户持有的代币数量。每个验证者(validator)必须知道最新版本的账本状态才能执行新交易。具体而言,账本状态是一个从AccountAddress到二进制 blobAccountStateBlob(表示账户状态)的映射:

type LedgerState = SparseMerkleTree<AccountStateBlob>;

每个AccountAddress被哈希为 256 位的HashValue,然后将每个(hash(address), account_state_blob)元组作为键值对插入稀疏 Merkle 树。基于密码学哈希函数的性质,无论地址如何生成,所得的稀疏 Merkle 树都(以压倒性概率)是平衡的。这棵稀疏 Merkle 树代表整个账本状态,其根哈希即状态根哈希(state root hash)——同一版本下任何账户状态都可以用状态根哈希作为认证器进行验证。

当交易执行后状态树被更新、新树被创建时,高效实现通常会复用前一版本中未变化的部分,形成持久化数据结构(persistent data structure),即写时复制(Copy-on-Write)模式,如下图所示:

账户(Accounts)

逻辑层面,一个账户是 Move 资源和模块的集合;物理层面,一个账户是访问路径(access path)到字节数组值的有序映射。账户存入账本状态时,该有序映射使用BCS(Binary Canonical Serialization)序列化成二进制 blobAccountStateBlob,再插入稀疏 Merkle 树:

type Path = Vec<u8>; struct AccessPath { address: AccountAddress, path: Path, } type AccountState = BTreeMap<Path, Vec<u8>>; struct AccountStateBlob { blob: Vec<u8>, } impl From<AccountState> for AccountStateBlob { fn from(account_state: AccountState) -> Self { Self { blob: bcs::to_bytes(&account_state), } } }

关于访问路径的精确定义,原文档以 "TODO: link to definition of access path" 标注待补,可结合 Move 虚拟机与 Move 框架规范进一步了解。

交易与交易输出(Transaction and Transaction Output)

每笔交易会更新一个或多个账户,从而产生新的账本状态。虽然账本状态的变化是交易的直接输出,但为了客户端查询便利,还额外生成了三类信息:

  1. 交易发出的事件列表(contract events);
  2. 交易执行期间消耗的 gas 数量;
  3. 指示交易执行结果的主要状态码(major status code)。
事件(Events)

每笔交易发出的事件列表存储在独立的 Merkle 累加器MerkleAccumulator<ContractEvent>中。事件按 Move VM 输出的顺序依次追加到累加器。该累加器的根哈希即事件根哈希(event root hash),充当任意事件的认证器。ContractEvent的详细定义见 公共数据结构规范(其V0版本包含keysequence_numbertype_tagevent_data四个字段)。

账本历史(Ledger History)

账本历史由所有已提交的交易及其输出构成。Diem 使用TransactionInfo类型同时表示交易与其输出,因此任意TransactionInfo对象都可以作为对应交易及其输出的认证器——包括执行该交易后每个账户的状态:

struct TransactionInfo { /// The hash of the transaction. transaction_hash: HashValue, /// The root hash of the ledger state at the end of this transaction. state_root_hash: HashValue, /// The root hash of the Merkle accumulator that stores all the events /// emitted by this transaction. event_root_hash: HashValue, /// The amount of gas consumed during this transaction execution. gas_used: u64, /// The major status code that indicates the transaction execution result. major_status: StatusCode, }

账本历史本身是一个 Merkle 累加器。它以单个条目(表示创世交易与创世状态)开始;随着更多交易被提交上链,越来越多的TransactionInfo对象被追加到累加器中:

type LedgerHistory = MerkleAccumulator<TransactionInfo>;

因此,账本历史累加器的根哈希可以认证所有TransactionInfo对象,进而认证链上发生的一切——包括所有交易、当前与历史账本状态、所有发出的事件。网络运行时,共识协议会周期性地分发带签名的LedgerInfo(其中包含该根哈希),客户端即可用它验证任何链接到累加器根的数据。

LedgerInfo及其签名封装LedgerInfoWithSignatures的定义见 公共数据结构规范:LedgerInfo包含commit_info: BlockInfo(含versionexecuted_state_id等)与consensus_data_hash;当超过 2f 个验证者对LedgerInfo签名后,即构成具有法定效力的LedgerInfoWithSignatures(其signatures为按账户地址排序的 Ed25519 签名映射,且要求签名总投票权 > 2f)。

客户端认证实战:三个典型场景

前文已说明 Merkle 累加器与稀疏 Merkle 树两大构件,以及基于它们构建的 Diem 数据结构。下面给出几个客户端利用这些数据结构认证服务器响应的具体示例。

本节始终假定:客户端已获得最新的LedgerInfo及其足够多的签名,因此客户端知道账本历史累加器的最新根哈希(它是LedgerInfo结构的一部分)。

场景一:证明一笔交易

要证明交易T是在版本v提交的交易,使用TransactionInfoWithProof。验证只需两步:先用从根到叶子的累加器证明验证TransactionInfo对象的正确性,再利用其中的交易哈希验证交易本身:

struct TransactionInfoWithProof { // The accumulator proof from the root of the ledger history accumulator to // the `TransactionInfo` object at version `v`. This proves that the // `transaction_info` object below is correct. ledger_info_to_transaction_info_proof: AccumulatorProof, // The `TransactionInfo` object at version `v`. Since it has the hash of the // transaction, verifying that `transaction_info.transaction_hash` matches // the tranasction should convince the verifier that the transaction is // correct. transaction_info: TransactionInfo, }

即:第一层累加器证明(账本历史根 →TransactionInfo)保证了transaction_info对象可信;transaction_info.transaction_hash与交易T的哈希一致,则证明T确实以版本v提交。

场景二:证明一个账户状态

要证明版本v时某账户的状态,使用AccountStateProof。首先按场景一的方式验证版本v处的TransactionInfo对象;然后将TransactionInfo内的状态根哈希与从交易信息到账户的稀疏 Merkle 证明结合,验证账户状态:

struct AccountStateProof { transaction_info_with_proof: TransactionInfoWithProof, transaction_info_to_account_proof: SparseMerkleProof, }

这条链路清晰地展示了两个 Merkle 原语的配合:累加器证明回答"这个版本的TransactionInfo是否真实",稀疏 Merkle 证明回答"在这个状态下该账户的余额/资源是否真实"。二者级联,最终锚定到共识签名的LedgerInfo根哈希上,构成完整的信任链。

场景三:证明事件与历史数据(扩展)

虽然原文档正文以"证明交易"与"证明账户状态"两个示例收尾,但从前文的数据结构可以看出更一般的模式:由于TransactionInfo内含event_root_hash,客户端可以通过"账本历史累加器证明 →TransactionInfo→ 事件累加器证明"的级联,验证任意版本任意事件的真实性;同理,历史版本的账户状态也可以通过历史版本的TransactionInfo.state_root_hash配合该版本的状态树证明来验证。所有验证最终都收敛到同一个共识锚点——被超过 2f 验证者签名的LedgerInfo根哈希。

实现参考:在仓库中定位关键代码

如果你想深入阅读源码,以下路径可供参考:

  • Merkle 累加器核心算法:storage/accumulator/src/lib.rs(追加、证明生成、一致性证明、范围证明;支持HashReader抽象将树与存储解耦);
  • 内存态累加器(InMemoryAccumulator):types/src/proof/accumulator/mod.rs(只存储各满子树的根,最多 O(log n) 个节点;不可变,追加返回新实例);
  • 证明类型定义与深度约束:types/src/proof/definition.rs(AccumulatorProofSparseMerkleProofMAX_ACCUMULATOR_PROOF_DEPTH = 63MAX_ACCUMULATOR_LEAVES = 2^63);
  • 占位符哈希:crates/diem-crypto/src/hash.rs(ACCUMULATOR_PLACEHOLDER_HASHSPARSE_MERKLE_PLACEHOLDER_HASH由字符串补零生成,无已知原像);
  • 哈希与密码学约定:specifications/crypto/README.md(SHA3-256、域分离、BCS 序列化);
  • 相关数据结构定义:公共数据结构规范(AccountAddressHashValueLedgerInfoLedgerInfoWithSignaturesContractEvent等)。

使用注意事项

  • 本文描述的证明验证逻辑要求客户端预先可信地获得根哈希。在 Diem 网络中,这一信任根来自共识层:只有被超过 2f 验证者签名(即总投票权 > 2f)的LedgerInfoWithSignatures才构成可验证的提交承诺(参见 data_structures.md 与共识规范);
  • 累加器证明的兄弟数上限 63 对应MAX_ACCUMULATOR_LEAVES = 2^63,稀疏 Merkle 证明的兄弟数上限 256 对应HashValue的位长——两者都是验证时的强制安全检查,用于抵御超长证明导致的资源浪费;
  • 稀疏 Merkle 树当前不支持删除操作的使用(协议层未启用),状态更新通过写时复制复用未变化子树,形成持久化结构。

通过本文,你应当已经能够读懂 Diem 认证数据结构的完整设计:从 Merkle 累加器与稀疏 Merkle 树两大原语,到账本状态、交易输出与账本历史的组装,再到客户端验证交易与账户状态的具体流程。这套机制是 Diem 区块链"无需信任服务器即可验证一切数据"的基石。

  • 区块链
  • 金融科技

【免费下载链接】diem

Diem’s mission is to build a trusted and innovative financial network that empowers people and businesses around the world.

项目地址:https://gitcode.com/gh_mirrors/di/diem
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询