Commit 51f3fc2a authored by pengjun's avatar pengjun

add Move

parent f7f3fda1
# Move语言和MoveVM虚拟机
# Move语言和MoveVM虚拟机
## 1. Move语言
### 1.1 What is Move
Move语言是Libra项目推出的智能合约语言,和传统的智能合约语言类似,Move语言主要用来编写Libra交易的执行逻辑
### 1.2 Why Move
Libra旨在建立一个简单的全球货币和金融基础设施,基于此,Move语言和其他智能合约对资产的描述和处理有很大不同。
现实世界中的资产数字化过程中面临着两方面的困难——稀缺性 和 权限控制。而现有的平台,如以太坊、比特币等也同样面临着几个问题:
* 资产的不正规表示(Indirect representation of assets)
* 自定义资产稀缺性描述,在区块链编程语言中得不到良好的支持(Scarcity is not extensible)
* 权限控制功能深植于语言之中,难以自定义(Access control is not flexible)
> 比如Solidity中发布Token,其中的Balance多数是用Integer表示,表意性太差;其次,由于Token自己颁发,几乎所有的内容都需要自己在合约中写出来,由合约所有者维护的,包括其本质的问题:稀缺性和权限控制。一旦出现问题,那么对于这个 Token 而言将是灾难性的。
区块链本身操作的是资产(Asset),资产本身的特性是所有被称为资产的 Class 所共有的逻辑,所以其应该作为一种基本的资源向开发者提供而不是作为一种可供选择的资源。
以此为据,我们可以姑且认为,在区块链的世界中基础的资产类型应当是开发者认为的底层,而不是业务逻辑。因此支撑区块链平台的语言要比以前的编程语言在基础上多了一层对于基础类型的封装 — 即对资产类型的保护。
而 Move 本身的设计目标就是 – 资产是一等公民(First-class assets)、灵活性(Flexibility)、安全性(Safety)、可验证性(Verifiability)
从语义上将资产(Asset)作为其支持的一部分,而不是由用户自定义其基础(稀缺性、权限控制)的实现逻辑。用户只需要自定义自己需要实现的部分即可,如:转账逻辑、退款逻辑等等。更加聚焦于业务而非底层
### 1.3 特点
#### 1.3.1 两大类型
* Unrestricted Value
* Resource
Unrestricted Value 是指那些如:u64 或者 钱包地址 一类的信息,可以被正常的复制和转移。
Resource 则是之前所说的资产(Asset),基于 Linear Logic,就如同货币一样,Resource 不能被复制,只能被转移,并且只能被转移一次;且不能被隐式丢弃
由于move语言这种强资产类型定义,保证了libra上的资产安全
#### 1.3.2 两个程序模型
Move把代码分成两类:
* Module:类似智能合约,可以定义内容的转移,销毁,发布等业务逻辑
* Transaction script:交易指令
比如,Alice 向 Bob 转 100 Libra,这个操作就是 Transaction script,而 100 Libra 和转移过程中要经历怎样的逻辑则是 Modules 的职责了
Transaction script 具有 all or nothing 的特性,即要么都成功,要么都失败,不会存在一种中间状态
#### 1.3.3 两个基本模块
* LibraAccount 账户基本模块
* LibraCoin coin基本模块
### 1.4 语法
```text
1. 基本操作
move:资源或变量的转移
copy:变量的复制
2. 类型
// kind
R resource
V value
// 基本类型
Address,
U64,
Bool,
ByteArray,
String,
// 引用
&t 类型t不可改变的引用
&mut t 类型t可变的引用
3. 内建函数
"create_account"
"release"
"freeze"
"get_txn_gas_unit_price"
"get_txn_max_gas_units"
"get_txn_public_key"
"get_txn_sender"
"get_txn_sequence_number"
"get_gas_remaining"
"get_height"
"emit_event"
4. &引用
&x x的不可变引用
&e.f 结构体e的成员f的引用
balance_value = LibraCoin.value(&move(account).balance);
且account需要是&类型“account: &R#Self.T”
*e 解引用
5. module操作
//也是资源操作,仅在定义了资源t的module中使用
move_to_sender<n>(e) 将类型为n的资源e发布到发送方地址
move_from<n>(e) 移除地址e下的资源n
borrow_global<n>(e) 生成一个不可更改的引用,指向地址e下的资源n
exists<n>(e) 地址e下后是否存在资源n
```
### 1.5 示例
```text
modules:
module Capability {
// Capability is responsible for declaring an account's permissions.
// We define the notion of an owner, minter and blacklisted.
// Only the owner can assign blacklisted and minter capabilities.
// The owner is defined by hardcoding its account before publishing the module.
// -----------------------------------------------------------------
// Declare owner as a resource. It's only meant to be published once
resource Owner { }
// Declare a resource that declares an address's capabilities.
// Every address using EToken will need to publish a resource themselves.
// However, only the owner can change its content.
resource T {
minter: bool,
blacklisted: bool,
}
// Every account should execute this once before using Capability and EToken module.
// If the sender is the hardcoded owner, then owner capability is published.
// Reverts if already published
public publish() {
let sender: address;
sender = get_txn_sender();
// Publish owner capability if sender is the privileged account
// Uncomment the following line in production and use a real owner account: if (move(sender) == 0x0) {
if (true) {
// Always branch to here when testing, otherwise the test can't complete as the sender address is randomly chosen.
Self.grant_owner_capability();
}
// Publish a new capability with no permissions.
move_to_sender<T>(T{ minter: false, blacklisted: false });
return;
}
// Internal function that grants owner capability
grant_owner_capability() {
move_to_sender<Owner>(Owner {});
return;
}
// Grants minter capability to receiver, but can only succeed if sender owns the owner capability.
public grant_minter_capability(receiver: address, owner_capability: &R#Self.Owner) {
let capability_ref: &mut R#Self.T;
release(move(owner_capability));
// Pull a mutable reference to the receiver's capability, and change its permission.
capability_ref = borrow_global<T>(move(receiver));
*(&mut move(capability_ref).minter) = true;
return;
}
// Grants blacklist capability to receiver, but can only succeed if sender owns the owner capability.
public grant_blacklisted_capability(receiver: address, owner_capability: &R#Self.Owner) {
let capability_ref: &mut R#Self.T;
release(move(owner_capability));
// Pull a mutable reference to the receiver's capability, and change its permission.
capability_ref = borrow_global<T>(move(receiver));
*(&mut move(capability_ref).blacklisted) = true;
return;
}
// This returns an immutable reference to the owner capability if it exists.
// Is used by the owner to show ownership to privileged functions.
// Reverts if owner capability does not exist.
public borrow_owner_capability(): &R#Self.Owner {
let sender: address;
let owner_capability_ref: &mut R#Self.Owner;
let owner_capability_immut_ref: &R#Self.Owner;
sender = get_txn_sender();
owner_capability_ref = borrow_global<Owner>(move(sender));
owner_capability_immut_ref = freeze(move(owner_capability_ref));
return move(owner_capability_immut_ref);
}
// This returns an immutable reference to the general capability if it exists.
// Should be used by every account to prove capabilities.
// Reverts if capability does not exist.
public borrow_capability(): &R#Self.T {
let sender: address;
let capability_ref: &mut R#Self.T;
let capability_immut_ref: &R#Self.T;
sender = get_txn_sender();
capability_ref = borrow_global<T>(move(sender));
capability_immut_ref = freeze(move(capability_ref));
return move(capability_immut_ref);
}
// Return whether the capability allows minting.
public is_minter(capability: &R#Self.T): bool {
let is_minter: bool;
is_minter = *(&move(capability).minter);
return move(is_minter);
}
// Return true the capability is not blacklisted.
public is_not_blacklisted(capability: &R#Self.T): bool {
let is_blacklisted: bool;
is_blacklisted = *(&move(capability).blacklisted);
return !move(is_blacklisted);
}
// Reverts if capability does not allow minting
public require_minter(capability: &R#Self.T) {
let is_minter: bool;
is_minter = Self.is_minter(move(capability));
assert(move(is_minter), 0);
return;
}
// Reverts if capability is blacklisted
public require_not_blacklisted(capability: &R#Self.T) {
let is_not_blacklisted: bool;
is_not_blacklisted = Self.is_not_blacklisted(move(capability));
assert(move(is_not_blacklisted), 0);
return;
}
}
module EToken {
// This module is responsible for an actual eToken.
// For it to be useful a capability has to be published by using the Capability module above.
// -----------------------------------------------------------------
import Transaction.Capability;
// Declare the eToken resource, storing an account's total balance.
resource T {
value: u64,
}
// Publishes an initial zero eToken to the sender.
// Should be called once before using this module.
public publish() {
move_to_sender<T>(T{ value: 0 });
return;
}
// Mint new eTokens.
// Reverts if capability does not allow it.
public mint(value: u64, capability: &R#Capability.T): R#Self.T {
Capability.require_minter(move(capability));
return T{value: move(value)};
}
// Returns an account's eToken balance.
// Reverts if an initial eToken hasn't been published.
public balance(): u64 {
let sender: address;
let token_ref: &mut R#Self.T;
let token_value: u64;
sender = get_txn_sender();
token_ref = borrow_global<T>(move(sender));
token_value = *(&move(token_ref).value);
return move(token_value);
}
// Deposit owned tokens to an payee's address, and destroy the tokens to deposit,
// Reverts if user is blacklisted.
public deposit(payee: address, to_deposit: R#Self.T, capability: &R#Capability.T) {
let payee_token_ref: &mut R#Self.T;
let payee_token_value: u64;
let to_deposit_value: u64;
Capability.require_not_blacklisted(move(capability));
payee_token_ref = borrow_global<T>(move(payee));
payee_token_value = *(&copy(payee_token_ref).value);
// Unpack and destroy to_deposit tokens
T{ value: to_deposit_value } = move(to_deposit);
// Increase the payees balance with the destroyed token amount
*(&mut move(payee_token_ref).value) = move(payee_token_value) + move(to_deposit_value);
return;
}
// Withdraw an amount of tokens of the sender and return it.
// This works by splitting the token published and returning the specified amount as tokens.
public withdraw(amount: u64, capability: &R#Capability.T): R#Self.T {
let sender: address;
let sender_token_ref: &mut R#Self.T;
let value: u64;
Capability.require_not_blacklisted(move(capability));
sender = get_txn_sender();
sender_token_ref = borrow_global<T>(move(sender));
value = *(&copy(sender_token_ref).value);
// Make sure that sender has enough tokens
assert(copy(value) >= copy(amount), 1);
// Split the senders token and return the amount specified
*(&mut move(sender_token_ref).value) = move(value) - copy(amount);
return T{ value: move(amount) };
}
}
script:
// Performs simple testing to crudely verify the published modules above.
import Transaction.Capability;
import Transaction.EToken;
main() {
let sender: address;
let owner_capability: &R#Capability.Owner;
let capability: &R#Capability.T;
let minted_tokens: R#EToken.T;
let balance: u64;
sender = get_txn_sender();
// Publish initial capability
Capability.publish();
// Borrow owner_capability for minter delegation
owner_capability = Capability.borrow_owner_capability();
// Delegate itself as a minter
Capability.grant_minter_capability(copy(sender), move(owner_capability));
// Borrow general capability for proof of minting capability
capability = Capability.borrow_capability();
// Publish an eToken account
EToken.publish();
// Mint 100 eTokens and prove minter capability
minted_tokens = EToken.mint(100, copy(capability));
// Deposit the freshly minted tokens to itself
EToken.deposit(move(sender), move(minted_tokens), move(capability));
// Test that the balance corresponds with the intended behaviour
balance = EToken.balance();
assert(move(balance) == 100, 3);
return;
}
```
### 1.6 现状
>In the longer term, Move must be capable of encoding the rich variety of assets and corresponding business logic that make up a financial infrastructure.
– Move: A Language With Programmable Resources
当前版本的move语言还不成熟,支持的功能相对比较简单,包括语法和测试都还有改进的地方
## 2. Move虚拟机
### 2.1 交易生命周期
![validator-sequence](validator-sequence.svg)
MoveVM不仅支持Move脚本的解析执行,还支持交易的验证
### 2.2 language目录结构
```
├── README.md # This README
├── benchmarks # Benchmarks for the Move language VM and surrounding code
├── bytecode_verifier # The bytecode verifier
├── e2e_tests # Infrastructure and tests for the end-to-end flow
├── functional_tests # Testing framework for the Move language
├── compiler # The IR to Move bytecode compiler
├── stdlib # Core Move modules and transaction scripts
├── test.sh # Script for running all the language tests
└── vm
├── cost_synthesis # Cost synthesis for bytecode instructions
├── src # Bytecode language definitions, serializer, and deserializer
├── tests # VM tests
├── vm_genesis # The genesis state creation, and blockchain genesis writeset
└── vm_runtime # The bytecode interpreter
```
### 2.3 compiler工具
compiler将move语言脚本编译成字节码,同时对脚本语法做简单的校验。高级的语义检查由bytecode verifier完成。
```
CompiledProgram: {
Modules: [
CompiledModule: {
Module Handles: [
0x0.Capability,]
Struct Handles: [
resource Owner@0x0.Capability,
resource T@0x0.Capability,]
Function Handles: [
0x0.Capability.publish(): (),
0x0.Capability.grant_owner_capability(): (),
0x0.Capability.grant_minter_capability(Address, &resource Owner@0x0.Capability): (),
0x0.Capability.grant_blacklisted_capability(Address, &resource Owner@0x0.Capability): (),
0x0.Capability.borrow_owner_capability(): (&resource Owner@0x0.Capability),
0x0.Capability.borrow_capability(): (&resource T@0x0.Capability),
0x0.Capability.is_minter(&resource T@0x0.Capability): (Bool),
0x0.Capability.is_not_blacklisted(&resource T@0x0.Capability): (Bool),
0x0.Capability.require_minter(&resource T@0x0.Capability): (),
0x0.Capability.require_not_blacklisted(&resource T@0x0.Capability): (),]
Struct Definitions: [
{resource Owner@0x0.Capability},
{resource T@0x0.Capability
resource T@0x0.Capability.blacklisted: Bool
resource T@0x0.Capability.minter: Bool},]
Field Definitions: [
resource T@0x0.Capability.blacklisted: Bool,
resource T@0x0.Capability.minter: Bool,]
Function Definitions: [
public 0x0.Capability.publish(): ()
locals(1): Address,
GetTxnSenderAddress
...
```
跟Move脚本的结构列斯,每个Move脚本都会编译成CompiledProgram结构,CompiledProgram主要包含CompiledModule和CompiledScript
### 2.4 测试框架
libra在functional_tests目录中提供了简单的move语言脚本测试框架,开发者可在目录下编写自己的脚本,使用框架编译测试。
测试框架使用的mork执行器在本地执行,不会连接测试网络
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1050px" height="900px" viewBox="0 0 1050 900" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch -->
<title>validator-sequence</title>
<desc>Created with Sketch.</desc>
<g id="validator-sequence" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect fill="#FFFFFF" x="0" y="0" width="1050" height="900"></rect>
<path d="M354,167 L902,167 C959.989899,167 1007,214.010101 1007,272 L1007,769 C1007,826.989899 959.989899,874 902,874 L354,874 C296.010101,874 249,826.989899 249,769 L249,272 C249,214.010101 296.010101,167 354,167 Z" id="Rectangle" fill="#B3BCC3"></path>
<path id="Line" d="M526.5,287.983652 L474.5,287.983652 L473,287.983652 L473,284.983652 L474.5,284.983652 L526.5,284.983652 L526.5,276.983652 L545.5,286.483652 L526.5,295.983652 L526.5,287.983652 Z" fill="#000000" fill-rule="nonzero"></path>
<path id="Line" d="M746,284.983652 L798,284.983652 L799.5,284.983652 L799.5,287.983652 L798,287.983652 L746,287.983652 L746,295.983652 L727,286.483652 L746,276.983652 L746,284.983652 Z" fill="#000000" fill-rule="nonzero"></path>
<path id="Line" d="M889.316973,643.515752 L889.000073,611.498498 L888.985228,609.998571 L891.985081,609.968879 L891.999927,611.468806 L892.316826,643.48606 L900.316435,643.406882 L891.004949,662.499976 L881.317365,643.59493 L889.316973,643.515752 Z" fill="#000000" fill-rule="nonzero"></path>
<path id="Line" d="M889.316973,409.515752 L889.000073,377.498498 L888.985228,375.998571 L891.985081,375.968879 L891.999927,377.468806 L892.316826,409.48606 L900.316435,409.406882 L891.004949,428.499976 L881.317365,409.59493 L889.316973,409.515752 Z" fill="#000000" fill-rule="nonzero"></path>
<path id="Line" d="M749.5,523.5 L798,523.5 L799.5,523.5 L799.5,526.5 L798,526.5 L749.5,526.5 L749.5,534.5 L730.5,525 L749.5,515.5 L749.5,523.5 Z" fill="#000000" fill-rule="nonzero"></path>
<path id="Line" d="M542.867663,462.903994 L381.896337,288.015838 L380.880498,286.912175 L383.087825,284.880498 L384.103663,285.984162 L545.074989,460.872317 L550.961192,455.454512 L556.838613,475.867888 L536.981459,468.321799 L542.867663,462.903994 Z" fill="#000000" fill-rule="nonzero"></path>
<path id="Line" d="M799.496558,689.317897 L639.927254,526.048435 L638.878819,524.975688 L641.024312,522.878819 L642.072746,523.951565 L801.642051,687.221028 L807.363365,681.629376 L813.849478,701.857582 L793.775245,694.909549 L799.496558,689.317897 Z" fill="#000000" fill-rule="nonzero"></path>
<path id="Line" d="M640.063514,552.118713 L606.142218,588.914695 L612.024168,594.337118 L592.161099,601.867622 L598.054537,581.458864 L603.936486,586.881287 L637.857782,550.085305 L631.975832,544.662882 L651.838901,537.132378 L645.945463,557.541136 L640.063514,552.118713 Z" fill="#000000" fill-rule="nonzero"></path>
<path id="Line" d="M888.31006,165.5173 L888.190051,147.516033 L876.190318,147.596031 L890.496667,118.500011 L905.189674,147.402702 L893.18994,147.4827 L893.309949,165.483967 L905.309682,165.403969 L891.003333,194.499989 L876.310326,165.597298 L888.31006,165.5173 Z" fill="#000000" fill-rule="nonzero"></path>
<path id="Line" d="M635.31006,165.5173 L635.190051,147.516033 L623.190318,147.596031 L637.496667,118.500011 L652.189674,147.402702 L640.18994,147.4827 L640.309949,165.483967 L652.309682,165.403969 L638.003333,194.499989 L623.310326,165.597298 L635.31006,165.5173 Z" fill="#000000" fill-rule="nonzero"></path>
<path d="M145.662951,211.162951 L194.837049,260.337049 C209.286448,274.786448 209.286448,298.213552 194.837049,312.662951 L145.662951,361.837049 C131.213552,376.286448 107.786448,376.286448 93.3370491,361.837049 L44.1629509,312.662951 C29.7135521,298.213552 29.7135521,274.786448 44.1629509,260.337049 L93.3370491,211.162951 C107.786448,196.713552 131.213552,196.713552 145.662951,211.162951 Z" id="Rectangle" fill="#000000"></path>
<text id="Client" font-family="Helvetica-Bold, Helvetica" font-size="25" font-weight="bold" fill="#FFFFFF">
<tspan x="85.2770996" y="295">Client</tspan>
</text>
<g id="Group" transform="translate(291.000000, 194.000000)">
<circle id="Oval" fill="#5645F5" cx="91.9332607" cy="91.9332607" r="91.9332607"></circle>
<text id="Admission-Control" font-family="Helvetica" font-size="25" font-weight="normal" line-spacing="30" fill="#FFFFFF">
<tspan x="33.8400879" y="86">Admission</tspan>
<tspan x="51.2045898" y="116">Control</tspan>
</text>
</g>
<g id="Group" transform="translate(545.000000, 194.000000)">
<circle id="Oval" fill="#5645F5" cx="91.9332607" cy="91.9332607" r="91.9332607"></circle>
<text id="Mempool" font-family="Helvetica" font-size="25" font-weight="normal" fill="#FFFFFF">
<tspan x="40.0900879" y="100">Mempool</tspan>
</text>
</g>
<g id="Group" transform="translate(799.000000, 194.000000)">
<circle id="Oval" fill="#5645F5" cx="91.9332607" cy="91.9332607" r="91.9332607"></circle>
<text id="Consensus" font-family="Helvetica" font-size="25" font-weight="normal" fill="#FFFFFF">
<tspan x="29.4633789" y="100">Consensus</tspan>
</text>
</g>
<g id="Group" transform="translate(799.000000, 428.000000)">
<circle id="Oval" fill="#5645F5" cx="91.9332607" cy="91.9332607" r="91.9332607"></circle>
<text id="Execution" font-family="Helvetica" font-size="25" font-weight="normal" fill="#FFFFFF">
<tspan x="36.6049805" y="100">Execution</tspan>
</text>
</g>
<g id="Group" transform="translate(546.000000, 429.000000)">
<circle id="Oval" fill="#5645F5" cx="91.9332607" cy="91.9332607" r="91.9332607"></circle>
<text id="Virtual-Machine" font-family="Helvetica" font-size="25" font-weight="normal" line-spacing="30" fill="#FFFFFF">
<tspan x="56.7888184" y="86">Virtual</tspan>
<tspan x="44.7526855" y="116">Machine</tspan>
</text>
</g>
<g id="Group" transform="translate(799.000000, 662.000000)">
<circle id="Oval" fill="#5645F5" cx="91.9332607" cy="91.9332607" r="91.9332607"></circle>
<text id="Storage" font-family="Helvetica" font-size="25" font-weight="normal" fill="#FFFFFF">
<tspan x="48.2194824" y="100">Storage</tspan>
</text>
</g>
<path id="Line" d="M272.5,287.983652 L206.5,287.983652 L205,287.983652 L205,284.983652 L206.5,284.983652 L272.5,284.983652 L272.5,276.983652 L291.5,286.483652 L272.5,295.983652 L272.5,287.983652 Z" fill="#000000" fill-rule="nonzero"></path>
<g id="Group-2" transform="translate(542.000000, 17.000000)">
<rect id="Rectangle" fill="#B3BCC3" opacity="0.75" x="0" y="0" width="105" height="105" rx="28"></rect>
<rect id="Rectangle" fill="#B3BCC3" opacity="0.75" x="30" y="0" width="105" height="105" rx="28"></rect>
<rect id="Rectangle" fill="#B3BCC3" opacity="0.75" x="60" y="0" width="105" height="105" rx="28"></rect>
<rect id="Rectangle" fill="#B3BCC3" opacity="0.75" x="90" y="0" width="105" height="105" rx="28"></rect>
<rect id="Rectangle" fill="#B3BCC3" opacity="0.75" x="120" y="0" width="105" height="105" rx="28"></rect>
<rect id="Rectangle" fill="#B3BCC3" opacity="0.75" x="150" y="0" width="105" height="105" rx="28"></rect>
<rect id="Rectangle" fill="#B3BCC3" opacity="0.75" x="180" y="0" width="105" height="105" rx="28"></rect>
<rect id="Rectangle" fill="#B3BCC3" opacity="0.75" x="210" y="0" width="105" height="105" rx="28"></rect>
<rect id="Rectangle" fill="#B3BCC3" opacity="0.75" x="240" y="0" width="105" height="105" rx="28"></rect>
<rect id="Rectangle" fill="#B3BCC3" opacity="0.75" x="270" y="0" width="105" height="105" rx="28"></rect>
<rect id="Rectangle" fill="#B3BCC3" opacity="0.75" x="300" y="0" width="105" height="105" rx="28"></rect>
<rect id="Rectangle" fill="#B3BCC3" opacity="0.75" x="330" y="0" width="105" height="105" rx="28"></rect>
<rect id="Rectangle" fill="#B3BCC3" opacity="0.75" x="360" y="0" width="105" height="105" rx="28"></rect>
<text id="Other-Validators" font-family="Helvetica" font-size="25" font-weight="normal" fill="#FFFFFF">
<tspan x="143.101807" y="61">Other Validators</tspan>
</text>
</g>
<text id="Validator" font-family="Helvetica" font-size="40" font-weight="normal" fill="#FFFFFF">
<tspan x="299" y="793">Validator</tspan>
</text>
<text id="3" font-family="Helvetica-Bold, Helvetica" font-size="25" font-weight="bold" fill="#15112C">
<tspan x="501.048096" y="265">3</tspan>
</text>
<text id="6" font-family="Helvetica-Bold, Helvetica" font-size="25" font-weight="bold" fill="#15112C">
<tspan x="758.548096" y="265">6</tspan>
</text>
<text id="7" font-family="Helvetica-Bold, Helvetica" font-size="25" font-weight="bold" fill="#15112C">
<tspan x="845.048096" y="163">7</tspan>
</text>
<text id="5" font-family="Helvetica-Bold, Helvetica" font-size="25" font-weight="bold" fill="#15112C">
<tspan x="594.548096" y="163">5</tspan>
</text>
<text id="9" font-family="Helvetica-Bold, Helvetica" font-size="25" font-weight="bold" fill="#15112C">
<tspan x="759.048096" y="506">9</tspan>
</text>
<text id="4" font-family="Helvetica-Bold, Helvetica" font-size="25" font-weight="bold" fill="#15112C">
<tspan x="630.548096" y="265">4</tspan>
</text>
<text id="2" font-family="Helvetica-Bold, Helvetica" font-size="25" font-weight="bold" fill="#15112C">
<tspan x="759.048096" y="626">2</tspan>
</text>
<text id="2" font-family="Helvetica-Bold, Helvetica" font-size="25" font-weight="bold" fill="#15112C">
<tspan x="503.048096" y="393">2</tspan>
</text>
<text id="12" font-family="Helvetica-Bold, Helvetica" font-size="25" font-weight="bold" letter-spacing="-1.29999995" fill="#15112C">
<tspan x="849.396191" y="637">12</tspan>
</text>
<text id="11" font-family="Helvetica-Bold, Helvetica" font-size="25" font-weight="bold" letter-spacing="-1.29999995" fill="#15112C">
<tspan x="920.079785" y="164">11</tspan>
</text>
<text id="10" font-family="Helvetica-Bold, Helvetica" font-size="25" font-weight="bold" letter-spacing="-1.29999995" fill="#15112C">
<tspan x="905.746191" y="406">1</tspan>
<tspan x="918.35" y="406">0</tspan>
</text>
<text id="8" font-family="Helvetica-Bold, Helvetica" font-size="25" font-weight="bold" fill="#15112C">
<tspan x="862.048096" y="406">8</tspan>
</text>
<text id="1" font-family="Helvetica-Bold, Helvetica" font-size="25" font-weight="bold" fill="#15112C">
<tspan x="221.048096" y="271">1</tspan>
</text>
</g>
</svg>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment