跳到主要内容

2. MoveVM资源修改和销毁的原理


0. Move中的资源类型

Move 中的资源类型,我们都知道它其实就是 Move 中的一个自定义结构体类型,只不过我们在结构体上加了一些限制:

  1. 资源存储在帐户下。因此,只有在分配帐户后才会存在,并且只能通过该帐户访问。
  2. 一个帐户同一时刻只能容纳一个某类型的资源。
  3. 资源不能被复制,准确的说没有办法去复制资源。
  4. Resource 必需被使用,这意味着必须将新创建的 Resource move 到某个帐户下,从帐户移出的 Resource 必须被解构或存储在另一个帐户下。

1. 资源的创建

1.1 创建资源对应的字节码指令

资源必须存储在账户下,也就是说创建资源的时候,必须要传入一个账户信息,例如下面的代码:

address 0x2 {
module Counter {
struct Counter has key { i: u64 }

public fun publish(account: &signer, i: u64) {
move_to(account, Counter { i })
}

}
}

move_to() 函数负责资源的创建,它有两个参数,账户和需要创建的资源类型。

move_to() 函数原型如下:

native fun move_to<T: key>(account: &signer, value: T);

上面的 publish() 经过编译出的字节码如下:

public publish() {
B0:
0: MoveLoc[0](account: &signer)
1: MoveLoc[1](i: u64)
2: Pack[0](Counter)
3: MoveTo[0](Counter)
4: Ret
}

下面我们对字节码列表中的指令逐一解析,让大家能看明白大致的编译过程。

不过在开始之前,先回顾一下 Move 虚拟机中代表函数栈帧的结构:

文件: language/move-vm/runtime/src/interpreter.rs

struct Frame {
pc: u16,
locals: Locals,
function: Arc<Function>,
ty_args: Vec<Type>,
}

我们看到 Frame 结构体 中有一个 locals 字段,它其实是一个数组,其中保存了函数的局部变量,也就是说,函数执行之前,所有的局部变量要先保存在这个数组中。

函数的实参和函数的局部变量,组合在一起,统称为函数的局部变量。

  1. MoveLoc[0](account: &signer) 指令把函数参数 accountlocals 数组中取出,并放在操作数栈上。
  2. MoveLoc[1](i: u64) 指令把局部变量 ilocals 数组中取出,并放在操作数栈上。现在栈上有两个元素:iaccount
  3. Pack[0](Counter) 指令从操作数栈的栈顶取出一个元素,将它打包成一个结构体对象,并再次放在操作数栈上。现在栈上的两个元素就变成了:Counter结构体对象account
  4. MoveTo[0](Counter) 指令是源码中 move_to() 函数的具体实现。MoveTomove_to() 函数一样接受两个参数,此时操作数栈中正好有两个元素:Counter结构体对象accountMoveTo 指令从栈中取出两个元素并执行执行,最终将 Counter结构体对象 保存到 account 代表的账户下。

经过对上面的字节码指令序列的分析,不难看出最重要的指令就是 MoveTo 指令,是它执行了最重要的资源创建操作。

1.2 虚拟机中 MoveTo 指令的实现

下面我们来对照 Move 虚拟机的代码,分析虚拟机中如何实现 MoveTo 指令。

解释器的总体执行过程:

下面的代码,就是 Move 虚拟机中 MoveTo 指令的解释执行过程:

文件:language/move-vm/runtime/src/interpreter.rs

// sd_idx: 代表了资源对应的结构体类型,在Move虚拟机的结构体定义列表中的索引
// Move虚拟机的结构体定义列表,是虚拟机从Move语言字节码文件中解析得来的
// 实际上资源对应的结构体定义,是编译器在生成字节码文件时写入的信息
Bytecode::MoveTo(sd_idx) => {

// 从操作数栈中弹出一个元素:资源对象
let resource = interpreter.operand_stack.pop()?;
// 从操作数栈中弹出一个元素:账户
let signer_reference = interpreter.operand_stack.pop_as::<StructRef>()?;

// 将操作数栈中弹出的账户对象,转换成 AccountAddress 类型
let addr = signer_reference
.borrow_field(0)?
.value_as::<Reference>()?
.read_ref()?
.value_as::<AccountAddress>()?;

// 使用sd_idx结构体索引,到结构体定义表中查询结构体类型
let ty = resolver.get_struct_type(*sd_idx);

// 调用解释器的 move_to 函数,传入数据存储 data_store,账户地址,资源类型,资源对象
let size = interpreter.move_to(data_store, addr, &ty, resource)?;

gas_status.charge_instr_with_size(Opcodes::MOVE_TO, size)?;
}

从上面Move虚拟机中 MoveTo 指令的代码实现入口就可以看出,move_to() 函数先从操作数栈上弹出两个元素:账户和资源对象,然后调用 interpreter.move_to() 函数实现真正的 MoveTo 功能。

interpreter.move_to() 的代码如下:

文件:language/move-vm/runtime/src/interpreter.rs

fn move_to(
&mut self,
data_store: &mut impl DataStore,
addr: AccountAddress,
ty: &Type,
resource: Value,
) -> PartialVMResult<AbstractMemorySize<GasCarrier>> {
let size = resource.size();
Self::load_resource(data_store, addr, ty)?.move_to(resource)?;
Ok(size)
}

查询账户下的资源:

我们可以看到,核心的代码,就是下面这行:

Self::load_resource(data_store, addr, ty)?.move_to(resource)?;

上面这行代码的含义是,首先根据 addr 即账户地址,查询到该账户下的资源,再调用资源的 move_to() 函数,将新的资源保存。

Self::load_resource() 函数最终会调用到 DataStore 类型的 load_resource() 函数:

文件:language/move-vm/runtime/src/interpreter.rs

impl<'r, 'l, S: MoveResolver> DataStore for TransactionDataCache<'r, 'l, S> {
fn load_resource(
&mut self,
addr: AccountAddress,
ty: &Type,
) -> PartialVMResult<&mut GlobalValue> {
// 从 self.account_map 中使用 addr 查询该账户地址,是否之前已经查询过,查询过则返回 AccountDataCache
let account_cache = Self::get_mut_or_insert_with(&mut self.account_map, &addr, || {
(addr, AccountDataCache::new())
});

// 账户的 AccountCache 保存了很多账户中很多类型的数据,下面一行代码使用 资源 的类型查询,账户下是否有该资源
// 如果没有对应的资源,则将资源对应的结构体的类型,和用户组成的 路径 从底层数据库中读取数据
// 如果能读取数据到则首先反序列化为 Value 类型,然后将 Value 类型包装为 GlobalValue 类型返回
if !account_cache.data_map.contains_key(ty) {
// 使用资源对应的结构体类型作为参数,在字节码的loader中查询结构体的Tag
let ty_tag = match self.loader.type_to_type_tag(ty)? {
TypeTag::Struct(s_tag) => s_tag,
_ =>
// non-struct top-level value; can't happen
{
return Err(PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR))
}
};
let ty_layout = self.loader.type_to_type_layout(ty)?;

// 将结构体的Tag和账户地址组合,作为路径,查询账户下的资源
let gv = match self.remote.get_resource(&addr, &ty_tag) {
Ok(Some(blob)) => {
let val = match Value::simple_deserialize(&blob, &ty_layout) {
Some(val) => val,
None => {
let msg =
format!("Failed to deserialize resource {} at {}!", ty_tag, addr);
return Err(PartialVMError::new(
StatusCode::FAILED_TO_DESERIALIZE_RESOURCE,
)
.with_message(msg));
}
};

GlobalValue::cached(val)? // 查到了返回 GlobalValue::Cached
}
Ok(None) => GlobalValue::none(), // 没查到返回 GlobalValue::None
Err(err) => {
let msg = format!("Unexpected storage error: {:?}", err);
return Err(
PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR)
.with_message(msg),
);
}
};

// 查询到了资源之后,将资源对象放在账户地址对应的 account_cache 中
account_cache.data_map.insert(ty.clone(), (ty_layout, gv));
}

// 返回这个资源对应的 GlobalValue 的可变引用
Ok(account_cache
.data_map
.get_mut(ty)
.map(|(_ty_layout, gv)| gv)
.expect("global value must exist"))
}
}

函数的核心功能,在代码中已经有注释,其中最关键的是 self.remote.get_resource(&addr, &ty_tag) 这一行代码,它将账户对应的 addr 和资源对应的结构体 tag,组成一个路径去底层数据库中查询数据。

以本地磁盘中查询数据举例:

文件:language/tools/move-cli/src/sandbox/utils/on_disk_state_view.rs

impl ResourceResolver for OnDiskStateView {
type Error = anyhow::Error;

fn get_resource(
&self,
address: &AccountAddress,
struct_tag: &StructTag,
) -> Result<Option<Vec<u8>>, Self::Error> {
self.get_resource_bytes(*address, struct_tag.clone())
}
}

OnDiskStateView 类实现了 get_resource() 函数,核心的一行代码是:self.get_resource_bytes(*address, struct_tag.clone())

OnDiskStateView 的函数 get_resources_bytes() 实现了查询数据功能:

文件:language/tools/move-resource-viewer/src/lib.rs

pub fn get_resource_bytes(&self, addr: AccountAddress, tag: StructTag) -> Result<Option<Vec<u8>>> {
Self::get_bytes(&self.get_resource_path(addr, tag))
}

get_resource_bytes() 函数首先调用 get_resource_path() 把账户的地址和资源结构体的Tag组织成路径参数,组合的过程如下:

文件:./language/tools/move-resource-viewer/src/lib.rs

fn get_resource_path(&self, addr: AccountAddress, tag: StructTag) -> PathBuf {
let mut path = self.get_addr_path(&addr);
path.push(RESOURCES_DIR);
path.push(StructID(tag).to_string());
path.with_extension(BCS_EXTENSION)
}

然后 get_resource_bytes() 函数直接从文件系统读取数据:

文件:language/tools/move-cli/src/sandbox/utils/on_disk_state_view.rs

fn get_bytes(path: &Path) -> Result<Option<Vec<u8>>> {
Ok(if path.exists() {
// 从磁盘读取文件,并返回 u8 字节数据
Some(fs::read(path)?)
} else {
None
})
}

到这里,我们了解到了 MoveTo 指令首先根据账户地址和对应的资源结构体类型,组合成一个路径,从磁盘中查询对应的数据。

将资源对象保存:

回到刚开始的代码,我们知道 MoveTo 指令的核心分为两部,第一步是把账户下的资源从内存中的缓存或磁盘中读取出来,第二步是将当前需要保存的 resource 保存。

Self::load_resource(data_store, addr, ty)?.move_to(resource)?;

通过上一部分的代码,我们知道了 Self::load_resource() 返回的是 GlobalValue 类型的数据,GlobalValue 类型结构如下:

#[derive(Debug)]
pub struct GlobalValue(GlobalValueImpl);

GlobalValue 结构体类型只保存一个类型: GlobalValueImpl

GlobalValueImpl 是一个枚举类型:

enum GlobalValueImpl {
// 没有保存任何资源
None,

// 资源已经保存在GlobalValue中,但是还没有持久化到存储
Fresh {
fields: Rc<RefCell<Vec<ValueImpl>>>,
},

// 资源已经保存在Global中,也在持久化的存储中
// status 字段指示资源可能已被更改
Cached {
fields: Rc<RefCell<Vec<ValueImpl>>>,
status: Rc<RefCell<GlobalDataStatus>>,
},

// 资源已经在存储中持久化,但是被当前交易标记为删除
Deleted,
}

我们可以看到,GlobalValueImpl 类型依靠 ValueImpl 类型保存真正的数据:

enum ValueImpl {
Invalid,

U8(u8),
U64(u64),
U128(u128),
Bool(bool),
Address(AccountAddress),

Container(Container),

ContainerRef(ContainerRef),
IndexedRef(IndexedRef),
}

枚举类型 ValueImpl 代表了真正保存的数据,出来基本类型以外,最重要的是 ContainerContainerRefIndexedRef,这几个类型在实现 Move 中借用的功能时,发挥了重要的作用。

Move 中借用的实现分两部分,一部分是编译器根据编译过程中的IR,分析出的借用依赖图,来判断是否有非法的借用,而虚拟机中的借用功能,则实现了对象的实际借用过程。

Move 中借用的实现这里只是顺带提及,之后会有其他文章详细分析借用的实现。

我们回到 GlobalValue 类型的 move_to() 函数中:

文件:language/move-vm/runtime/src/interpreter.rs

fn move_to(&mut self, val: ValueImpl) -> PartialVMResult<()> {
match self {
// 之前查出数据,类型是 Self::Cached,说明已经账户下已经有该资源,不能再次 move_to
Self::Fresh { .. } | Self::Cached { .. } => {
return Err(PartialVMError::new(StatusCode::RESOURCE_ALREADY_EXISTS))
}

// 之前未查出数据,就设置新的值,并标记自身(GlobalValueImpl)为Fresh状态
// Fresh状态在 into_effect() 函数中会被标记为 GlobalValueEffect::Changed
Self::None => *self = Self::fresh(val)?,

// 数据已被标记为 Deleted,move_from() 函数标记
// Dirty 状态也会被标记为 GlobalValueEffect::Changed
Self::Deleted => *self = Self::cached(val, GlobalDataStatus::Dirty)?,
}
Ok(())
}

可以看到比较关键的部分,如果 ValueImpl 对象是 Fresh ,说明账户下已经存在对应的资源,则虚拟机报错:资源已经存在!

如果是 None 说明之前账户下从来没存在过该类型的资源,就执行下面的代码:

*self = Self::fresh(val)

这里的 selfGlobalValueImpl 结构体对象,这行代码是把 GlobalValueImpl 对象设置为 GlobalValueImpl::Fresh,并保存资源对象。

这样我们就把从磁盘中查询到,然后保存在内存中的 GlobalValue 对象设置为 GlobalValueImpl::Fresh 并保存了实际的资源。

Move虚拟机保存资源:

虽然已经执行了 Move 的解释器已经执行了 move_to() 函数,但目前新的资源还保存在内存中,还需要在交易执行结束时,保存到磁盘。

在文件 sandbox/commands/run.rs 文件中,有函数 run():

pub fn run() {
// 读取字节码文件
let bytecode = if is_bytecode_file(script_path) {...};

// 创建需要传递给 Move VM 的参数
let vm_args: Vec<Vec<u8>> = convert_txn_args(txn_args);

// 创建 Move VM 实例
let vm = MoveVM::new(natives).unwrap();

// 传入状态存储对象,创建 Move VM 会话
let mut session: Session<OnDiskStateView> = vm.new_session(state);

// 执行 Move 脚本
session.execute_script(
bytecode.to_vec(),
vm_type_args.clone(),
vm_args,
&mut gas_status,
);

// 完成 Move VM 的执行
// 拿到内存保存的所有账户下的 GlobalValue 数据对应的修改类型
// GlobalValueEffect::Deleted 或者 GlobalValueEffect::Changed
// 以及修改类型对应的数据
let (changeset, events) = session.finish().map_err(|e| e.into_vm_status())?;

// 把内存 changeset 中的数据保存到磁盘
maybe_commit_effects(!dry_run, changeset, events, state)
}

在 Move VM 执行脚本的整个过程中,在最后的阶段,会调用 session.finish() 把内存中的数据保存到磁盘:

pub fn finish(self) -> VMResult<(ChangeSet, Vec<Event>)> {
self.data_cache
.into_effects()
.map_err(|e| e.finish(Location::Undefined))
}

上面的代码中,self.data_cache.into_effects() 是最重要的函数,最终调用的是 TransactionDataCache 类型的 into_effects() 函数:

文件:language/move-vm/runtime/src/data_cache.rs

pub(crate) fn into_effects(self) -> PartialVMResult<(ChangeSet, Vec<Event>)> {
// change_set 用于保存账户地址对应的修改集列表
let mut change_set = ChangeSet::new();

// 循环所有账户的账户缓存 AccountDataCache
for (addr, account_data_cache::AccountDataCache) in self.account_map.into_iter() {
let mut resources = BTreeMap::new();

// 循环单个账户下缓存中的所有 GlobalValue
for (ty, (layout, gv::GlobalValue)) in account_data_cache.data_map {
// 调用 GlobalValue 的 into_effect() 函数返回 GlobalValue 对应的修改类型
match gv.into_effect()? {
GlobalValueEffect::None => (), // 什么都不做
GlobalValueEffect::Deleted => {
// 标记为删除
resources.insert(struct_tag, None); // 资源类型: None
}
GlobalValueEffect::Changed(val) => {
// 标记为已被修改
let resource_blob = val
.simple_serialize(&layout)
.ok_or_else(|| PartialVMError::new(StatusCode::INTERNAL_TYPE_ERROR))?;
resources.insert(struct_tag, Some(resource_blob)); // 资源类型:修改后的数据
}
}
// 为每个 change_set 关联账户地址
change_set.publish_or_overwrite_account_change_set(
addr,
AccountChangeSet::from_modules_resources(modules, resources),
);
}
}

// 返回 change_set
Ok((change_set, events))
}

上面的函数 TransactionDataCache.into_effects() 循环所有账户的缓存中的 GlobalValue,并调用 GlobalValue.into_effects(),目的是把 GlobalValue 对应的枚举类型,变成 GlobalValueEffect 对应的类型。

GlobalValue.into_effects() 函数:

文件:language/move-vm/types/src/values/values_impl.rs

pub fn into_effect(self) -> PartialVMResult<GlobalValueEffect<Value>> {
Ok(match self.0.into_effect()? {
GlobalValueEffect::None => GlobalValueEffect::None,
GlobalValueEffect::Deleted => GlobalValueEffect::Deleted,
GlobalValueEffect::Changed(v) => GlobalValueEffect::Changed(Value(v)),
})
}

核心的函数是这一行:

self.0.into_effect()

上面这行代码,实际调用的是 ValueImpl.into_effects() 函数:

文件:language/move-vm/types/src/values/values_impl.rs

fn into_effect(self) -> PartialVMResult<GlobalValueEffect<ValueImpl>> {
Ok(match self {
Self::None => GlobalValueEffect::None,
Self::Deleted => GlobalValueEffect::Deleted,
Self::Fresh { fields } => {
// 新的 GlobalValue 类型数据,即该账户下第一次创建对应类型的资源
GlobalValueEffect::Changed(ValueImpl::Container(Container::Struct(fields)))
}
Self::Cached { fields, status } => match &*status.borrow() {
GlobalDataStatus::Dirty => {
GlobalValueEffect::Changed(ValueImpl::Container(Container::Struct(fields)))
}
GlobalDataStatus::Clean => GlobalValueEffect::None,
},
})
}

可以看到 GlobalValue::Fresh 类型的枚举,会返回 GlobalValueEffech::Changed 类型的枚举,并把实际的数据 fields 保存。

之前的内容中提到,GlobalValue::Fresh 代表该账户下第一次创建对应类型的资源。 

到这里 Move VM 执行过程中的 finish() 函数执行完毕,拿到了所有用户的资源的修改记录:

let (changeset, events) = session.finish().map_err(|e| e.into_vm_status())?;

最后要做的,就是把这些修改记录保存到磁盘中:

maybe_commit_effects(!dry_run, changeset, events, state)

maybe_commit_effects 函数判断,只要不是 dry_run,就循环修改集中的每个元素,把它写入到磁盘:

文件:language/tools/move-cli/src/sandbox/utils/mod.rs

pub(crate) fn maybe_commit_effects(
commit: bool,
changeset: ChangeSet,
events: Vec<Event>,
state: &OnDiskStateView,
) -> Result<()> {
if commit {
for (addr, account) in changeset.into_inner() {
for (struct_tag, blob_opt) in account.into_resources() {
match blob_opt {
Some(blob) => state.save_resource(addr, struct_tag, &blob)?,
None => state.delete_resource(addr, struct_tag)?,
}
}
}

for (event_key, event_sequence_number, event_type, event_data) in events {
state.save_event(&event_key, event_sequence_number, event_type, event_data)?
}
}

Ok(())
}

如果修改集存在要数据,说明要保存最新的数据:

Some(blob) => state.save_resource(addr, struct_tag, &blob)

如果修改集为空,说明要删除数据:

None => state.delete_resource(addr, struct_tag)

save_resource() 函数保存数据:

文件:language/tools/move-cli/src/sandbox/utils/on_disk_state_view.rs

pub fn save_resource(&self, addr: AccountAddress, tag: StructTag, bcs_bytes: &[u8]) -> Result<()> {
let path = self.get_resource_path(addr, tag);
if !path.exists() {
fs::create_dir_all(path.parent().unwrap())?;
}
Ok(fs::write(path, bcs_bytes)?)
}

delete_resource() 函数删除数据:

文件:language/tools/move-cli/src/sandbox/utils/on_disk_state_view.rs

pub fn delete_resource(&self, addr: AccountAddress, tag: StructTag) -> Result<()> {
let path = self.get_resource_path(addr, tag);
fs::remove_file(path)?;

// delete addr directory if this address is now empty
let addr_path = self.get_addr_path(&addr);
if addr_path.read_dir()?.next().is_none() {
fs::remove_dir(addr_path)?
}
Ok(())
}

到目前为止,在某个账户下创建资源的源码分析完毕。