Metaplex 的底层原理:插件如何实现轻量级、细粒度的状态管理
- 原文链接:medium.com/metaplex...
- 译者:AI翻译官,校对:翻译小组
- 本文链接:learnblockchain.cn/article…
今天,我们将深入探讨 Metaplex 核心资产如何提供完全细粒度的权限和无尽的自定义选项,同时保持非常轻量。
我们将关注:
在与 Keith(又名 Blockiosaurus)的一次对话中,得到了一个关于核心构建的有趣见解:“从根本上讲,链上数据只是一种‘blob’——一组我们通常赋予特定含义或结构的非结构化字节。”
传统上,协议将这些 blobs 锁定到固定结构中,以便更容易处理。但当你在制定一个真正能够与数字资产的未来共同发展的标准时,你不能仅仅遵循旧规则——你必须挑战现状。
听到这个传说让我产生了好奇。我想更深入地了解核心背后的设计选择,并理解将链上数据视为流动“blobs”的概念是如何被利用来构建一个灵活的、基于插件的系统,以服务于广泛的用例。
在 SPL Token 程序之上为 NFT 构建代币元数据的过程中,暴露了一个重大限制:通过创建新账户来添加自定义行为使标准变得庞大、复杂且难以使用。每个额外的账户都成为一个障碍,减缓了创新并使整个系统的灵活性降低。
经验教训: 新标准需要是一个单一账户——一个可以同时容纳与资产相关的数据和任何自定义行为的账户。
但是如何构建一个标准,利用将每一片链上数据视为流动 blob 的“疯狂想法”,并用来创建一个处理成千上万种不同自定义和用例的单账户解决方案呢?
每个核心状态账户,如 AssetV1
和 CollectionV1
,由两个主要部分组成:
插件元数据是实际魔法发生的地方。为了帮助我们理解这部分元数据从何而起,在 DataBlob
特性中有一个方便的辅助函数,称为 get_size
。这个函数通过将 AssetV1
结构的基础长度与资产的名称和 URI 的长度相加,再加上任何可选序列,来计算资产的大小。
// mpl-core/src/state/asset.rs
impl DataBlob for AssetV1 {
fn get_size(&self) -> usize {
let mut size = AssetV1::BASE_LENGTH + self.name.len() + self.uri.len();
if self.seq.is_some() {
size += size_of::<u64>();
}
size
}
}
mpl-core/programs/mpl-core/src/state/asset.rs at main · metaplex-foundation/mpl-core
这个 get_size
函数随后在 create_meta_idempotent
函数内部使用,以通过比较资产的大小与 DataBlob
的大小,检查插件元数据是否已经被创建。
// mpl-core/src/plugins/utils.rs
pub fn create_meta_idempotent<'a, T: SolanaAccount + DataBlob>(
account: &AccountInfo<'a>,
payer: &AccountInfo<'a>,
system_program: &AccountInfo<'a>,
) -> Result<(T, PluginHeaderV1, PluginRegistryV1), ProgramError> {
let core = T::load(account, 0)?;
let header_offset = core.get_size();
// 检查插件头和注册表是否存在。
if header_offset == account.data_len() {
// 它们不存在,所以创建它们。
/* .. */
} else {
// 它们存在,所以加载它们。
/* .. */
}
}
mpl-core/programs/mpl-core/src/plugins/utils.rs at main · metaplex-foundation/mpl-core
在插件元数据的开头,是 PluginHeader
。这个头部包含 plugin_registry_offset
——一个指针,用于指示插件注册表在账户中的位置。
// programs/mpl-core/src/plugins/plugin_header.rs
pub struct PluginHeaderV1 {
/// 该头的歧视符,同时也是插件元数据版本。
pub key: Key, // 1
/// 存储在账户末尾的插件注册表的偏移。
pub plugin_registry_offset: usize, // 8
}
mpl-core/programs/mpl-core/src/plugins/plugin_header.rs at main · metaplex-foundation/mpl-core
插件注册表是所有重要操作发生的地方。它存储了一个 RegistryRecord
和 ExternalRegistryRecord
的向量,存储有关可用插件及其必要信息的详细信息。
// programs/mpl-core/src/plugins/plugin_registry.rs
pub struct PluginRegistryV1 {
/// 该头的歧视符,同时也是插件元数据版本。
pub key: Key, // 1
/// 所有插件的注册表。
pub registry: Vec<RegistryRecord>, // 4
/// 所有适配器、第三方插件的注册表。
pub external_registry: Vec<ExternalRegistryRecord>, // 4
}
/* .. */
pub struct RegistryRecord {
/// 插件的类型。
pub plugin_type: PluginType, // 2
/// 有权使用插件的权限。
pub authority: Authority, // 可变
/// 账户中插件的偏移。
pub offset: usize, // 8
}
/* .. */
pub struct ExternalRegistryRecord {
/// 适配器、第三方插件类型。
pub plugin_type: ExternalPluginAdapterType,
/// 外部插件适配器的权限。
pub authority: Authority,
/// 外部插件适配器有效的生命周期事件。
pub lifecycle_checks: Option<Vec<(HookableLifecycleEvent, ExternalCheckResult)>>,
/// 账户中插件的偏移。
pub offset: usize, // 8
/// 对于包含数据的插件,账户中数据的偏移。
pub data_offset: Option<usize>,
/// 对于包含数据的插件,账户中数据的长度。
pub data_len: Option<usize>,
}
mpl-core/programs/mpl-core/src/plugins/plugin_registry.rs at main · metaplex-foundation/mpl-core
在本文的早期,我们提到了状态 Blob 的概念——将链上数据视为可以动态定义的非结构化“Blob”。但是我们还没有谈到如何在设置注册表之外利用这一点。
支持可定制结构的灵活性,而不是将行为强制到刚性格式中,是为 Core 提供细粒度权限和状态管理的原因!
每个插件为权限定义自己的“cookie 政策”,指定谁可以执行特定操作。虽然权限始终存在于注册表中,但其他一切都可以根据需要进行调整。
主要挑战在于确保这些不同的权限和权限不冲突。管理这一复杂性是 Core 验证系统的一部分——这是一个足够详细的话题,值得在后面进行进一步讨论。
现在你对插件的操作有了更清晰的了解,你可能想知道所有这些魔法是如何在 mpl-core
程序中发生的。让我们逐步仔细看看在该系统中管理插件的具体指令。
注意:你即将深入更技术性的领域。但别担心!我会尽量简化每一步及其背后的逻辑,并提供一些视觉辅助!
根据之前关于插件注册表的讨论,检索与资产相关的所有插件列表的方式不应该让人感到惊讶。
这个过程遵循我们在处理插件操作时使用的“基本”事件序列:
1. 获取并加载插件头:使用我们之前讨论过的 get_size()
函数来确定插件头的起始位置。
let header = PluginHeaderV1::load(account, asset.get_size())?;
2. 获取并加载插件注册表:使用插件头中的 plugin_registry_offset
字段来定位插件注册表。
let PluginRegistryV1 { registry, .. } =
PluginRegistryV1::load(account, header.plugin_registry_offset)?;
3. 遍历插件注册表:循环遍历 RegistryRecord
向量,收集 plugin_type
中存在的每个插件,并将其作为响应返回。
Ok(registry
.iter()
.map(|registry_record| registry_record.plugin_type)
.collect()
)
完整指令
// mpl-core/src/plugins/utils.rs
pub fn list_plugins(account: &AccountInfo) -> Result<Vec<PluginType>, ProgramError> {
let asset = AssetV1::load(account, 0)?;
if asset.get_size() == account.data_len() {
return Err(MplCoreError::PluginNotFound.into());
}
let header = PluginHeaderV1::load(account, asset.get_size())?;
let PluginRegistryV1 { registry, .. } =
PluginRegistryV1::load(account, header.plugin_registry_offset)?;
Ok(registry
.iter()
.map(|registry_record| registry_record.plugin_type)
.collect())
}
mpl-core/programs/mpl-core/src/plugins/utils.rs at main · metaplex-foundation/mpl-core
要从注册表中获取特定插件数据,我们从加载插件注册表的地方开始:
1. 在注册表中找到插件:遍历插件注册表,通过匹配 plugin_type
找到你要查找的插件。检索 RegistryRecord
。
let registry_record = registry
.iter()
.find(|record| record.plugin_type == plugin_type)
.ok_or(MplCoreError::PluginNotFound)?;
2. 反序列化插件数据:验证 RegistryRecord
中保存的偏移处的插件是否正确,并通过从标志开始获取所有数据来反序列化插件数据。
let plugin = Plugin::deserialize(&mut &(*account.data).borrow()[registry_record.offset..])?;
if PluginType::from(&plugin) != plugin_type {
return Err(MplCoreError::PluginNotFound.into());
}
let inner = U::deserialize(
&mut &(*account.data).borrow()[registry_record
.offset
.checked_add(1)
.ok_or(MplCoreError::NumericalOverflow)?..],
)?;
3. 返回插件详情:返回从反序列化切片中获取的权限、数据和偏移。
Ok((registry_record.authority, inner, registry_record.offset))
完整指令
// mpl-core/src/plugins/utils.rs
pub fn fetch_plugin<T: DataBlob + SolanaAccount, U: BorshDeserialize>(
account: &AccountInfo,
plugin_type: PluginType,
) -> Result<(Authority, U, usize), ProgramError> {
let asset = T::load(account, 0)?;
if asset.get_size() == account.data_len() {
return Err(MplCoreError::PluginNotFound.into());
}
let header = PluginHeaderV1::load(account, asset.get_size())?;
let PluginRegistryV1 { registry, .. } =
PluginRegistryV1::load(account, header.plugin_registry_offset)?;
// 反序列化插件。
let plugin = Plugin::deserialize(&mut &(*account.data).borrow()[registry_record.offset..])?;
if PluginType::from(&plugin) != plugin_type {
return Err(MplCoreError::PluginNotFound.into());
}
let inner = U::deserialize(
&mut &(*account.data).borrow()[registry_record
.offset
.checked_add(1)
.ok_or(MplCoreError::NumericalOverflow)?..],
)?;
// 返回插件及其权限。
Ok((registry_record.authority, inner, registry_record.offset))
}
mpl-core/programs/mpl-core/src/plugins/utils.rs at main · metaplex-foundation/mpl-core
注意:你可能已经看到 fetch_plugin
函数使用了一个泛型类型 U
来处理插件内部数据的反序列化。这是因为插件的内部数据结构在类型和大小上可能会差异很大,因此这种灵活性对于处理不同的内部表示至关重要。
要从注册表中添加特定插件数据,我们从加载插件注册表的地方开始:
1. 检查重复插件:在添加插件之前,确保注册表中不存在相同类型的插件。如果找到重复项,函数返回错误以避免冲突条目或覆盖。
if plugin_registry
.registry
.iter()
.any(|record| record.plugin_type == plugin_type)
{
return Err(MplCoreError::PluginAlreadyExists.into());
}
2. 计算新的偏移量和大小调整: 通过计算插件注册表的新偏移量来确定新插件数据将存储的位置。此外,计算所需的总大小增加,以适应新插件数据。然后更新头部中的 plugin_registry_offset
,以反映注册表的新位置。
let old_registry_offset = plugin_header.plugin_registry_offset;
let new_registry_record = RegistryRecord {
plugin_type,
offset: old_registry_offset,
authority: *authority,
};
let size_increase = plugin_size
.checked_add(new_registry_record.try_to_vec()?.len())
.ok_or(MplCoreError::NumericalOverflow)?;
let new_registry_offset = plugin_header
.plugin_registry_offset
.checked_add(plugin_size)
.ok_or(MplCoreError::NumericalOverflow)?;
plugin_header.plugin_registry_offset = new_registry_offset;
plugin_registry.registry.push(new_registry_record);
3. 调整或重新分配账户数据: 如果需要,调整或重新分配账户,以容纳新插件数据。这确保有足够的空间来存储更新的数据,而不会造成溢出错误。
let new_size = account
.data_len()
.checked_add(size_increase)
.ok_or(MplCoreError::NumericalOverflow)?;
resize_or_reallocate_account(account, payer, system_program, new_size)?;
4. 保存更新后的状态: 将更新的插件头、序列化的插件数据和更新的插件注册表保存回账户,以完成更改。
plugin_header.save(account, header_offset)?;
plugin.save(account, old_registry_offset)?;
plugin_registry.save(account, new_registry_offset)?;
完整指令
// mpl-core/src/plugins/utils.rs
pub fn initialize_plugin<'a, T: DataBlob + SolanaAccount>(
plugin: &Plugin,
authority: &Authority,
plugin_header: &mut PluginHeaderV1,
plugin_registry: &mut PluginRegistryV1,
account: &AccountInfo<'a>,
payer: &AccountInfo<'a>,
system_program: &AccountInfo<'a>,
) -> ProgramResult {
let core = T::load(account, 0)?;
let header_offset = core.get_size();
let plugin_type = plugin.into();
let plugin_data = plugin.try_to_vec()?;
let plugin_size = plugin_data.len();
// 不能添加重复的插件。
if plugin_registry
.registry
.iter()
.any(|record| record.plugin_type == plugin_type)
{
return Err(MplCoreError::PluginAlreadyExists.into());
}
let old_registry_offset = plugin_header.plugin_registry_offset;
let new_registry_record = RegistryRecord {
plugin_type,
offset: old_registry_offset,
authority: *authority,
};
let size_increase = plugin_size
.checked_add(new_registry_record.try_to_vec()?.len())
.ok_or(MplCoreError::NumericalOverflow)?;
let new_registry_offset = plugin_header
.plugin_registry_offset
.checked_add(plugin_size)
.ok_or(MplCoreError::NumericalOverflow)?;
plugin_header.plugin_registry_offset = new_registry_offset;
plugin_registry.registry.push(new_registry_record);
let new_size = account
.data_len()
.checked_add(size_increase)
.ok_or(MplCoreError::NumericalOverflow)?;
resize_or_reallocate_account(account, payer, system_program, new_size)?;
plugin_header.save(account, header_offset)?;
plugin.save(account, old_registry_offset)?;
plugin_registry.save(account, new_registry_offset)?;
Ok(())
}
mpl-core/programs/mpl-core/src/plugins/utils.rs at main
要从注册表中删除特定的插件数据,我们从加载插件注册表的地方开始:
plugin_registry
,找到与要删除的 plugin_type
对应的 RegistryRecord
。如果未找到插件,则返回错误。
if let Some(index) = plugin_registry
.registry
.iter_mut()
.position(|record| record.plugin_type == *plugin_type)
{
let registry_record = plugin_registry.registry.remove(index);
let serialized_registry_record = registry_record.try_to_vec()?;
2. 检索并删除插件数据: 获取要删除插件的偏移量并加载插件数据。计算偏移量和大小,以确定将释放多少空间。
let plugin_offset = registry_record.offset;
let plugin = Plugin::load(account, plugin_offset)?;
let serialized_plugin = plugin.try_to_vec()?;
let next_plugin_offset = plugin_offset
.checked_add(serialized_plugin.len())
.ok_or(MplCoreError::NumericalOverflow)?;
3\. **计算账户的新大小:** 在删除插件数据和相关注册记录后,计算账户的新大小。
let new_size = account
.data_len()
.checked_sub(serialized_registry_record.len())
.ok_or(MplCoreError::NumericalOverflow)?
.checked_sub(serialized_plugin.len())
.ok_or(MplCoreError::NumericalOverflow)?;
let new_registry_offset = header
.plugin_registry_offset
.checked_sub(serialized_plugin.len())
.ok_or(MplCoreError::NumericalOverflow)?;
4\. **移动剩余数据以填补空缺:** 在账户中移动剩余数据以填补已删除插件留下的空缺。这一步可以防止出现未使用或浪费的空间。
let data_to_move = header
.plugin_registry_offset
.checked_sub(next_plugin_offset)
.ok_or(MplCoreError::NumericalOverflow)?;
let src = account.data.borrow()[next_plugin_offset..].to_vec();
sol_memcpy(
&mut account.data.borrow_mut()[plugin_offset..],
&src,
data_to_move,
);
5\. **更新剩余记录的偏移量并调整账户大小:** 调整内部和外部注册表中剩余插件的偏移量,以考虑已删除的数据,并调整账户以反映释放的空间。
header.plugin_registry_offset = new_registry_offset;
header.save(account, asset.get_size())?;
// 移动现有注册记录的偏移量。
plugin_registry.bump_offsets(plugin_offset, -(serialized_plugin.len() as isize))?;
plugin_registry.save(account, new_registry_offset)?;
resize_or_reallocate_account(account, payer, system_program, new_size)?;
**完整指令**
pub fn delete_plugin<'a, T: DataBlob>(
plugin_type: &PluginType,
asset: &T,
account: &AccountInfo<'a>,
payer: &AccountInfo<'a>,
system_program: &AccountInfo<'a>,
) -> ProgramResult {
if asset.get_size() == account.data_len() {
return Err(MplCoreError::PluginNotFound.into());
}
```rust
let mut header = PluginHeaderV1::load(account, asset.get_size())?;
let mut plugin_registry = PluginRegistryV1::load(account, header.plugin_registry_offset)?;
if let Some(index) = plugin_registry
.registry
.iter_mut()
.position(|record| record.plugin_type == *plugin_type)
{
let registry_record = plugin_registry.registry.remove(index);
let serialized_registry_record = registry_record.try_to_vec()?;
// Fetch the offset of the plugin to be removed.
let plugin_offset = registry_record.offset;
let plugin = Plugin::load(account, plugin_offset)?;
let serialized_plugin = plugin.try_to_vec()?;
// Get the offset of the plugin after the one being removed.
let next_plugin_offset = plugin_offset
.checked_add(serialized_plugin.len())
.ok_or(MplCoreError::NumericalOverflow)?;
// Calculate the new size of the account.
let new_size = account
.data_len()
.checked_sub(serialized_registry_record.len())
.ok_or(MplCoreError::NumericalOverflow)?
.checked_sub(serialized_plugin.len())
.ok_or(MplCoreError::NumericalOverflow)?;
let new_registry_offset = header
.plugin_registry_offset
.checked_sub(serialized_plugin.len())
.ok_or(MplCoreError::NumericalOverflow)?;
let data_to_move = header
.plugin_registry_offset
.checked_sub(next_plugin_offset)
.ok_or(MplCoreError::NumericalOverflow)?;
let src = account.data.borrow()[next_plugin_offset..].to_vec();
sol_memcpy(
&mut account.data.borrow_mut()[plugin_offset..],
&src,
data_to_move,
);
header.plugin_registry_offset = new_registry_offset;
header.save(account, asset.get_size())?;
// Move offsets for existing registry records.
plugin_registry.bump_offsets(plugin_offset, -(serialized_plugin.len() as isize))?;
plugin_registry.save(account, new_registry_offset)?;
resize_or_reallocate_account(account, payer, system_program, new_size)?;
} else {
return Err(MplCoreError::PluginNotFound.into());
}
Ok(())
mpl-core/programs/mpl-core/src/plugins/utils.rs at main · metaplex-foundation/mpl-core
恭喜! 你现在了解了会让 Core 插件如此特别的一切。这只是驱动 Core 的技术的一部分,这是一项旨在革新我们对数字资产的思考的新 Metaplex 标准。
如果你想了解更多关于 Core 和 Metaplex 的信息,请查看开发者门户: 这里 。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!