From c425f4bc9c67141afe9f31464c4fc9fb4acc58d2 Mon Sep 17 00:00:00 2001 From: George Danezis Date: Wed, 9 Oct 2024 15:33:01 +0100 Subject: [PATCH 01/50] Testnet update --- contracts/blob_store/README.md | 9 ++-- docs/README.md | 25 +++++++---- docs/SUMMARY.md | 4 +- docs/blog/04_testnet_update.md | 9 ++++ docs/dev-guide/dev-guide.md | 14 +++--- docs/dev-guide/dev-operations.md | 6 +-- docs/dev-guide/sui-struct.md | 4 +- docs/{tos.md => devnet_tos.md} | 0 docs/operator-guide/aggregator.md | 1 + docs/operator-guide/operator-guide.md | 3 +- docs/operator-guide/storage-node.md | 5 ++- docs/testnet_tos.md | 50 +++++++++++++++++++++ docs/usage/client-cli.md | 62 +++++++++++++++++++++++---- docs/usage/setup.md | 34 ++++++++++++++- examples/move/walrus_dep/README.md | 2 +- 15 files changed, 189 insertions(+), 39 deletions(-) create mode 100644 docs/blog/04_testnet_update.md rename docs/{tos.md => devnet_tos.md} (100%) create mode 100644 docs/testnet_tos.md diff --git a/contracts/blob_store/README.md b/contracts/blob_store/README.md index 38f7b71e..6ed6f356 100644 --- a/contracts/blob_store/README.md +++ b/contracts/blob_store/README.md @@ -1,9 +1,10 @@ -# Walrus Devnet Move contracts +# Walrus Testnet Move contracts + -This is the Move source code for the Walrus Devnet instance. We provide this so developers can +This is the Move source code for the Walrus Testnet instance. We provide this so developers can experiment with building Walrus apps that require Move extensions. This code is published on Sui Testnet at package ID `0x7e12d67a52106ddd5f26c6ff4fe740ba5dea7cfc138d5b1d33863ba9098aa6fe`. -**A word of caution:** Walrus Testnet will use new Move packages with struct layouts and function +**A word of caution:** Walrus Mainnet will use new Move packages with struct layouts and function signatures that may not be compatible with this package. Move code that builds against this package -will need to rewritten. +will need to adapted. diff --git a/docs/README.md b/docs/README.md index 784baa4d..6d0306b0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,19 +11,19 @@ Walrus behind the scenes. See the [Walrus Sites chapter](./walrus-sites/intro.md details on how this works. ``` -```admonish danger title="Disclaimer about the Walrus developer preview" -The current Devnet release of Walrus and Walrus Sites is a developer preview intended to showcase -the technology and solicit feedback from builders. All storage nodes and aggregators are operated by -Mysten Labs and all transactions are executed on the Sui Testnet and use Testnet SUI which has no +```admonish danger title="Disclaimer about the Walrus Testnet" +The current Testnet release of Walrus and Walrus Sites is a preview intended to showcase +the technology and solicit feedback from builders, users and storage node operators. +All transactions are executed on the Sui Testnet and use Testnet WAL and SUI which has no value. The state of the store **can and will be wiped**, at any point and possibly with no warning. -Do not rely on this developer preview for any production purposes, it comes with no availability or +Do not rely on this Testnet for any production purposes, it comes with no availability or persistence guarantees. Furthermore, encodings and blob IDs may be incompatible with the future Testnet and Mainnet and -developers will be responsible for migrating any Devnet applications and data to Testnet. Detailed -migration guides will be provided when Testnet becomes available. +developers will be responsible for migrating any Testnet applications and data to Mainnet. Detailed +migration guides will be provided when Mainnet becomes available. -Also see the [Devnet terms of service](../tos.md) under which this developer preview is made +Also see the [Testnet terms of service](../testnet_tos.md) under which this Testnet is made available. ``` @@ -49,7 +49,14 @@ confidentiality. for coordination, attesting availability, and payments. Storage space is represented as a resource on Sui, which can be owned, split, merged, and transferred. Stored blobs are also represented by objects on Sui, which means that smart contracts can check whether a blob is available and for how - long. + long, extend its lifetime or optionally delete it. + +- **Epochs, Tokenomics and Delegated Proof of Stake** Walrus is operated by a committee of storage + nodes that evolve between epochs. A native token, WAL (and its subdivision FROST), is used + to delegate stake to storage nodes, and those with high stake become part of the epoch committee. + The WAL token is also used for payments for storage. At the end of each epoch rewards for + selecting storage nodes, storing and serving blobs are distributed to storage nodes and whose that + stake with them. All these processes are mediated by smart contracts on the Sui platform. - **Flexible access:** Users can interact with Walrus through a command-line interface (CLI), software development kits (SDKs), and web2 HTTP technologies. Walrus is designed to work well diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 8d357a64..c6451c51 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -12,6 +12,7 @@ - [Announcing Walrus](./blog/01_announcing_walrus.md) - [2024-08-12 Devnet Update](./blog/02_devnet_update.md) - [Announcing the Walrus Whitepaper](./blog/03_whitepaper.md) + - [2024-10-17 Testnet Update](./blog/04_testnet_update.md) --- @@ -64,4 +65,5 @@ --- [Glossary](./glossary.md) -[Devnet terms of service](./tos.md) +[Devnet terms of service](./devnet_tos.md) +[Testnet terms of service](./testnet_tos.md) diff --git a/docs/blog/04_testnet_update.md b/docs/blog/04_testnet_update.md new file mode 100644 index 00000000..3995b224 --- /dev/null +++ b/docs/blog/04_testnet_update.md @@ -0,0 +1,9 @@ +# 2024-10-17 Testnet Update + + +* Deletable blobs and reclaiming storage +* External Storage nodes +* WAL Token used for payments and WAL <> SUI faucet +* Staking and Unstaking with Staking App +* Epoch change and shard migration +* Explorer diff --git a/docs/dev-guide/dev-guide.md b/docs/dev-guide/dev-guide.md index a6b1c775..dd43e65c 100644 --- a/docs/dev-guide/dev-guide.md +++ b/docs/dev-guide/dev-guide.md @@ -15,17 +15,17 @@ This developer guide describes the following: Refer again to the [glossary](../glossary.md) of terms as a reference. ```admonish danger title="Disclaimer about the Walrus developer preview" -The current Devnet release of Walrus and Walrus Sites is a developer preview intended to showcase -the technology and solicit feedback from builders. All storage nodes and aggregators are operated by -Mysten Labs and all transactions are executed on the Sui Testnet and use Testnet SUI which has no +The current Testnet release of Walrus and Walrus Sites is a preview intended to showcase +the technology and solicit feedback from builders, users and storage node operators. +All transactions are executed on the Sui Testnet and use Testnet WAL and SUI which has no value. The state of the store **can and will be wiped**, at any point and possibly with no warning. -Do not rely on this developer preview for any production purposes, it comes with no availability or +Do not rely on this Testnet for any production purposes, it comes with no availability or persistence guarantees. Furthermore, encodings and blob IDs may be incompatible with the future Testnet and Mainnet and -developers will be responsible for migrating any Devnet applications and data to Testnet. Detailed -migration guides will be provided when Testnet becomes available. +developers will be responsible for migrating any Testnet applications and data to Mainnet. Detailed +migration guides will be provided when Mainnet becomes available. -Also see the [Devnet terms of service](../tos.md) under which this developer preview is made +Also see the [Testnet terms of service](../testnet_tos.md) under which this Testnet is made available. ``` diff --git a/docs/dev-guide/dev-operations.md b/docs/dev-guide/dev-operations.md index 1b24a230..ff41e069 100644 --- a/docs/dev-guide/dev-operations.md +++ b/docs/dev-guide/dev-operations.md @@ -39,9 +39,9 @@ maximum blob size is currently 957 MiB. You may store larger blobs by split smaller chunks. Blobs are stored for a certain number of *epochs*, as specified at the time they were stored. Walrus -storage nodes ensure that within these epochs a read succeeds. The Walrus Devnet only uses a single -epoch today, and blobs uploaded will be available in that single epoch (until the Devnet is wiped). -Future devnets may span across multiple epochs. +storage nodes ensure that within these epochs a read succeeds. The current Testnet uses a short +epoch duration of one day for testing purposes, but Mainnet epochs are likely to be longer such as +many weeks each. ## Read diff --git a/docs/dev-guide/sui-struct.md b/docs/dev-guide/sui-struct.md index 1226e1ff..fdbee337 100644 --- a/docs/dev-guide/sui-struct.md +++ b/docs/dev-guide/sui-struct.md @@ -7,7 +7,7 @@ querying or executing transactions on Sui directly. However, Walrus uses Sui to and smart contract developers can read information about the Walrus system, as well as stored blobs, on Sui. -The Move code of the Walrus Devnet contracts is available at +The Move code of the Walrus Testnet contracts is available at . An example package using the Walrus contracts is available at . @@ -16,7 +16,7 @@ The following sections provide further insights into the contract and an overvie Walrus objects in your own Sui smart contracts. ```admonish danger title="A word of caution" -Walrus Testnet will use new Move packages with struct layouts and function signatures that may not +Walrus Mainnet will use new Move packages with struct layouts and function signatures that may not be compatible with this package. Move code that builds against this package will need to rewritten. ``` diff --git a/docs/tos.md b/docs/devnet_tos.md similarity index 100% rename from docs/tos.md rename to docs/devnet_tos.md diff --git a/docs/operator-guide/aggregator.md b/docs/operator-guide/aggregator.md index d6ba8161..6f23c792 100644 --- a/docs/operator-guide/aggregator.md +++ b/docs/operator-guide/aggregator.md @@ -1,4 +1,5 @@ # Operating an aggregator + Below is an example of an aggregator node which hosts a HTTP endpoint that can be used to fetch data from Walrus over the web. diff --git a/docs/operator-guide/operator-guide.md b/docs/operator-guide/operator-guide.md index 8fee5b94..f5cc3731 100644 --- a/docs/operator-guide/operator-guide.md +++ b/docs/operator-guide/operator-guide.md @@ -1,7 +1,8 @@ # Operator guide This chapter introduces all the concepts needed for operators of the different components that make -up the Walrus system. It is currently a work in progress and will be updated as the platform grows. +up the Walrus system. It is currently a work in progress and will be updated as the platform +matures. Specifically, this guide describes the following: diff --git a/docs/operator-guide/storage-node.md b/docs/operator-guide/storage-node.md index 8c761955..815f6fcb 100644 --- a/docs/operator-guide/storage-node.md +++ b/docs/operator-guide/storage-node.md @@ -1,7 +1,8 @@ # Operating a storage node + -The binary of the storage node is not yet publicly available. It will be made available in September -to operators for Testnet nodes. Prior to official network launch the code will be open-sourced. +The binary of the storage node is not yet publicly available. Prior to official network launch the +code will be open-sourced. A basic systemd service running the Storage Node could look like this: diff --git a/docs/testnet_tos.md b/docs/testnet_tos.md new file mode 100644 index 00000000..5546e032 --- /dev/null +++ b/docs/testnet_tos.md @@ -0,0 +1,50 @@ + +# TESTNET TERMS OF SERVICE - WALRUS + + +Last updated: June 13, 2024 + +By using Mysten Labs Devnet software, technologies, tools, and other services (collectively +“Devnet”), you agree to the general Terms of Service and these additional Devnet Terms of Service +(together, the “Terms”). If you do not agree, do not participate in Devnet. If you are using Devnet +on behalf of an organization, you represent and warrant that you are an authorized representative of +that organization and have the authority to bind that business or entity to the Terms. + +## Eligibility Criteria + +You may use Devnet only if you: + +- Are 18 years or older and capable of forming a binding contract with us. +- Are not otherwise barred from participating in Devnet under applicable law. + +We may, at our discretion, introduce new or change existing eligibility criteria or conditions we +deem appropriate. Devnet may operate in certain phases, and your participation in any one phase of +Devnet does not guarantee that you will be selected for any other phases of Devnet. + +## Duration + +Devnet will commence on the date we prescribe and continue until terminated at our discretion. We +may change, discontinue, or wipe, temporarily or permanently, all or any part of Devnet, at any +time and without notice at our discretion, including, without limitation, the modification of the +presence, amounts, or any other conditions applicable to data you have stored within Devnet, without +any liability to you or other Devnet users. + +## No Warranty + +Mysten Labs provides the Devnet platform solely as a developer preview. Devnet is provided "as is" +and "with all faults." We make no warranties, express or implied, regarding the reliability, +accuracy, performance, or fitness for a particular purpose of the service provided. You accept all +risks associated with the use of Devnet and agree that Mysten Labs, its affiliates, and its +employees shall not be liable for any damages, whether direct, indirect, incidental, special, +consequential, or punitive, arising out of the use or inability to use the service, including but +not limited to lost profits, loss of business, or data loss. + +No employee or representative of Mysten Labs is authorized to make any warranties or representations +beyond those stated in this agreement. Any statements made by employees or representatives of Mysten +Labs regarding the service shall not be construed as warranties or representations, and customers +agree to indemnify and hold harmless Mysten Labs from any such statements. + +Any deficiencies or errors in the Devnet platform shall not constitute a breach of this agreement, +and customers agree to waive any right to seek a refund or compensation based on such deficiencies +or errors. This "as is, no warranty" provision shall survive the termination or expiration of any +other agreements between you and Mysten Labs. diff --git a/docs/usage/client-cli.md b/docs/usage/client-cli.md index c6772324..4b587f4e 100644 --- a/docs/usage/client-cli.md +++ b/docs/usage/client-cli.md @@ -15,12 +15,13 @@ their meaning. ## Walrus system information Information about the Walrus system is available through the `walrus info` command. For example, + ```console $ walrus info Walrus system information -Current epoch: 0 +Current epoch: 54 Storage nodes Number of nodes: 10 @@ -31,18 +32,19 @@ Maximum blob size: 13.3 GiB (14,273,391,930 B) Storage unit: 1.00 KiB Approximate storage prices per epoch -Price per encoded storage unit: 50 MIST -Price to store metadata: 0.0031 SUI -Marginal price per additional 1 MiB (w/o metadata): 241,950 MIST +Price per encoded storage unit: 5 FROST +Price to store metadata: 0.0003 WAL +Marginal price per additional 1 MiB (w/o metadata): 24,195 FROST Total price for example blob sizes -16.0 MiB unencoded (135 MiB encoded): 0.0069 SUI per epoch -512 MiB unencoded (2.33 GiB encoded): 0.122 SUI per epoch -13.3 GiB unencoded (60.5 GiB encoded): 3.174 SUI per epoch +16.0 MiB unencoded (135 MiB encoded): 0.0007 WAL per epoch +512 MiB unencoded (2.33 GiB encoded): 0.012 WAL per epoch +13.3 GiB unencoded (60.5 GiB encoded): 0.317 WAL per epoch + ``` gives an overview of the number of storage nodes and shards in the system, the maximum blob size, -and the current cost in (Testnet) SUI for storing blobs. +and the current cost in (Testnet) WAL for storing blobs. (Note: 1 WAL = 1 000 000 000 FROST) Additional information such as encoding parameters and sizes, BFT system information, and information on the storage nodes and their shard distribution can be viewed with the `--dev` @@ -94,6 +96,50 @@ By default the blob data is written to the standard output. The `--out ` CL can be used to specify an output file name. The `--rpc-url ` (or `-r`) may be used to specify a Sui RPC node to use instead of the one set in the wallet configuration or the default one. +## Reclaiming space via deletable blobs + +By default `walrus store` uploads a blob and Walrus will keep it available until after its expiry +epoch. Not even the uploader may delete it beforehand. However, optionally, the store command +may be invoked with the `--deletable` flag, to indicate the blob may be deleted before its expiry +by the owner of the Sui blob object representing the blob. Deletable blobs are indicated as such +in the Sui events that certify them, and should not be relied upon for availability by others. + +A deletable blob may be deleted with the command: +``` +walrus delete --blob-id +``` +Optionally the delete command can be invoked by specifying a `--file ` option, to derive the +blob ID from a file, or `--object-id ` to delete the blob in the Sui blob object specified. + +The `delete` command reclaims the storage object associated with the deleted blob, which is +re-used to store new blobs. The delete operation provides +flexibility around managing storage costs and re-using storage. + +The delete operation has limited utility for privacy: It only deletes slivers from the current +epoch storage nodes, and subsequent epoch storage nodes, if no other user has uploaded a copy of +the same blob. If another copy of the same blob exists in Walrus the delete operation will not +make the blob unavailable for download, and `walrus read` invocations will download it. Copies of +the public blob may be cached or downloaded by users, and these copies are not deleted. + +```admonish danger title="Delete reclaims space only" +**All blobs stored in Walrus are public and discoverable by all.** The `delete` command will +not delete slivers if other copies of the blob are stored on Walrus possibly by other users. +It does not delete blobs from caches, slivers from past storage nodes, or copies +that could have been made by users before the blob was deleted. +``` + +## Blob ID utilities + +The `walrus blob-id ` may be used to derive the blob ID of any file. The blob ID is a +commitment to the file, and any blob with the same ID will decode to the same content. The blob +ID is a 256 bit number and represented on some Sui explorer as a decimal large number. The +command `walrus convert-blob-id ` may be used to convert it to a base64 URL safe +encoding used by the command line tools and other APIs. + +The `walrus list-blobs` command lists all the non expired Sui blob object that the current account +owns, including their blob ID, object ID, and metadata about expiry and deletable status. +The option `--include-expired` also lists expired blob objects. + ## Changing the default configuration Use the `--config` option to specify a custom path to the diff --git a/docs/usage/setup.md b/docs/usage/setup.md index 3159c2d1..d53fd775 100644 --- a/docs/usage/setup.md +++ b/docs/usage/setup.md @@ -7,7 +7,7 @@ patterns (see [the next chapter](./interacting.md)). This chapter describes the of the Walrus client. ```admonish note -Note that our Walrus Devnet uses Sui **Testnet** for coordination. +Note that the Walrus Testnet uses Sui **Testnet** for coordination. ``` ## Prerequisites @@ -131,6 +131,36 @@ In addition to the latest version of the `walrus` binary, the GCS bucket also co versions. An overview in XML format is available at . +## Testnet WAL faucet + +The Walrus Testnet uses Testnet WAL tokens to buy storage and stake. Testnet WAL tokens have no +value and can be exchanged for some Testnet SUI tokens, which also have no value, thought the +command: + +``` +walrus get-wal +``` + +You can check you have received Testnet WAL by checking the Sui balances: + +``` +sui client balance +╭─────────────────────────────────────────╮ +│ Balance of coins owned by this address │ +├─────────────────────────────────────────┤ +│ ╭─────────────────────────────────────╮ │ +│ │ coin balance (raw) balance │ │ +│ ├─────────────────────────────────────┤ │ +│ │ Sui 8869252670 8.86 SUI │ │ +│ │ WAL 500000000 0.50 WAL │ │ +│ ╰─────────────────────────────────────╯ │ +╰─────────────────────────────────────────╯ +``` + +By default 0.5 SUI are exchanged for 0.5 WAL, but a different amount of SUI may be exchanged +using the `--amount` option, and a specific coin ID may be used through the `--exchange-id`. +The `walrus get-wal --help` command provides more information about those. + ## Configuration A single parameter is required to configure Walrus, namely the ID of the [system @@ -153,6 +183,8 @@ you need to use the `--config` option when running the `walrus` binary. ### Advanced configuration (optional) + + The configuration file currently supports the following parameters: ```yaml diff --git a/examples/move/walrus_dep/README.md b/examples/move/walrus_dep/README.md index ea8b2a73..4952287a 100644 --- a/examples/move/walrus_dep/README.md +++ b/examples/move/walrus_dep/README.md @@ -1,3 +1,3 @@ # Example Move package depending on Walrus -A simple example of depending on the Walrus Devnet Move package. +A simple example of depending on the Walrus Testnet Move package. From 313aedfc9dbd48d65408fba804c66c428d8e78f4 Mon Sep 17 00:00:00 2001 From: George Danezis Date: Wed, 9 Oct 2024 15:36:30 +0100 Subject: [PATCH 02/50] Added 2 todo --- contracts/blob_store/README.md | 2 +- docs/testnet_tos.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/blob_store/README.md b/contracts/blob_store/README.md index 6ed6f356..79dce623 100644 --- a/contracts/blob_store/README.md +++ b/contracts/blob_store/README.md @@ -1,5 +1,5 @@ # Walrus Testnet Move contracts - + This is the Move source code for the Walrus Testnet instance. We provide this so developers can experiment with building Walrus apps that require Move extensions. This code is published on Sui diff --git a/docs/testnet_tos.md b/docs/testnet_tos.md index 5546e032..b2ed9e19 100644 --- a/docs/testnet_tos.md +++ b/docs/testnet_tos.md @@ -1,6 +1,6 @@ # TESTNET TERMS OF SERVICE - WALRUS - + Last updated: June 13, 2024 From fad33a16c8c9f78d08d3baa284b0c76671ea1543 Mon Sep 17 00:00:00 2001 From: George Danezis Date: Wed, 9 Oct 2024 15:41:07 +0100 Subject: [PATCH 03/50] Make linter happy --- docs/blog/04_testnet_update.md | 12 ++++++------ docs/usage/client-cli.md | 4 +++- docs/usage/setup.md | 4 ++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/blog/04_testnet_update.md b/docs/blog/04_testnet_update.md index 3995b224..1e29937c 100644 --- a/docs/blog/04_testnet_update.md +++ b/docs/blog/04_testnet_update.md @@ -1,9 +1,9 @@ # 2024-10-17 Testnet Update -* Deletable blobs and reclaiming storage -* External Storage nodes -* WAL Token used for payments and WAL <> SUI faucet -* Staking and Unstaking with Staking App -* Epoch change and shard migration -* Explorer +- Deletable blobs and reclaiming storage +- External Storage nodes +- WAL Token used for payments and WAL <> SUI faucet +- Staking and Unstaking with Staking App +- Epoch change and shard migration +- Explorer diff --git a/docs/usage/client-cli.md b/docs/usage/client-cli.md index 4b587f4e..b5045629 100644 --- a/docs/usage/client-cli.md +++ b/docs/usage/client-cli.md @@ -105,9 +105,11 @@ by the owner of the Sui blob object representing the blob. Deletable blobs are i in the Sui events that certify them, and should not be relied upon for availability by others. A deletable blob may be deleted with the command: -``` + +```sh walrus delete --blob-id ``` + Optionally the delete command can be invoked by specifying a `--file ` option, to derive the blob ID from a file, or `--object-id ` to delete the blob in the Sui blob object specified. diff --git a/docs/usage/setup.md b/docs/usage/setup.md index d53fd775..7c9e99b3 100644 --- a/docs/usage/setup.md +++ b/docs/usage/setup.md @@ -137,13 +137,13 @@ The Walrus Testnet uses Testnet WAL tokens to buy storage and stake. Testnet WAL value and can be exchanged for some Testnet SUI tokens, which also have no value, thought the command: -``` +```sh walrus get-wal ``` You can check you have received Testnet WAL by checking the Sui balances: -``` +```sh sui client balance ╭─────────────────────────────────────────╮ │ Balance of coins owned by this address │ From c7308cf0b0dc22352f566ed8356c2a6c67090add Mon Sep 17 00:00:00 2001 From: George Danezis Date: Wed, 9 Oct 2024 16:00:13 +0100 Subject: [PATCH 04/50] Updated move --- docs/dev-guide/sui-struct.md | 146 +++++++++++++++++++++-------------- 1 file changed, 86 insertions(+), 60 deletions(-) diff --git a/docs/dev-guide/sui-struct.md b/docs/dev-guide/sui-struct.md index fdbee337..9a2df795 100644 --- a/docs/dev-guide/sui-struct.md +++ b/docs/dev-guide/sui-struct.md @@ -7,6 +7,7 @@ querying or executing transactions on Sui directly. However, Walrus uses Sui to and smart contract developers can read information about the Walrus system, as well as stored blobs, on Sui. + The Move code of the Walrus Testnet contracts is available at . An example package using the Walrus contracts is available at @@ -16,7 +17,7 @@ The following sections provide further insights into the contract and an overvie Walrus objects in your own Sui smart contracts. ```admonish danger title="A word of caution" -Walrus Mainnet will use new Move packages with struct layouts and function signatures that may not +Walrus Mainnet will use new Move packages with `struct` layouts and function signatures that may not be compatible with this package. Move code that builds against this package will need to rewritten. ``` @@ -28,7 +29,7 @@ indicating that a sufficient number of slivers have been stored to guarantee the availability. When a blob is certified, its `certified_epoch` field contains the epoch in which it was certified. -A `Storage` object is always associated with a `Blob` object, reserving enough space for +A `Blob` object is always associated with a `Storage` object, reserving enough space for a long enough period for the blob's storage. A certified blob is available for the period the underlying storage resource guarantees storage. @@ -36,26 +37,27 @@ Concretely, `Blob` and `Storage` objects have the following fields, which can be Sui SDKs: ```move -/// The blob structure represents a blob that has been registered to with some -/// storage, and then may eventually be certified as being available in the -/// system. + +/// The blob structure represents a blob that has been registered to with some storage, +/// and then may eventually be certified as being available in the system. public struct Blob has key, store { id: UID, - stored_epoch: u64, + registered_epoch: u32, blob_id: u256, size: u64, - erasure_code_type: u8, - certified_epoch: option::Option, // The epoch first certified, - // or None if not certified. + encoding_type: u8, + // Stores the epoch first certified. + certified_epoch: option::Option, storage: Storage, + // Marks if this blob can be deleted. + deletable: bool, } -/// Reservation for storage for a given period, which is inclusive start, -/// exclusive end. +/// Reservation for storage for a given period, which is inclusive start, exclusive end. public struct Storage has key, store { id: UID, - start_epoch: u64, - end_epoch: u64, + start_epoch: u32, + end_epoch: u32, storage_size: u64, } ``` @@ -64,17 +66,19 @@ All fields of `Blob` and `Storage` objects can be read using the expected functi ```move // Blob functions -public fun stored_epoch(b: &Blob): u64; public fun blob_id(b: &Blob): u256; public fun size(b: &Blob): u64; public fun erasure_code_type(b: &Blob): u8; -public fun certified_epoch(b: &Blob): &Option; +public fun registered_epoch(self: &Blob): u32; +public fun certified_epoch(b: &Blob): &Option; public fun storage(b: &Blob): &Storage; +... // Storage functions -public fun start_epoch(self: &Storage): u64; -public fun end_epoch(self: &Storage): u64; +public fun start_epoch(self: &Storage): u32; +public fun end_epoch(self: &Storage): u32; public fun storage_size(self: &Storage): u64; +... ``` ## Events @@ -85,20 +89,47 @@ certified, a `BlobCertified` is emitted containing information about the blob ID after which the blob will be deleted. Before that epoch the blob is guaranteed to be available. ```move -/// Signals a blob with metadata is registered. +/// Signals that a blob with meta-data has been registered. public struct BlobRegistered has copy, drop { - epoch: u64, + epoch: u32, blob_id: u256, size: u64, - erasure_code_type: u8, - end_epoch: u64, + encoding_type: u8, + end_epoch: u32, + deletable: bool, + // The object id of the related `Blob` object + object_id: ID, } -/// Signals a blob is certified. +/// Signals that a blob is certified. public struct BlobCertified has copy, drop { - epoch: u64, + epoch: u32, blob_id: u256, - end_epoch: u64, + end_epoch: u32, + deletable: bool, + // The object id of the related `Blob` object + object_id: ID, + // Marks if this is an extension for explorers, etc. + is_extension: bool, +} +``` + +The `BlobCertified` event with `deletable` set to false and a `end_epoch` in the future indicates +that the blob will be available until this epoch. A light client proof this event was emitted +for a blob ID constitutes a proof of availability for the data with this blob ID. + +When a deletable blob is deleted, a `BlobDeleted` event is emitted: + +```move +/// Signals that a blob has been deleted. +public struct BlobDeleted has copy, drop { + epoch: u32, + blob_id: u256, + end_epoch: u32, + // The object ID of the related `Blob` object. + object_id: ID, + // If the blob object was previously certified. + was_certified: bool, } ``` @@ -108,62 +139,57 @@ Anyone attempting a read on such a blob is guaranteed to also detect it as inval ```move /// Signals that a BlobID is invalid. public struct InvalidBlobID has copy, drop { - epoch: u64, // The epoch in which the blob ID is first registered as invalid + epoch: u32, // The epoch in which the blob ID is first registered as invalid blob_id: u256, } ``` +System level events such as `EpochChangeStart` and `EpochChangeDone` indicate transitions +between epochs. And associated events such as `ShardsReceived`, `EpochParametersSelected`, +and `ShardRecoveryStart` indicate storage node level events related to epoch transitions, +shard migrations and epoch parameters. + ## System information The Walrus system object contains metadata about the available and used storage, as well as the -price of storage per KiB of storage in MIST. The committee +price of storage per KiB of storage in FROST. The committee structure within the system object can be used to read the current epoch number, as well as information about the committee. ```move -const BYTES_PER_UNIT_SIZE: u64 = 1_024; - -public struct System has key, store { +public struct SystemStateInnerV1 has key, store { id: UID, - /// The current committee, with the current epoch. - /// The option is always Some, but need it for swap. - current_committee: Option, - - /// When we first enter the current epoch we SYNC, - /// and then we are DONE after a cert from a quorum. - epoch_status: u8, - + committee: BlsCommittee, // Some accounting total_capacity_size: u64, used_capacity_size: u64, - /// The price per unit size of storage. - price_per_unit_size: u64, - - /// Tables about the future and the past. - past_committees: Table, - future_accounting: FutureAccountingRingBuffer, + storage_price_per_unit_size: u64, + /// The write price per unit size. + write_price_per_unit_size: u64, + /// Accounting ring buffer for future epochs. + future_accounting: FutureAccountingRingBuffer, + /// Event blob certification state + event_blob_certification_state: EventBlobCertificationState, } -public struct Committee has store { - epoch: u64, - bls_committee: BlsCommittee, +/// This represents a BLS signing committee for a given epoch. +public struct BlsCommittee has store, copy, drop { + /// A vector of committee members + members: vector, + /// The total number of shards held by the committee + n_shards: u16, + /// The epoch in which the committee is active. + epoch: u32, } -``` - -A few public functions of the committee allow contracts to read Walrus metadata: -```move -/// Get epoch. Uses the committee to get the epoch. -public fun epoch(self: &System): u64; - -/// Accessor for total capacity size. -public fun total_capacity_size(self: &System): u64; - -/// Accessor for used capacity size. -public fun used_capacity_size(self: &System): u64; +public struct BlsCommitteeMember has store, copy, drop { + public_key: Element, + weight: u16, + node_id: ID, +} -// The number of shards -public fun n_shards(self: &System): u16; ``` + + \ No newline at end of file From 5b35d524bea156c3718fa05a0d427c191d6be1ae Mon Sep 17 00:00:00 2001 From: George Danezis Date: Wed, 9 Oct 2024 18:40:42 +0100 Subject: [PATCH 05/50] Line ending --- docs/dev-guide/sui-struct.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev-guide/sui-struct.md b/docs/dev-guide/sui-struct.md index 9a2df795..c7a6f32c 100644 --- a/docs/dev-guide/sui-struct.md +++ b/docs/dev-guide/sui-struct.md @@ -192,4 +192,4 @@ public struct BlsCommitteeMember has store, copy, drop { ``` - \ No newline at end of file + From 98bcfb19a7ea7145acc18e982a67b21ce85d776b Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 10 Oct 2024 11:48:33 +0200 Subject: [PATCH 06/50] update agg and pub links and reformat HTML --- .editorconfig | 2 +- docs/usage/web-api.md | 12 +- .../blob_upload_download_webapi.html | 456 +++++++++--------- 3 files changed, 238 insertions(+), 232 deletions(-) diff --git a/.editorconfig b/.editorconfig index a03f3032..74b21386 100644 --- a/.editorconfig +++ b/.editorconfig @@ -19,7 +19,7 @@ indent_size = unset max_line_length = unset trim_trailing_whitespace = unset -[{*.yml,*.yaml,*.toml,*.css}] +[{*.yml,*.yaml,*.toml,*.css,*.html}] indent_size = 2 max_line_length = 150 diff --git a/docs/usage/web-api.md b/docs/usage/web-api.md index 571b8f2a..0cd0d711 100644 --- a/docs/usage/web-api.md +++ b/docs/usage/web-api.md @@ -37,8 +37,9 @@ For some use cases (e.g., a public website), or to just try out the HTTP API, a aggregator and/or publisher is required. For your convenience, we provide these at the following hosts: -- Aggregator: `https://aggregator-devnet.walrus.space` -- Publisher: `https://publisher-devnet.walrus.space` + +- Aggregator: `http://ewr-ptn-agg-00.walrus-private-testnet.walrus.space:9000` +- Publisher: `http://lax-ptn-pub-00.walrus-private-testnet.walrus.space:9000` Our publisher is currently limiting requests to 10 MiB. If you want to upload larger files, you need to [run your own publisher](#local-daemon) or use the [CLI](./client-cli.md). @@ -52,14 +53,15 @@ authentication and compensation for the Sui used. For the following examples, we assume you set the `AGGREGATOR` and `PUBLISHER` environment variables to your desired aggregator and publisher, respectively. For example: + ```sh -AGGREGATOR=https://aggregator-devnet.walrus.space -PUBLISHER=https://publisher-devnet.walrus.space +AGGREGATOR=http://ewr-ptn-agg-00.walrus-private-testnet.walrus.space:9000 +PUBLISHER=http://lax-ptn-pub-00.walrus-private-testnet.walrus.space:9000 ``` ```admonish tip title="API specification" Walrus aggregators and publishers expose their API specifications at the path `/v1/api`. You can -view this in the browser` e.g., at +view this in the browser` e.g., at ``` ### Store diff --git a/examples/javascript/blob_upload_download_webapi.html b/examples/javascript/blob_upload_download_webapi.html index 10d404fb..7554740a 100644 --- a/examples/javascript/blob_upload_download_webapi.html +++ b/examples/javascript/blob_upload_download_webapi.html @@ -2,263 +2,267 @@ - - - - Upload Blobs + + + + Upload Blobs - + - + /** + * Helper to display an error message. + */ + function alert(message) { + const alertElement = document.getElementById("alert"); + if (message !== null) { + alertElement.textContent = message; + } + alertElement.style.visibility = message !== null ? "visible" : "hidden"; + } + + +
+
+

Walrus Blob Upload

+

An example uploading and displaying files with Walrus.

+
+
+
-
+
+
-

Walrus Blob Upload

-

An example uploading and displaying files with Walrus.

+

Blob Upload

+

+ Upload blobs to Walrus, and display them on this page. See the + + Walrus documentation + for more information. + The file size is limited to 10 MiB on the default publisher. Use the CLI + tool to store bigger files. +

-
-
-
-
-
-

Blob Upload

-

- Upload blobs to Walrus, and display them on this page. See the - - Walrus documentation - for more information. - The file size is limited to 10 MiB on the default publisher. Use the CLI - tool to store bigger files. -

-
+
+
+
+ + + +
- -
-
- - -
+
+ + + +
-
- - -
+
+ + +
-
- - -
+
+ + +
+ The number of Walrus epochs for which to store the blob. +
+
-
- - -
- The number of Walrus epochs for which to store the blob. -
-
+ +
+ - -
- - - -
+ + -
-

Uploaded Blobs

-
- -
-
+
+

Uploaded Blobs

+
+
-
+ + +
From 2302da96a4db10e73d30f3e395e7ccddffc2cb2a Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 10 Oct 2024 11:49:05 +0200 Subject: [PATCH 07/50] replace devnet by testnet in testnet ToS --- docs/testnet_tos.md | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/docs/testnet_tos.md b/docs/testnet_tos.md index b2ed9e19..bac13308 100644 --- a/docs/testnet_tos.md +++ b/docs/testnet_tos.md @@ -2,39 +2,40 @@ # TESTNET TERMS OF SERVICE - WALRUS -Last updated: June 13, 2024 +Last updated: October 10, 2024 -By using Mysten Labs Devnet software, technologies, tools, and other services (collectively -“Devnet”), you agree to the general Terms of Service and these additional Devnet Terms of Service -(together, the “Terms”). If you do not agree, do not participate in Devnet. If you are using Devnet -on behalf of an organization, you represent and warrant that you are an authorized representative of -that organization and have the authority to bind that business or entity to the Terms. +By using Mysten Labs Testnet software, technologies, tools, and other services (collectively +“Testnet”), you agree to the general Terms of Service and these additional Testnet Terms of Service +(together, the “Terms”). If you do not agree, do not participate in Testnet. If you are using +Testnet on behalf of an organization, you represent and warrant that you are an authorized +representative of that organization and have the authority to bind that business or entity to the +Terms. ## Eligibility Criteria -You may use Devnet only if you: +You may use Testnet only if you: - Are 18 years or older and capable of forming a binding contract with us. -- Are not otherwise barred from participating in Devnet under applicable law. +- Are not otherwise barred from participating in Testnet under applicable law. We may, at our discretion, introduce new or change existing eligibility criteria or conditions we -deem appropriate. Devnet may operate in certain phases, and your participation in any one phase of -Devnet does not guarantee that you will be selected for any other phases of Devnet. +deem appropriate. Testnet may operate in certain phases, and your participation in any one phase of +Testnet does not guarantee that you will be selected for any other phases of Testnet. ## Duration -Devnet will commence on the date we prescribe and continue until terminated at our discretion. We -may change, discontinue, or wipe, temporarily or permanently, all or any part of Devnet, at any +Testnet will commence on the date we prescribe and continue until terminated at our discretion. We +may change, discontinue, or wipe, temporarily or permanently, all or any part of Testnet, at any time and without notice at our discretion, including, without limitation, the modification of the -presence, amounts, or any other conditions applicable to data you have stored within Devnet, without -any liability to you or other Devnet users. +presence, amounts, or any other conditions applicable to data you have stored within Testnet, +without any liability to you or other Testnet users. ## No Warranty -Mysten Labs provides the Devnet platform solely as a developer preview. Devnet is provided "as is" +Mysten Labs provides the Testnet platform solely as a developer preview. Testnet is provided "as is" and "with all faults." We make no warranties, express or implied, regarding the reliability, accuracy, performance, or fitness for a particular purpose of the service provided. You accept all -risks associated with the use of Devnet and agree that Mysten Labs, its affiliates, and its +risks associated with the use of Testnet and agree that Mysten Labs, its affiliates, and its employees shall not be liable for any damages, whether direct, indirect, incidental, special, consequential, or punitive, arising out of the use or inability to use the service, including but not limited to lost profits, loss of business, or data loss. @@ -44,7 +45,7 @@ beyond those stated in this agreement. Any statements made by employees or repre Labs regarding the service shall not be construed as warranties or representations, and customers agree to indemnify and hold harmless Mysten Labs from any such statements. -Any deficiencies or errors in the Devnet platform shall not constitute a breach of this agreement, +Any deficiencies or errors in the Testnet platform shall not constitute a breach of this agreement, and customers agree to waive any right to seek a refund or compensation based on such deficiencies or errors. This "as is, no warranty" provision shall survive the termination or expiration of any other agreements between you and Mysten Labs. From 42bd98e70cdf29f595b0c8450fa53acc2c4962bd Mon Sep 17 00:00:00 2001 From: George Danezis Date: Thu, 10 Oct 2024 11:30:43 +0100 Subject: [PATCH 08/50] Initial blog post --- docs/blog/04_testnet_update.md | 66 ++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/docs/blog/04_testnet_update.md b/docs/blog/04_testnet_update.md index 1e29937c..e6280deb 100644 --- a/docs/blog/04_testnet_update.md +++ b/docs/blog/04_testnet_update.md @@ -1,9 +1,61 @@ -# 2024-10-17 Testnet Update +# 2024-10-17 Announcing Testnet -- Deletable blobs and reclaiming storage -- External Storage nodes -- WAL Token used for payments and WAL <> SUI faucet -- Staking and Unstaking with Staking App -- Epoch change and shard migration -- Explorer +Today, a community of storage node operators launches the first public Testnet employing the Walrus +Storage protocol. This is an important milestone in validating the operation of Walrus by a set of +independent operators, that change over time through a delegated proof of stake mechanism. The +Testnet also brings functionality updates relating to governance, epochs, system events, and blob +deletion. + +## Blob deletion + +The most important blob-facing new feature is optional blob deletion. The uploader of a blob can +optionally indicate a blob is "deletable". This information is stored in the Sui blob meta-data +object, and is also included in the event denoting when the blob is certified. Subsequently, the +owner of the Sui blob meta-data object can "delete" it. As a result storage for the remaining +period is reclaimed and can be used by subsequent blob storage operations. + +Blob deletion allows more fine grained storage cost management: smart contracts that wrap blob +meta-data objects can define logic that stores blobs and delete them to minimize costs, and reclaim +storage space before Walrus epochs end. + +However, blob deletion is not an effective privacy +mechanism: copies of the blob may exist outside Walrus storage nodes on caches and end-user stores +or devices. Furthermore, if the identical blob is stored by multiple Walrus users, the blob will +still be available on Walrus until no copy exists. Thus deleting your own copy of a blob cannot +guarantee that it is deleted from Walrus as a whole. + +## Epochs + +Walrus Testnet enables multiple epochs. Initially an epoch is a single day to ensure the logic of +epoch change is thoroughly tested. At Mainnet epochs may be multiple weeks long. + +Now stored blob expiry epoch is meaningful, and blobs may become unavailable after their expiry +epoch. The store command can be used for a blob that are not expired to extend their expiry epoch. +This operation is efficient and only affects payments and meta-data, and does not re-upload blob +contents. + +## WAL and the Testnet WAL faucet + +Payments for blob storage and extending blob expiry epochs are denominated in Testnet WAL, a +Walrus token issued on the Sui Testnet. Testnet WAL has no value, and an unlimited supply - so no +need to covet or hoard it - its just for testing purposes and only issued on Sui Testnet. + +To make Testnet WAL available to all who want to experiment with the Walrus Testnet we provide a +utility and smart contract to convert Testnet SUI (which also has no value) into Testnet WAL using +a one-to-one exchange rate. This is chosen arbitrarily, and generally one should not read too much +into the actual WAL denominated costs of storage on Testnet. They have been chosen arbitrarily. + +## Decentralization through Staking & Unstaking + +The WAL token may also be used to stake with storage operators. Staked WAL can be unstaked and +re-staked with other operators or used to purchase storage. + +Each epoch storage nodes are selected and allocated storage shards according to their delegated +stake. At the end of each epoch payments for storing blobs for the epoch are distributed to storage +nodes and those that delegate stake to them. Furthermore, important network parameters - such as +total available storage and storage price - are set by the selected storage operators each epoch +according to their stake weight. + +A staking web dapps is provided to experiment with this functionality. Community members have also +created explorers that can be used to view storage nodes when considering who to stake with. From 3b65fafaa8de3d235bfee069de8f65654374a5a6 Mon Sep 17 00:00:00 2001 From: George Danezis Date: Thu, 10 Oct 2024 11:36:05 +0100 Subject: [PATCH 09/50] Uniformize dates in blog --- docs/SUMMARY.md | 4 ++-- docs/blog/02_devnet_update.md | 4 +++- docs/blog/04_testnet_update.md | 8 ++++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index c6451c51..e11139c6 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -10,9 +10,9 @@ - [Blog Preface](./blog/00_intro.md) - [Announcing Walrus](./blog/01_announcing_walrus.md) - - [2024-08-12 Devnet Update](./blog/02_devnet_update.md) + - [Devnet Update](./blog/02_devnet_update.md) - [Announcing the Walrus Whitepaper](./blog/03_whitepaper.md) - - [2024-10-17 Testnet Update](./blog/04_testnet_update.md) + - [Announcing Testnet](./blog/04_testnet_update.md) --- diff --git a/docs/blog/02_devnet_update.md b/docs/blog/02_devnet_update.md index 343849b4..503455ba 100644 --- a/docs/blog/02_devnet_update.md +++ b/docs/blog/02_devnet_update.md @@ -1,4 +1,6 @@ -# 2024-08-12 Devnet Update +# Devnet Update + +Published on: 2024-08-12 We have redeployed the Walrus Devnet to incorporate various improvements to the Walrus storage nodes and clients. In this process, all blobs stored on Walrus were wiped. Note that this may happen again diff --git a/docs/blog/04_testnet_update.md b/docs/blog/04_testnet_update.md index e6280deb..cb7941e3 100644 --- a/docs/blog/04_testnet_update.md +++ b/docs/blog/04_testnet_update.md @@ -1,4 +1,6 @@ -# 2024-10-17 Announcing Testnet +# Announcing Testnet + +Published on: 2024-10-17 Today, a community of storage node operators launches the first public Testnet employing the Walrus @@ -58,4 +60,6 @@ total available storage and storage price - are set by the selected storage oper according to their stake weight. A staking web dapps is provided to experiment with this functionality. Community members have also -created explorers that can be used to view storage nodes when considering who to stake with. +created explorers that can be used to view storage nodes when considering who to stake with. Staking +ensures that the ultimate governance of Walrus, directly in terms of storage nodes, and indirectly +in terms of parameters and software they chose, rests with WAL Token holders. From a38c155212b43e95b7ab15387e54602ab0c07676 Mon Sep 17 00:00:00 2001 From: George Danezis Date: Thu, 10 Oct 2024 11:46:54 +0100 Subject: [PATCH 10/50] Copy edit blog --- docs/blog/04_testnet_update.md | 37 +++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/docs/blog/04_testnet_update.md b/docs/blog/04_testnet_update.md index cb7941e3..2d47c334 100644 --- a/docs/blog/04_testnet_update.md +++ b/docs/blog/04_testnet_update.md @@ -1,17 +1,17 @@ # Announcing Testnet -Published on: 2024-10-17 - + +Published on: 2024-10-XX -Today, a community of storage node operators launches the first public Testnet employing the Walrus -Storage protocol. This is an important milestone in validating the operation of Walrus by a set of -independent operators, that change over time through a delegated proof of stake mechanism. The -Testnet also brings functionality updates relating to governance, epochs, system events, and blob -deletion. +Today, a community of operators launches the first public Walrus Testnet. +This is an important milestone in validating the operation of Walrus as a decentralized blob store, +by operating it on a set of independent storage nodes, that change over time through a delegated +proof of stake mechanism. The Testnet also brings functionality updates relating to governance, +epochs, and blob deletion. ## Blob deletion -The most important blob-facing new feature is optional blob deletion. The uploader of a blob can +The most important user-facing new feature is optional blob deletion. The uploader of a blob can optionally indicate a blob is "deletable". This information is stored in the Sui blob meta-data object, and is also included in the event denoting when the blob is certified. Subsequently, the owner of the Sui blob meta-data object can "delete" it. As a result storage for the remaining @@ -21,8 +21,8 @@ Blob deletion allows more fine grained storage cost management: smart contracts meta-data objects can define logic that stores blobs and delete them to minimize costs, and reclaim storage space before Walrus epochs end. -However, blob deletion is not an effective privacy -mechanism: copies of the blob may exist outside Walrus storage nodes on caches and end-user stores +However, blob deletion is not an effective privacy mechanism in itself: copies of the blob may exist +outside Walrus storage nodes on caches and end-user stores or devices. Furthermore, if the identical blob is stored by multiple Walrus users, the blob will still be available on Walrus until no copy exists. Thus deleting your own copy of a blob cannot guarantee that it is deleted from Walrus as a whole. @@ -30,16 +30,16 @@ guarantee that it is deleted from Walrus as a whole. ## Epochs Walrus Testnet enables multiple epochs. Initially an epoch is a single day to ensure the logic of -epoch change is thoroughly tested. At Mainnet epochs may be multiple weeks long. +epoch change is thoroughly tested. At Mainnet epochs will likely be multiple weeks long. -Now stored blob expiry epoch is meaningful, and blobs may become unavailable after their expiry -epoch. The store command can be used for a blob that are not expired to extend their expiry epoch. +Now stored blob expiry epoch is meaningful, and blobs will become unavailable after their expiry +epoch. The store command may be used to extend the expiry epoch of a blob that is still available. This operation is efficient and only affects payments and meta-data, and does not re-upload blob contents. -## WAL and the Testnet WAL faucet +## The WAL token and the Testnet WAL faucet -Payments for blob storage and extending blob expiry epochs are denominated in Testnet WAL, a +Payments for blob storage and extending blob expiry are denominated in Testnet WAL, a Walrus token issued on the Sui Testnet. Testnet WAL has no value, and an unlimited supply - so no need to covet or hoard it - its just for testing purposes and only issued on Sui Testnet. @@ -48,7 +48,7 @@ utility and smart contract to convert Testnet SUI (which also has no value) into a one-to-one exchange rate. This is chosen arbitrarily, and generally one should not read too much into the actual WAL denominated costs of storage on Testnet. They have been chosen arbitrarily. -## Decentralization through Staking & Unstaking +## Decentralization through staking & unstaking The WAL token may also be used to stake with storage operators. Staked WAL can be unstaked and re-staked with other operators or used to purchase storage. @@ -63,3 +63,8 @@ A staking web dapps is provided to experiment with this functionality. Community created explorers that can be used to view storage nodes when considering who to stake with. Staking ensures that the ultimate governance of Walrus, directly in terms of storage nodes, and indirectly in terms of parameters and software they chose, rests with WAL Token holders. + +Under the hood and over the next months we will be testing many aspects of epoch changes and +storage node committee changes: better shard allocation mechanisms upon changes or storage node +stake; efficient ways to sync state between storage nodes; as well as better ways for storage nodes +to follow Sui event streams. \ No newline at end of file From c79b60e50ce31e6b1983a9d192fa91b203057b0a Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 10 Oct 2024 12:06:56 +0200 Subject: [PATCH 11/50] minor edits --- contracts/blob_store/README.md | 4 +++- docs/README.md | 12 ++++++------ docs/dev-guide/dev-guide.md | 10 +++++----- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/contracts/blob_store/README.md b/contracts/blob_store/README.md index 79dce623..3ee9bae6 100644 --- a/contracts/blob_store/README.md +++ b/contracts/blob_store/README.md @@ -1,5 +1,7 @@ # Walrus Testnet Move contracts - + +> TODO: directory still contains the Devnet contracts, these and the package ID below need to be +> updated for Testnet. This is the Move source code for the Walrus Testnet instance. We provide this so developers can experiment with building Walrus apps that require Move extensions. This code is published on Sui diff --git a/docs/README.md b/docs/README.md index 6d0306b0..c5849fd9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,13 +13,13 @@ details on how this works. ```admonish danger title="Disclaimer about the Walrus Testnet" The current Testnet release of Walrus and Walrus Sites is a preview intended to showcase -the technology and solicit feedback from builders, users and storage node operators. -All transactions are executed on the Sui Testnet and use Testnet WAL and SUI which has no -value. The state of the store **can and will be wiped**, at any point and possibly with no warning. +the technology and solicit feedback from builders, users, and storage-node operators. +All transactions are executed on the Sui Testnet and use Testnet WAL and SUI which have no +value. The state of the store **can and will be wiped** at any point and possibly with no warning. Do not rely on this Testnet for any production purposes, it comes with no availability or persistence guarantees. -Furthermore, encodings and blob IDs may be incompatible with the future Testnet and Mainnet and +Furthermore, encodings and blob IDs may be incompatible with the future Testnet and Mainnet, and developers will be responsible for migrating any Testnet applications and data to Mainnet. Detailed migration guides will be provided when Mainnet becomes available. @@ -51,10 +51,10 @@ confidentiality. objects on Sui, which means that smart contracts can check whether a blob is available and for how long, extend its lifetime or optionally delete it. -- **Epochs, Tokenomics and Delegated Proof of Stake** Walrus is operated by a committee of storage +- **Epochs, tokenomics, and delegated proof of stake** Walrus is operated by a committee of storage nodes that evolve between epochs. A native token, WAL (and its subdivision FROST), is used to delegate stake to storage nodes, and those with high stake become part of the epoch committee. - The WAL token is also used for payments for storage. At the end of each epoch rewards for + The WAL token is also used for payments for storage. At the end of each epoch, rewards for selecting storage nodes, storing and serving blobs are distributed to storage nodes and whose that stake with them. All these processes are mediated by smart contracts on the Sui platform. diff --git a/docs/dev-guide/dev-guide.md b/docs/dev-guide/dev-guide.md index dd43e65c..a41d05e0 100644 --- a/docs/dev-guide/dev-guide.md +++ b/docs/dev-guide/dev-guide.md @@ -14,15 +14,15 @@ This developer guide describes the following: Refer again to the [glossary](../glossary.md) of terms as a reference. -```admonish danger title="Disclaimer about the Walrus developer preview" +```admonish danger title="Disclaimer about the Walrus Testnet" The current Testnet release of Walrus and Walrus Sites is a preview intended to showcase -the technology and solicit feedback from builders, users and storage node operators. -All transactions are executed on the Sui Testnet and use Testnet WAL and SUI which has no -value. The state of the store **can and will be wiped**, at any point and possibly with no warning. +the technology and solicit feedback from builders, users, and storage-node operators. +All transactions are executed on the Sui Testnet and use Testnet WAL and SUI which have no +value. The state of the store **can and will be wiped** at any point and possibly with no warning. Do not rely on this Testnet for any production purposes, it comes with no availability or persistence guarantees. -Furthermore, encodings and blob IDs may be incompatible with the future Testnet and Mainnet and +Furthermore, encodings and blob IDs may be incompatible with the future Testnet and Mainnet, and developers will be responsible for migrating any Testnet applications and data to Mainnet. Detailed migration guides will be provided when Mainnet becomes available. From 1da82ba0f8d52771eb9539544e21e7630ec9bc6e Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 10 Oct 2024 13:01:41 +0200 Subject: [PATCH 12/50] update setup instructions for our current PTN --- docs/dev-guide/sui-struct.md | 2 +- docs/usage/setup.md | 60 ++++++++++++++++++++++-------------- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/docs/dev-guide/sui-struct.md b/docs/dev-guide/sui-struct.md index c7a6f32c..92a64997 100644 --- a/docs/dev-guide/sui-struct.md +++ b/docs/dev-guide/sui-struct.md @@ -149,7 +149,7 @@ between epochs. And associated events such as `ShardsReceived`, `EpochParameters and `ShardRecoveryStart` indicate storage node level events related to epoch transitions, shard migrations and epoch parameters. -## System information +## System and staking information The Walrus system object contains metadata about the available and used storage, as well as the price of storage per KiB of storage in FROST. The committee diff --git a/docs/usage/setup.md b/docs/usage/setup.md index 7c9e99b3..f6c37ee8 100644 --- a/docs/usage/setup.md +++ b/docs/usage/setup.md @@ -75,19 +75,23 @@ when [running the CLI](./interacting.md). We currently provide the `walrus` client binary for macOS (Intel and Apple CPUs) and Ubuntu: -| OS | CPU | Architecture | -| ------ | --------------------- | -------------------------------------------------------------------------------------------------------------------- | -| MacOS | Apple Silicon | [`macos-arm64`](https://storage.googleapis.com/mysten-walrus-binaries/walrus-latest-macos-arm64) | -| MacOS | Intel 64bit | [`macos-x86_64`](https://storage.googleapis.com/mysten-walrus-binaries/walrus-latest-macos-x86_64) | -| Ubuntu | Intel 64bit | [`ubuntu-x86_64`](https://storage.googleapis.com/mysten-walrus-binaries/walrus-latest-ubuntu-x86_64) | -| Ubuntu | Intel 64bit (generic) | [`ubuntu-x86_64-generic`](https://storage.googleapis.com/mysten-walrus-binaries/walrus-latest-ubuntu-x86_64-generic) | +| OS | CPU | Architecture | +| ------ | ------------- | ------------------------------------------------------------------------------------------------------------ | +| MacOS | Apple Silicon | [`macos-arm64`](https://storage.googleapis.com/mysten-walrus-binaries/walrus-testnet-latest-macos-arm64) | +| MacOS | Intel 64bit | [`macos-x86_64`](https://storage.googleapis.com/mysten-walrus-binaries/walrus-testnet-latest-macos-x86_64) | +| Ubuntu | Intel 64bit | [`ubuntu-x86_64`](https://storage.googleapis.com/mysten-walrus-binaries/walrus-testnet-latest-ubuntu-x86_64) | + + + | + + You can download the latest build from our Google Cloud Storage (GCS) bucket (correctly setting the `$SYSTEM` variable)`: ```sh SYSTEM=ubuntu-x86_64 # or macos-x86_64 or macos-arm64 -curl https://storage.googleapis.com/mysten-walrus-binaries/walrus-latest-$SYSTEM -o walrus +curl https://storage.googleapis.com/mysten-walrus-binaries/walrus-testnet-latest-$SYSTEM -o walrus chmod +x walrus ``` @@ -163,15 +167,18 @@ The `walrus get-wal --help` command provides more information about those. ## Configuration -A single parameter is required to configure Walrus, namely the ID of the [system -object](../dev-guide/sui-struct.md#system-information) on Sui. You can create your client -configuration as follows: +The Walrus client needs to know about the Sui objects that store the Walrus system and staking +information, see the [developer guide](../dev-guide/sui-struct.md#system-and-staking-information). +These need to be configured in a file `~/.config/walrus/client_config.yaml`. - -```sh -mkdir -p ~/.config/walrus -curl https://storage.googleapis.com/mysten-walrus-binaries/walrus-configs/client_config.yaml \ - -o ~/.config/walrus/client_config.yaml +The current Testnet deployment uses the following objects: + + + +```yaml +system_object: 0x24d91f106fc001a04bda01922295ea96a299bffa06c03679b5becd74acaf43d3 +staking_object: 0xa006240ac8a29f60644e1eb4785a3417aa165b497ea50e4c969e1fd89d541b77 +exchange_object: 0x5246ab7860b3c661af2bc6555fe68b5299a52402c82ecb96b6ad6ff7c2bc20b3 ``` ### Custom path (optional) {#config-custom-path} @@ -188,12 +195,11 @@ you need to use the `--config` option when running the `walrus` binary. The configuration file currently supports the following parameters: ```yaml -# This is the only mandatory field. The system object is specific for a particular Walrus -# deployment. -# -# NOTE: THE VALUE INCLUDED HERE IS AN EXAMPLE VALUE. -# You can get the object ID for the current Walrus Devnet deployment as described above. -system_object: 0x3243.... +# These are the only mandatory fields. These objects are specific for a particular Walrus +# deployment but then do not change over time. +system_object: 0x24d91f106fc001a04bda01922295ea96a299bffa06c03679b5becd74acaf43d3 +staking_object: 0xa006240ac8a29f60644e1eb4785a3417aa165b497ea50e4c969e1fd89d541b77 +exchange_object: 0x5246ab7860b3c661af2bc6555fe68b5299a52402c82ecb96b6ad6ff7c2bc20b3 # You can define a custom path to your Sui wallet configuration here. If this is unset or `null`, # the wallet is configured from `./sui_config.yaml` (relative to your current working directory), or @@ -206,11 +212,12 @@ wallet_config: null communication_config: max_concurrent_writes: null max_concurrent_sliver_reads: null - max_concurrent_metadata_reads: 3 + max_concurrent_metadata_reads: null max_concurrent_status_reads: null + max_data_in_flight: null reqwest_config: total_timeout: - secs: 180 + secs: 30 nanos: 0 pool_idle_timeout: null http2_keep_alive_timeout: @@ -229,6 +236,13 @@ communication_config: max_backoff: secs: 60 nanos: 0 + disable_proxy: false + disable_native_certs: false + sliver_write_extra_time: + factor: 0.5 + base: + secs: 0 + nanos: 500000000 ``` ```admonish warning title="Important" From 047ae7ffac655325178829cfcd2ea523cc9a31b4 Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 10 Oct 2024 13:13:30 +0200 Subject: [PATCH 13/50] minor fixes to setup instructions --- docs/blog/04_testnet_update.md | 4 +- docs/usage/setup.md | 67 +++++++++++++++++----------------- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/docs/blog/04_testnet_update.md b/docs/blog/04_testnet_update.md index 2d47c334..9c262b48 100644 --- a/docs/blog/04_testnet_update.md +++ b/docs/blog/04_testnet_update.md @@ -1,4 +1,4 @@ -# Announcing Testnet +# Announcing Testnet Published on: 2024-10-XX @@ -67,4 +67,4 @@ in terms of parameters and software they chose, rests with WAL Token holders. Under the hood and over the next months we will be testing many aspects of epoch changes and storage node committee changes: better shard allocation mechanisms upon changes or storage node stake; efficient ways to sync state between storage nodes; as well as better ways for storage nodes -to follow Sui event streams. \ No newline at end of file +to follow Sui event streams. diff --git a/docs/usage/setup.md b/docs/usage/setup.md index f6c37ee8..8c724543 100644 --- a/docs/usage/setup.md +++ b/docs/usage/setup.md @@ -95,9 +95,12 @@ curl https://storage.googleapis.com/mysten-walrus-binaries/walrus-testnet-latest chmod +x walrus ``` + + To be able to run it simply as `walrus`, move the binary to any directory included in your `$PATH` environment variable. Standard locations are `/usr/local/bin/`, `$HOME/bin/`, or @@ -106,7 +109,7 @@ environment variable. Standard locations are `/usr/local/bin/`, `$HOME/bin/`, or ```admonish warn Previously, this guide recommended placing the binary in `$HOME/.local/bin/`. If you install the latest binary somewhere else, make sure to clean up old versions. You can find the binary in use by -calling `which walrus`. +calling `which walrus` and its version through `walrus -V`. ``` Once this is done, you should be able to simply type `walrus` in your terminal. For example you can @@ -135,36 +138,6 @@ In addition to the latest version of the `walrus` binary, the GCS bucket also co versions. An overview in XML format is available at . -## Testnet WAL faucet - -The Walrus Testnet uses Testnet WAL tokens to buy storage and stake. Testnet WAL tokens have no -value and can be exchanged for some Testnet SUI tokens, which also have no value, thought the -command: - -```sh -walrus get-wal -``` - -You can check you have received Testnet WAL by checking the Sui balances: - -```sh -sui client balance -╭─────────────────────────────────────────╮ -│ Balance of coins owned by this address │ -├─────────────────────────────────────────┤ -│ ╭─────────────────────────────────────╮ │ -│ │ coin balance (raw) balance │ │ -│ ├─────────────────────────────────────┤ │ -│ │ Sui 8869252670 8.86 SUI │ │ -│ │ WAL 500000000 0.50 WAL │ │ -│ ╰─────────────────────────────────────╯ │ -╰─────────────────────────────────────────╯ -``` - -By default 0.5 SUI are exchanged for 0.5 WAL, but a different amount of SUI may be exchanged -using the `--amount` option, and a specific coin ID may be used through the `--exchange-id`. -The `walrus get-wal --help` command provides more information about those. - ## Configuration The Walrus client needs to know about the Sui objects that store the Walrus system and staking @@ -190,8 +163,6 @@ you need to use the `--config` option when running the `walrus` binary. ### Advanced configuration (optional) - - The configuration file currently supports the following parameters: ```yaml @@ -248,3 +219,33 @@ communication_config: ```admonish warning title="Important" If you specify a wallet path, make sure your wallet is set up for Sui **Testnet**. ``` + +## Testnet WAL faucet + +The Walrus Testnet uses Testnet WAL tokens to buy storage and stake. Testnet WAL tokens have no +value and can be exchanged for some Testnet SUI tokens, which also have no value, through the +command: + +```sh +walrus get-wal +``` + +You can check that you have received Testnet WAL by checking the Sui balances: + +```sh +sui client balance +╭─────────────────────────────────────────╮ +│ Balance of coins owned by this address │ +├─────────────────────────────────────────┤ +│ ╭─────────────────────────────────────╮ │ +│ │ coin balance (raw) balance │ │ +│ ├─────────────────────────────────────┤ │ +│ │ Sui 8869252670 8.86 SUI │ │ +│ │ WAL 500000000 0.50 WAL │ │ +│ ╰─────────────────────────────────────╯ │ +╰─────────────────────────────────────────╯ +``` + +By default, 0.5 SUI are exchanged for 0.5 WAL, but a different amount of SUI may be exchanged using +the `--amount` option, and a specific SUI/WAL exchange object may be used through the +`--exchange-id` option. The `walrus get-wal --help` command provides more information about those. From 035522aad573809ff28c8a4efa203c3a64fa10de Mon Sep 17 00:00:00 2001 From: George Danezis Date: Thu, 10 Oct 2024 12:32:09 +0100 Subject: [PATCH 14/50] Ops update --- docs/dev-guide/dev-operations.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/dev-guide/dev-operations.md b/docs/dev-guide/dev-operations.md index ff41e069..9a201489 100644 --- a/docs/dev-guide/dev-operations.md +++ b/docs/dev-guide/dev-operations.md @@ -35,7 +35,7 @@ client, or a publisher that accepts and publishes blobs via HTTP. Walrus currently allows the storage of blobs up to a maximum size that may be determined through the [`walrus info`](../usage/client-cli.md#walrus-system-information) CLI command. The -maximum blob size is currently 957 MiB. You may store larger blobs by splitting them into +maximum blob size is currently 13.3 GiB. You may store larger blobs by splitting them into smaller chunks. Blobs are stored for a certain number of *epochs*, as specified at the time they were stored. Walrus @@ -67,9 +67,10 @@ may currently be done in 3 different ways: used to authenticate the certified blob event emitted when the blob ID was certified on Sui. The client `walrus blob-status` command may be used to identify the event ID that needs to be checked. - A Sui SDK read may be - used to authenticate the Sui blob object corresponding to the blob ID, and check it is certified. + used to authenticate the Sui blob object corresponding to the blob ID, and check it is certified, + before the expiry epoch, and not deletable. - A Sui smart contract can read the blob object on Sui (or a reference to it) to check - is is certified. + is is certified, before the expiry epoch, and not deletable. The underlying protocol of the [Sui light client](https://github.com/MystenLabs/sui/tree/main/crates/sui-light-client) @@ -79,3 +80,15 @@ for the blob ID for a certain number of epochs. Once a blob is certified, Walrus will ensure that sufficient slivers will always be available on storage nodes to recover it within the specified epochs. + +## Delete + +Stored blobs can be optionally set as deletable by the user that creates them. This meta-data is +stored in the Sui blob object, and whether a blob is deletable or not is included in certified blob +events. A deletable blob may be deleted by the owner of the blob object, to reclaim and re-use +the storage resource associated with it. + +If no other copies of the blob exist in Walrus, deleting a blob will eventually make it +unrecoverable using read commands. However, if other copies of the blob exist on Walrus, a delete +command will reclaim storage space for the user that invoked it, but will not make the blob +unavailable until all other copies have been deleted or expire. \ No newline at end of file From 51727ed24ec96f8a571737bf2721d007617cec14 Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 10 Oct 2024 15:00:02 +0200 Subject: [PATCH 15/50] don't emphasize Sui testnet as much --- docs/dev-guide/dev-operations.md | 2 +- docs/usage/setup.md | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/dev-guide/dev-operations.md b/docs/dev-guide/dev-operations.md index 9a201489..c47fe090 100644 --- a/docs/dev-guide/dev-operations.md +++ b/docs/dev-guide/dev-operations.md @@ -91,4 +91,4 @@ the storage resource associated with it. If no other copies of the blob exist in Walrus, deleting a blob will eventually make it unrecoverable using read commands. However, if other copies of the blob exist on Walrus, a delete command will reclaim storage space for the user that invoked it, but will not make the blob -unavailable until all other copies have been deleted or expire. \ No newline at end of file +unavailable until all other copies have been deleted or expire. diff --git a/docs/usage/setup.md b/docs/usage/setup.md index 8c724543..6cb5a3ed 100644 --- a/docs/usage/setup.md +++ b/docs/usage/setup.md @@ -6,13 +6,11 @@ patterns (see [the next chapter](./interacting.md)). This chapter describes the [prerequisites](#prerequisites), [installation](#installation), and [configuration](#configuration) of the Walrus client. -```admonish note -Note that the Walrus Testnet uses Sui **Testnet** for coordination. -``` - ## Prerequisites -Interacting with Walrus requires a valid Sui **Testnet** wallet with some amount of SUI tokens. The + + +Interacting with Walrus requires a valid Sui Testnet wallet with some amount of SUI tokens. The easiest way to set this up is via the Sui CLI; see the [installation instructions](https://docs.sui.io/guides/developer/getting-started/sui-install) in the Sui documentation. From ad8238296db3b8a9695238d5c7bc46a64bc1b40c Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 10 Oct 2024 15:00:18 +0200 Subject: [PATCH 16/50] place setup at the front of the usage chapter --- docs/SUMMARY.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index e11139c6..dbbc4c44 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -30,6 +30,11 @@ # Usage +- [Setup](./usage/setup.md) +- [Interacting with Walrus](./usage/interacting.md) + - [Using the client CLI](./usage/client-cli.md) + - [Using the client JSON API](./usage/json-api.md) + - [Using the client HTTP API](./usage/web-api.md) - [Developer guide](./dev-guide/dev-guide.md) - [Components](./dev-guide/components.md) - [Operations](./dev-guide/dev-operations.md) @@ -37,13 +42,8 @@ - [Operator guide](./operator-guide/operator-guide.md) - [Storage node](./operator-guide/storage-node.md) - [Aggregator](./operator-guide/aggregator.md) -- [Setup](./usage/setup.md) -- [Interacting with Walrus](./usage/interacting.md) - - [Using the client CLI](./usage/client-cli.md) - - [Using the client JSON API](./usage/json-api.md) - - [Using the client HTTP API](./usage/web-api.md) -- [Troubleshooting](./usage/troubleshooting.md) - [Examples](./usage/examples.md) +- [Troubleshooting](./usage/troubleshooting.md) # Walrus Sites From b2eb48fa38e989a0618ddf7e58414b81c638790c Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 10 Oct 2024 15:02:44 +0200 Subject: [PATCH 17/50] typo --- docs/usage/setup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/setup.md b/docs/usage/setup.md index 6cb5a3ed..38f8c7bc 100644 --- a/docs/usage/setup.md +++ b/docs/usage/setup.md @@ -80,7 +80,7 @@ We currently provide the `walrus` client binary for macOS (Intel and Apple CPUs) | Ubuntu | Intel 64bit | [`ubuntu-x86_64`](https://storage.googleapis.com/mysten-walrus-binaries/walrus-testnet-latest-ubuntu-x86_64) | - | + From 5cb8dfb446631953a1b8b6803d3bfceedd67560b Mon Sep 17 00:00:00 2001 From: George Danezis Date: Thu, 10 Oct 2024 16:00:07 +0100 Subject: [PATCH 18/50] Added links to blog + stake docs placeholder --- docs/SUMMARY.md | 1 + docs/blog/04_testnet_update.md | 20 ++++++++++++++++++++ docs/usage/interacting.md | 2 ++ docs/usage/stake.md | 7 +++++++ 4 files changed, 30 insertions(+) create mode 100644 docs/usage/stake.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index e11139c6..973e3e00 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -42,6 +42,7 @@ - [Using the client CLI](./usage/client-cli.md) - [Using the client JSON API](./usage/json-api.md) - [Using the client HTTP API](./usage/web-api.md) +- [Stake and Unstake](./usage/stake.md) - [Troubleshooting](./usage/troubleshooting.md) - [Examples](./usage/examples.md) diff --git a/docs/blog/04_testnet_update.md b/docs/blog/04_testnet_update.md index 9c262b48..792f67b3 100644 --- a/docs/blog/04_testnet_update.md +++ b/docs/blog/04_testnet_update.md @@ -27,6 +27,11 @@ or devices. Furthermore, if the identical blob is stored by multiple Walrus user still be available on Walrus until no copy exists. Thus deleting your own copy of a blob cannot guarantee that it is deleted from Walrus as a whole. +- Find out how to + [upload and delete deletable blobs](../usage/client-cli.md#reclaiming-space-via-deletable-blobs) + thought the CLI. +- Find out more about how [delete operations work](../dev-guide/dev-operations.md#delete). + ## Epochs Walrus Testnet enables multiple epochs. Initially an epoch is a single day to ensure the logic of @@ -37,6 +42,10 @@ epoch. The store command may be used to extend the expiry epoch of a blob that i This operation is efficient and only affects payments and meta-data, and does not re-upload blob contents. +- Find out the [current epoch](../usage/client-cli.md#walrus-system-information) through the CLI. +- Find out how to store a blob for + [multiple epochs](../usage/client-cli.md#storing-querying-status-and-reading-blobs). + ## The WAL token and the Testnet WAL faucet Payments for blob storage and extending blob expiry are denominated in Testnet WAL, a @@ -48,6 +57,8 @@ utility and smart contract to convert Testnet SUI (which also has no value) into a one-to-one exchange rate. This is chosen arbitrarily, and generally one should not read too much into the actual WAL denominated costs of storage on Testnet. They have been chosen arbitrarily. +- Find out how to [request Test WAL tokens](../usage/setup.md#testnet-wal-faucet) through the CLI. + ## Decentralization through staking & unstaking The WAL token may also be used to stake with storage operators. Staked WAL can be unstaked and @@ -68,3 +79,12 @@ Under the hood and over the next months we will be testing many aspects of epoch storage node committee changes: better shard allocation mechanisms upon changes or storage node stake; efficient ways to sync state between storage nodes; as well as better ways for storage nodes to follow Sui event streams. + +- Explore the [Walrus staking dapp]() +- Look at recent activity on the [Walrus Explorer]() + +## New Move Contracts & documentation + +As part of the Testnet release of Walrus the documentation and Move Smart contracts have been +updated, and can be found at the [Walrus-docs repository](https://github.com/MystenLabs/walrus-docs) +and as a [Walrus Docs Site](https://docs.walrus.site/). \ No newline at end of file diff --git a/docs/usage/interacting.md b/docs/usage/interacting.md index 67b4cd4f..7111043c 100644 --- a/docs/usage/interacting.md +++ b/docs/usage/interacting.md @@ -5,3 +5,5 @@ We provide 3 ways to interact directly with the Walrus storage system: - Through the Walrus [client command line interface (CLI)](./client-cli.md). - Through a [JSON API](./json-api.md) of the Walrus CLI. - Through an [HTTP API](./web-api.md) exposed by a public or local Walrus client daemon. + +Furthermore, users can [stake and unstake](./stake.md) through the staking dapp or Sui smart contracts. diff --git a/docs/usage/stake.md b/docs/usage/stake.md new file mode 100644 index 00000000..ee5a06b0 --- /dev/null +++ b/docs/usage/stake.md @@ -0,0 +1,7 @@ +# Stake and Unstake + + + +- Stake / Unstake dapp link and docs +- How to monitor nodes for stake / apr etc +- Move contracts to stake / unstake \ No newline at end of file From c85f673abcfe42b8d007e3a00154e11dd1f5dc0a Mon Sep 17 00:00:00 2001 From: George Danezis Date: Thu, 10 Oct 2024 16:03:39 +0100 Subject: [PATCH 19/50] lint --- docs/blog/04_testnet_update.md | 2 +- docs/usage/stake.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/blog/04_testnet_update.md b/docs/blog/04_testnet_update.md index 792f67b3..ef4ff3dc 100644 --- a/docs/blog/04_testnet_update.md +++ b/docs/blog/04_testnet_update.md @@ -87,4 +87,4 @@ to follow Sui event streams. As part of the Testnet release of Walrus the documentation and Move Smart contracts have been updated, and can be found at the [Walrus-docs repository](https://github.com/MystenLabs/walrus-docs) -and as a [Walrus Docs Site](https://docs.walrus.site/). \ No newline at end of file +and as a [Walrus Docs Site](https://docs.walrus.site/). diff --git a/docs/usage/stake.md b/docs/usage/stake.md index ee5a06b0..47e908ee 100644 --- a/docs/usage/stake.md +++ b/docs/usage/stake.md @@ -4,4 +4,4 @@ - Stake / Unstake dapp link and docs - How to monitor nodes for stake / apr etc -- Move contracts to stake / unstake \ No newline at end of file +- Move contracts to stake / unstake From d5653a10bc64fc2e2383f16d5617c12d775ee8c6 Mon Sep 17 00:00:00 2001 From: George Danezis Date: Thu, 10 Oct 2024 17:11:37 +0100 Subject: [PATCH 20/50] Fix lints --- docs/blog/04_testnet_update.md | 7 ++++--- docs/usage/interacting.md | 3 ++- docs/usage/stake.md | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/blog/04_testnet_update.md b/docs/blog/04_testnet_update.md index ef4ff3dc..77559227 100644 --- a/docs/blog/04_testnet_update.md +++ b/docs/blog/04_testnet_update.md @@ -70,7 +70,7 @@ nodes and those that delegate stake to them. Furthermore, important network para total available storage and storage price - are set by the selected storage operators each epoch according to their stake weight. -A staking web dapps is provided to experiment with this functionality. Community members have also +A staking web dApps is provided to experiment with this functionality. Community members have also created explorers that can be used to view storage nodes when considering who to stake with. Staking ensures that the ultimate governance of Walrus, directly in terms of storage nodes, and indirectly in terms of parameters and software they chose, rests with WAL Token holders. @@ -80,8 +80,9 @@ storage node committee changes: better shard allocation mechanisms upon changes stake; efficient ways to sync state between storage nodes; as well as better ways for storage nodes to follow Sui event streams. -- Explore the [Walrus staking dapp]() -- Look at recent activity on the [Walrus Explorer]() + +- Explore the [Walrus staking dApp](https://app.org) +- Look at recent activity on the [Walrus Explorer](https://app.org) ## New Move Contracts & documentation diff --git a/docs/usage/interacting.md b/docs/usage/interacting.md index 7111043c..5aa2c8a3 100644 --- a/docs/usage/interacting.md +++ b/docs/usage/interacting.md @@ -6,4 +6,5 @@ We provide 3 ways to interact directly with the Walrus storage system: - Through a [JSON API](./json-api.md) of the Walrus CLI. - Through an [HTTP API](./web-api.md) exposed by a public or local Walrus client daemon. -Furthermore, users can [stake and unstake](./stake.md) through the staking dapp or Sui smart contracts. +Furthermore, users can [stake and unstake](./stake.md) through the staking dApp or Sui smart +contracts. diff --git a/docs/usage/stake.md b/docs/usage/stake.md index 47e908ee..133025c1 100644 --- a/docs/usage/stake.md +++ b/docs/usage/stake.md @@ -2,6 +2,6 @@ -- Stake / Unstake dapp link and docs +- Stake / Unstake dApp link and docs - How to monitor nodes for stake / apr etc - Move contracts to stake / unstake From f5ac4bab6ee06cf602138b903e3d56d5f1fa2011 Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Fri, 11 Oct 2024 10:04:52 +0200 Subject: [PATCH 21/50] add latest object IDs of PTN --- docs/usage/setup.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/usage/setup.md b/docs/usage/setup.md index 38f8c7bc..eea3e475 100644 --- a/docs/usage/setup.md +++ b/docs/usage/setup.md @@ -147,9 +147,9 @@ The current Testnet deployment uses the following objects: ```yaml -system_object: 0x24d91f106fc001a04bda01922295ea96a299bffa06c03679b5becd74acaf43d3 -staking_object: 0xa006240ac8a29f60644e1eb4785a3417aa165b497ea50e4c969e1fd89d541b77 -exchange_object: 0x5246ab7860b3c661af2bc6555fe68b5299a52402c82ecb96b6ad6ff7c2bc20b3 +system_object: 0xccb3f5c1f63adf8d1e40d9bda649cc6ed3a46d399ce2b205b187e028f4253e57 +staking_object: 0x6d1380cc205471c73fc048033d0c4f031fc1ac3628a27a1baf5e17729a396345 +exchange_object: 0x41d3fdd9c5007d551d005af097af45ad37c1ba5f15b7b50ad5c1072bd069dcb6 ``` ### Custom path (optional) {#config-custom-path} @@ -166,9 +166,9 @@ The configuration file currently supports the following parameters: ```yaml # These are the only mandatory fields. These objects are specific for a particular Walrus # deployment but then do not change over time. -system_object: 0x24d91f106fc001a04bda01922295ea96a299bffa06c03679b5becd74acaf43d3 -staking_object: 0xa006240ac8a29f60644e1eb4785a3417aa165b497ea50e4c969e1fd89d541b77 -exchange_object: 0x5246ab7860b3c661af2bc6555fe68b5299a52402c82ecb96b6ad6ff7c2bc20b3 +system_object: 0xccb3f5c1f63adf8d1e40d9bda649cc6ed3a46d399ce2b205b187e028f4253e57 +staking_object: 0x6d1380cc205471c73fc048033d0c4f031fc1ac3628a27a1baf5e17729a396345 +exchange_object: 0x41d3fdd9c5007d551d005af097af45ad37c1ba5f15b7b50ad5c1072bd069dcb6 # You can define a custom path to your Sui wallet configuration here. If this is unset or `null`, # the wallet is configured from `./sui_config.yaml` (relative to your current working directory), or From 1077f51692650803ee1c19bb7e84179877fe1025 Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Fri, 11 Oct 2024 18:33:26 +0200 Subject: [PATCH 22/50] add latest contracts --- .gitignore | 1 + contracts/blob_store/Move.lock | 26 - contracts/blob_store/sources/blob.move | 304 -------- contracts/blob_store/sources/blob_events.move | 50 -- .../blob_store/sources/bls_aggregate.move | 116 --- contracts/blob_store/sources/committee.move | 165 ---- contracts/blob_store/sources/e2etest.move | 40 - contracts/blob_store/sources/encoding.move | 20 - contracts/blob_store/sources/redstuff.move | 106 --- .../sources/storage_accounting.move | 131 ---- .../blob_store/sources/storage_node.move | 76 -- .../blob_store/sources/storage_resource.move | 146 ---- contracts/blob_store/sources/system.move | 370 --------- .../blob_store/sources/tests/blob_tests.move | 647 ---------------- .../blob_store/sources/tests/bls_tests.move | 205 ----- .../sources/tests/committee_cert_tests.move | 75 -- .../sources/tests/epoch_change_tests.move | 288 ------- .../sources/tests/invalid_tests.move | 303 -------- .../sources/tests/ringbuffer_tests.move | 44 -- .../sources/tests/storage_resource_tests.move | 125 --- contracts/walrus/Move.lock | 45 ++ contracts/{blob_store => walrus}/Move.toml | 11 +- contracts/{blob_store => walrus}/README.md | 8 +- .../docs/msg_formats.txt | 11 +- contracts/walrus/sources/init.move | 49 ++ contracts/walrus/sources/staking.move | 280 +++++++ .../walrus/sources/staking/active_set.move | 281 +++++++ .../walrus/sources/staking/committee.move | 135 ++++ .../walrus/sources/staking/exchange_rate.move | 56 ++ .../sources/staking/pending_values.move | 79 ++ .../walrus/sources/staking/staked_wal.move | 172 +++++ .../walrus/sources/staking/staking_inner.move | 731 ++++++++++++++++++ .../walrus/sources/staking/staking_pool.move | 480 ++++++++++++ .../walrus/sources/staking/storage_node.move | 158 ++++ .../sources/staking/walrus_context.move | 42 + contracts/walrus/sources/system.move | 223 ++++++ contracts/walrus/sources/system/blob.move | 307 ++++++++ .../walrus/sources/system/bls_aggregate.move | 199 +++++ contracts/walrus/sources/system/encoding.move | 20 + .../sources/system/epoch_parameters.move | 55 ++ .../walrus/sources/system/event_blob.move | 159 ++++ contracts/walrus/sources/system/events.move | 146 ++++ contracts/walrus/sources/system/messages.move | 273 +++++++ contracts/walrus/sources/system/metadata.move | 37 + contracts/walrus/sources/system/redstuff.move | 104 +++ .../walrus/sources/system/shared_blob.move | 43 ++ .../sources/system/storage_accounting.move | 137 ++++ .../sources/system/storage_resource.move | 144 ++++ .../sources/system/system_state_inner.move | 528 +++++++++++++ docs/dev-guide/sui-struct.md | 2 +- examples/move/walrus_dep/Move.toml | 2 +- 51 files changed, 4903 insertions(+), 3252 deletions(-) delete mode 100644 contracts/blob_store/Move.lock delete mode 100644 contracts/blob_store/sources/blob.move delete mode 100644 contracts/blob_store/sources/blob_events.move delete mode 100644 contracts/blob_store/sources/bls_aggregate.move delete mode 100644 contracts/blob_store/sources/committee.move delete mode 100644 contracts/blob_store/sources/e2etest.move delete mode 100644 contracts/blob_store/sources/encoding.move delete mode 100644 contracts/blob_store/sources/redstuff.move delete mode 100644 contracts/blob_store/sources/storage_accounting.move delete mode 100644 contracts/blob_store/sources/storage_node.move delete mode 100644 contracts/blob_store/sources/storage_resource.move delete mode 100644 contracts/blob_store/sources/system.move delete mode 100644 contracts/blob_store/sources/tests/blob_tests.move delete mode 100644 contracts/blob_store/sources/tests/bls_tests.move delete mode 100644 contracts/blob_store/sources/tests/committee_cert_tests.move delete mode 100644 contracts/blob_store/sources/tests/epoch_change_tests.move delete mode 100644 contracts/blob_store/sources/tests/invalid_tests.move delete mode 100644 contracts/blob_store/sources/tests/ringbuffer_tests.move delete mode 100644 contracts/blob_store/sources/tests/storage_resource_tests.move create mode 100644 contracts/walrus/Move.lock rename contracts/{blob_store => walrus}/Move.toml (50%) rename contracts/{blob_store => walrus}/README.md (62%) rename contracts/{blob_store => walrus}/docs/msg_formats.txt (82%) create mode 100644 contracts/walrus/sources/init.move create mode 100644 contracts/walrus/sources/staking.move create mode 100644 contracts/walrus/sources/staking/active_set.move create mode 100644 contracts/walrus/sources/staking/committee.move create mode 100644 contracts/walrus/sources/staking/exchange_rate.move create mode 100644 contracts/walrus/sources/staking/pending_values.move create mode 100644 contracts/walrus/sources/staking/staked_wal.move create mode 100644 contracts/walrus/sources/staking/staking_inner.move create mode 100644 contracts/walrus/sources/staking/staking_pool.move create mode 100644 contracts/walrus/sources/staking/storage_node.move create mode 100644 contracts/walrus/sources/staking/walrus_context.move create mode 100644 contracts/walrus/sources/system.move create mode 100644 contracts/walrus/sources/system/blob.move create mode 100644 contracts/walrus/sources/system/bls_aggregate.move create mode 100644 contracts/walrus/sources/system/encoding.move create mode 100644 contracts/walrus/sources/system/epoch_parameters.move create mode 100644 contracts/walrus/sources/system/event_blob.move create mode 100644 contracts/walrus/sources/system/events.move create mode 100644 contracts/walrus/sources/system/messages.move create mode 100644 contracts/walrus/sources/system/metadata.move create mode 100644 contracts/walrus/sources/system/redstuff.move create mode 100644 contracts/walrus/sources/system/shared_blob.move create mode 100644 contracts/walrus/sources/system/storage_accounting.move create mode 100644 contracts/walrus/sources/system/storage_resource.move create mode 100644 contracts/walrus/sources/system/system_state_inner.move diff --git a/.gitignore b/.gitignore index 8f6bdec8..e20a84bc 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ working_dir # Walrus binary and configuration walrus +!/contracts/walrus examples/CONFIG/bin/walrus client_config.yaml examples/CONFIG/config_dir/client_config.yaml diff --git a/contracts/blob_store/Move.lock b/contracts/blob_store/Move.lock deleted file mode 100644 index 0b6f01a9..00000000 --- a/contracts/blob_store/Move.lock +++ /dev/null @@ -1,26 +0,0 @@ -# @generated by Move, please check-in and do not edit manually. - -[move] -version = 2 -manifest_digest = "C461A25DAED3234921DF6DD2B4AE93FF11BA907C5A5CF469400757C595C15B68" -deps_digest = "F8BBB0CCB2491CA29A3DF03D6F92277A4F3574266507ACD77214D37ECA3F3082" -dependencies = [ - { name = "Sui" }, -] - -[[move.package]] -name = "MoveStdlib" -source = { git = "https://github.com/MystenLabs/sui.git", rev = "testnet-v1.31.1", subdir = "crates/sui-framework/packages/move-stdlib" } - -[[move.package]] -name = "Sui" -source = { git = "https://github.com/MystenLabs/sui.git", rev = "testnet-v1.31.1", subdir = "crates/sui-framework/packages/sui-framework" } - -dependencies = [ - { name = "MoveStdlib" }, -] - -[move.toolchain-version] -compiler-version = "1.30.1" -edition = "2024.beta" -flavor = "sui" diff --git a/contracts/blob_store/sources/blob.move b/contracts/blob_store/sources/blob.move deleted file mode 100644 index 85877a80..00000000 --- a/contracts/blob_store/sources/blob.move +++ /dev/null @@ -1,304 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -module blob_store::blob { - use sui::bcs; - use sui::hash; - - use blob_store::committee::{Self, CertifiedMessage}; - use blob_store::system::System; - use blob_store::storage_resource::{ - Storage, - start_epoch, - end_epoch, - storage_size, - fuse_periods, - destroy, - }; - use blob_store::encoding; - use blob_store::blob_events::{emit_blob_registered, emit_blob_certified}; - - // A certify blob message structure - const BLOB_CERT_MSG_TYPE: u8 = 1; - - // Error codes - const EInvalidMsgType: u64 = 1; - const EResourceBounds: u64 = 2; - const EResourceSize: u64 = 3; - const EWrongEpoch: u64 = 4; - const EAlreadyCertified: u64 = 5; - const EInvalidBlobId: u64 = 6; - const ENotCertified: u64 = 7; - - // Object definitions - - /// The blob structure represents a blob that has been registered to with some storage, - /// and then may eventually be certified as being available in the system. - public struct Blob has key, store { - id: UID, - stored_epoch: u64, - blob_id: u256, - size: u64, - erasure_code_type: u8, - certified_epoch: option::Option, // Store the epoch first certified - storage: Storage, - } - - // Accessor functions - - public fun stored_epoch(b: &Blob): u64 { - b.stored_epoch - } - - public fun blob_id(b: &Blob): u256 { - b.blob_id - } - - public fun size(b: &Blob): u64 { - b.size - } - - public fun erasure_code_type(b: &Blob): u8 { - b.erasure_code_type - } - - public fun certified_epoch(b: &Blob): &Option { - &b.certified_epoch - } - - public fun storage(b: &Blob): &Storage { - &b.storage - } - - public struct BlobIdDerivation has drop { - erasure_code_type: u8, - size: u64, - root_hash: u256, - } - - /// Derive the blob_id for a blob given the root_hash, erasure_code_type and size. - public fun derive_blob_id(root_hash: u256, erasure_code_type: u8, size: u64): u256 { - let blob_id_struct = BlobIdDerivation { - erasure_code_type, - size, - root_hash, - }; - - let serialized = bcs::to_bytes(&blob_id_struct); - let encoded = hash::blake2b256(&serialized); - let mut decoder = bcs::new(encoded); - let blob_id = decoder.peel_u256(); - blob_id - } - - /// Register a new blob in the system. - /// `size` is the size of the unencoded blob. The reserved space in `storage` must be at - /// least the size of the encoded blob. - public fun register( - sys: &System, - storage: Storage, - blob_id: u256, - root_hash: u256, - size: u64, - erasure_code_type: u8, - ctx: &mut TxContext, - ): Blob { - let id = object::new(ctx); - let stored_epoch = sys.epoch(); - - // Check resource bounds. - assert!(stored_epoch >= start_epoch(&storage), EResourceBounds); - assert!(stored_epoch < end_epoch(&storage), EResourceBounds); - - // check that the encoded size is less than the storage size - let encoded_size = encoding::encoded_blob_length( - size, - erasure_code_type, - sys.n_shards(), - ); - assert!(encoded_size <= storage_size(&storage), EResourceSize); - - // Cryptographically verify that the Blob ID authenticates - // both the size and fe_type. - assert!( - derive_blob_id(root_hash, erasure_code_type, size) == blob_id, - EInvalidBlobId, - ); - - // Emit register event - emit_blob_registered( - stored_epoch, - blob_id, - size, - erasure_code_type, - end_epoch(&storage), - ); - - Blob { - id, - stored_epoch, - blob_id, - size, - // - erasure_code_type, - certified_epoch: option::none(), - storage, - } - } - - public struct CertifiedBlobMessage has drop { - epoch: u64, - blob_id: u256, - } - - /// Construct the certified blob message, note that constructing - /// implies a certified message, that is already checked. - public fun certify_blob_message(message: CertifiedMessage): CertifiedBlobMessage { - // Assert type is correct - assert!(message.intent_type() == BLOB_CERT_MSG_TYPE, EInvalidMsgType); - - // The certified blob message contain a blob_id : u256 - let epoch = message.cert_epoch(); - let message_body = message.into_message(); - - let mut bcs_body = bcs::new(message_body); - let blob_id = bcs_body.peel_u256(); - - // On purpose we do not check that nothing is left in the message - // to allow in the future for extensibility. - - CertifiedBlobMessage { epoch, blob_id } - } - - /// Certify that a blob will be available in the storage system until the end epoch of the - /// storage associated with it, given a [`CertifiedBlobMessage`]. - public fun certify_with_certified_msg( - sys: &System, - message: CertifiedBlobMessage, - blob: &mut Blob, - ) { - // Check that the blob is registered in the system - assert!(blob_id(blob) == message.blob_id, EInvalidBlobId); - - // Check that the blob is not already certified - assert!(!blob.certified_epoch.is_some(), EAlreadyCertified); - - // Check that the message is from the current epoch - assert!(message.epoch == sys.epoch(), EWrongEpoch); - - // Check that the storage in the blob is still valid - assert!(message.epoch < end_epoch(storage(blob)), EResourceBounds); - - // Mark the blob as certified - blob.certified_epoch = option::some(message.epoch); - - // Emit certified event - emit_blob_certified( - message.epoch, - message.blob_id, - end_epoch(storage(blob)), - ); - } - - /// Certify that a blob will be available in the storage system until the end epoch of the - /// storage associated with it. - public fun certify( - sys: &System, - blob: &mut Blob, - signature: vector, - members: vector, - message: vector, - ) { - let certified_msg = committee::verify_quorum_in_epoch( - sys.current_committee(), - signature, - members, - message, - ); - let certified_blob_msg = certify_blob_message(certified_msg); - certify_with_certified_msg(sys, certified_blob_msg, blob); - } - - /// After the period of validity expires for the blob we can destroy the blob resource. - public fun destroy_blob(sys: &System, blob: Blob) { - let current_epoch = sys.epoch(); - assert!(current_epoch >= end_epoch(storage(&blob)), EResourceBounds); - - // Destroy the blob - let Blob { - id, - stored_epoch: _, - blob_id: _, - size: _, - erasure_code_type: _, - certified_epoch: _, - storage, - } = blob; - - id.delete(); - destroy(storage); - } - - /// Extend the period of validity of a blob with a new storage resource. - /// The new storage resource must be the same size as the storage resource - /// used in the blob, and have a longer period of validity. - public fun extend(sys: &System, blob: &mut Blob, extension: Storage) { - // We only extend certified blobs within their period of validity - // with storage that extends this period. First we check for these - // conditions. - - // Assert this is a certified blob - assert!(blob.certified_epoch.is_some(), ENotCertified); - - // Check the blob is within its availability period - assert!(sys.epoch() < end_epoch(storage(blob)), EResourceBounds); - - // Check that the extension is valid, and the end - // period of the extension is after the current period. - assert!(end_epoch(&extension) > end_epoch(storage(blob)), EResourceBounds); - - // Note: if the amounts do not match there will be an abort here. - fuse_periods(&mut blob.storage, extension); - - // Emit certified event - // - // Note: We use the original certified period since for the purposes of - // reconfiguration this is the committee that has a quorum that hold the - // resource. - emit_blob_certified( - *option::borrow(&blob.certified_epoch), - blob.blob_id, - end_epoch(storage(blob)), - ); - } - - // Testing Functions - - #[test_only] - public fun drop_for_testing(b: Blob) { - // deconstruct - let Blob { - id, - stored_epoch: _, - blob_id: _, - size: _, - erasure_code_type: _, - certified_epoch: _, - storage, - } = b; - - id.delete(); - destroy(storage); - } - - #[test_only] - // Accessor for blob - public fun message_blob_id(m: &CertifiedBlobMessage): u256 { - m.blob_id - } - - #[test_only] - public fun certified_blob_message_for_testing(epoch: u64, blob_id: u256): CertifiedBlobMessage { - CertifiedBlobMessage { epoch, blob_id } - } -} diff --git a/contracts/blob_store/sources/blob_events.move b/contracts/blob_store/sources/blob_events.move deleted file mode 100644 index 6b87dafb..00000000 --- a/contracts/blob_store/sources/blob_events.move +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -/// Module to emit blob events. Used to allow filtering all blob events in the -/// rust client (as work-around for the lack of composable event filters). -module blob_store::blob_events { - use sui::event; - - // Event definitions - - /// Signals a blob with meta-data is registered. - public struct BlobRegistered has copy, drop { - epoch: u64, - blob_id: u256, - size: u64, - erasure_code_type: u8, - end_epoch: u64, - } - - /// Signals a blob is certified. - public struct BlobCertified has copy, drop { - epoch: u64, - blob_id: u256, - end_epoch: u64, - } - - /// Signals that a BlobID is invalid. - public struct InvalidBlobID has copy, drop { - epoch: u64, // The epoch in which the blob ID is first registered as invalid - blob_id: u256, - } - - public(package) fun emit_blob_registered( - epoch: u64, - blob_id: u256, - size: u64, - erasure_code_type: u8, - end_epoch: u64, - ) { - event::emit(BlobRegistered { epoch, blob_id, size, erasure_code_type, end_epoch }); - } - - public(package) fun emit_blob_certified(epoch: u64, blob_id: u256, end_epoch: u64) { - event::emit(BlobCertified { epoch, blob_id, end_epoch }); - } - - public(package) fun emit_invalid_blob_id(epoch: u64, blob_id: u256) { - event::emit(InvalidBlobID { epoch, blob_id }); - } -} diff --git a/contracts/blob_store/sources/bls_aggregate.move b/contracts/blob_store/sources/bls_aggregate.move deleted file mode 100644 index 8568dcc6..00000000 --- a/contracts/blob_store/sources/bls_aggregate.move +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -// editorconfig-checker-disable-file - -module blob_store::bls_aggregate { - use sui::group_ops::Self; - use sui::bls12381::{Self, bls12381_min_pk_verify}; - - use blob_store::storage_node::StorageNodeInfo; - - // Error codes - const ETotalMemberOrder: u64 = 0; - const ESigVerification: u64 = 1; - const ENotEnoughStake: u64 = 2; - const EIncorrectCommittee: u64 = 3; - - /// This represents a BLS signing committee. - public struct BlsCommittee has store, drop { - /// A vector of committee members - members: vector, - /// The total number of shards held by the committee - n_shards: u16, - } - - /// Constructor - public fun new_bls_committee(members: vector): BlsCommittee { - // Compute the total number of shards - let mut n_shards = 0; - let mut i = 0; - while (i < members.length()) { - let added_weight = members[i].weight(); - assert!(added_weight > 0, EIncorrectCommittee); - n_shards = n_shards + added_weight; - i = i + 1; - }; - assert!(n_shards != 0, EIncorrectCommittee); - - BlsCommittee { members, n_shards } - } - - /// Returns the number of shards held by the committee. - public fun n_shards(self: &BlsCommittee): u16 { - self.n_shards - } - - /// Verify an aggregate BLS signature is a certificate in the epoch, and return the type of - /// certificate and the bytes certified. The `signers` vector is an increasing list of indexes - /// into the `members` vector of the committee. If there is a certificate, the function - /// returns the total stake. Otherwise, it aborts. - public fun verify_certificate( - self: &BlsCommittee, - signature: &vector, - signers: &vector, - message: &vector, - ): u16 { - // Use the signers flags to construct the key and the weights. - - // Lower bound for the next `member_index` to ensure they are monotonically increasing - let mut min_next_member_index = 0; - let mut i = 0; - - let mut aggregate_key = bls12381::g1_identity(); - let mut aggregate_weight = 0; - - while (i < signers.length()) { - let member_index = signers[i] as u64; - assert!(member_index >= min_next_member_index, ETotalMemberOrder); - min_next_member_index = member_index + 1; - - // Bounds check happens here - let member = &self.members[member_index]; - let key = member.public_key(); - let weight = member.weight(); - - aggregate_key = bls12381::g1_add(&aggregate_key, key); - aggregate_weight = aggregate_weight + weight; - - i = i + 1; - }; - - // The expression below is the solution to the inequality: - // n_shards = 3 f + 1 - // stake >= 2f + 1 - assert!( - 3 * (aggregate_weight as u64) >= 2 * (self.n_shards as u64) + 1, - ENotEnoughStake, - ); - - // Verify the signature - let pub_key_bytes = group_ops::bytes(&aggregate_key); - assert!( - bls12381_min_pk_verify( - signature, - pub_key_bytes, - message, - ), - ESigVerification, - ); - - (aggregate_weight as u16) - } - - - #[test_only] - use blob_store::storage_node::Self; - - #[test_only] - /// Test committee - public fun new_bls_committee_for_testing(): BlsCommittee { - // Pk corresponding to secret key scalar(117) - let pub_key_bytes = x"95eacc3adc09c827593f581e8e2de068bf4cf5d0c0eb29e5372f0d23364788ee0f9beb112c8a7e9c2f0c720433705cf0"; - let storage_node = storage_node::new_for_testing(pub_key_bytes, 100); - BlsCommittee { members: vector[storage_node], n_shards: 100 } - } -} diff --git a/contracts/blob_store/sources/committee.move b/contracts/blob_store/sources/committee.move deleted file mode 100644 index 04116952..00000000 --- a/contracts/blob_store/sources/committee.move +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -module blob_store::committee { - use sui::bcs; - - const APP_ID: u8 = 3; - - // Errors - const EIncorrectAppId: u64 = 0; - const EIncorrectEpoch: u64 = 1; - - #[test_only] - use blob_store::bls_aggregate::new_bls_committee_for_testing; - - use blob_store::bls_aggregate::{Self, BlsCommittee, new_bls_committee, verify_certificate}; - use blob_store::storage_node::StorageNodeInfo; - - /// Represents a committee for a given epoch - /// - /// The construction of a committee for a type is a controlled operation - /// and signifies that the committee is valid for the given epoch. It has - /// no drop since valid committees must be stored for ever. And no copy - /// since each epoch must only have one committee. Finally, no key since - /// It must never be stored outside controlled places. - /// - /// The above restrictions allow us to implement a separation between committee - /// formation and the actual System object. One structure - /// can take care of the epoch management including the committee formation, and - /// the System object can simply receive a committee of the correct type as a - /// signal that the new epoch has started. - public struct Committee has store { - epoch: u64, - bls_committee: BlsCommittee, - } - - /// Get the epoch of the committee. - public fun epoch(self: &Committee): u64 { - self.epoch - } - - /// A capability that allows the creation of committees - public struct CreateCommitteeCap has copy, store, drop {} - - /// A constructor for the capability to create committees - /// This is only accessible through friend modules. - public(package) fun create_committee_cap(): CreateCommitteeCap { - CreateCommitteeCap {} - } - - /// Returns the number of shards held by the committee. - public fun n_shards(self: &Committee): u16 { - bls_aggregate::n_shards(&self.bls_committee) - } - - #[test_only] - /// A constructor for the capability to create committees for tests - public fun create_committee_cap_for_tests(): CreateCommitteeCap { - CreateCommitteeCap {} - } - - /// Creating a committee for a given epoch. - /// Requires a capability - public fun create_committee( - _cap: &CreateCommitteeCap, - epoch: u64, - members: vector, - ): Committee { - // Make BlsCommittee - let bls_committee = new_bls_committee(members); - - Committee { epoch, bls_committee } - } - - #[test_only] - public fun committee_for_testing(epoch: u64): Committee { - let bls_committee = new_bls_committee_for_testing(); - Committee { epoch, bls_committee } - } - - #[test_only] - public fun committee_for_testing_with_bls(epoch: u64, bls_committee: BlsCommittee): Committee { - Committee { epoch, bls_committee } - } - - public struct CertifiedMessage has drop { - intent_type: u8, - intent_version: u8, - cert_epoch: u64, - stake_support: u16, - message: vector, - } - - #[test_only] - public fun certified_message_for_testing( - intent_type: u8, - intent_version: u8, - cert_epoch: u64, - stake_support: u16, - message: vector, - ): CertifiedMessage { - CertifiedMessage { intent_type, intent_version, cert_epoch, stake_support, message } - } - - // Make accessors for the CertifiedMessage - public fun intent_type(self: &CertifiedMessage): u8 { - self.intent_type - } - - public fun intent_version(self: &CertifiedMessage): u8 { - self.intent_version - } - - public fun cert_epoch(self: &CertifiedMessage): u64 { - self.cert_epoch - } - - public fun stake_support(self: &CertifiedMessage): u16 { - self.stake_support - } - - public fun message(self: &CertifiedMessage): &vector { - &self.message - } - - // Deconstruct into the vector of message bytes - public fun into_message(self: CertifiedMessage): vector { - self.message - } - - /// Verifies that a message is signed by a quorum of the members of a committee. - /// - /// The members are listed in increasing order and with no repetitions. And the signatures - /// match the order of the members. The total stake is returned, but if a quorum is not reached - /// the function aborts with an error. - public fun verify_quorum_in_epoch( - committee: &Committee, - signature: vector, - members: vector, - message: vector, - ): CertifiedMessage { - let stake_support = verify_certificate( - &committee.bls_committee, - &signature, - &members, - &message, - ); - - // Here we BCS decode the header of the message to check intents, epochs, etc. - - let mut bcs_message = bcs::new(message); - let intent_type = bcs_message.peel_u8(); - let intent_version = bcs_message.peel_u8(); - - let intent_app = bcs_message.peel_u8(); - assert!(intent_app == APP_ID, EIncorrectAppId); - - let cert_epoch = bcs_message.peel_u64(); - assert!(cert_epoch == epoch(committee), EIncorrectEpoch); - - let message = bcs_message.into_remainder_bytes(); - - CertifiedMessage { intent_type, intent_version, cert_epoch, stake_support, message } - } -} diff --git a/contracts/blob_store/sources/e2etest.move b/contracts/blob_store/sources/e2etest.move deleted file mode 100644 index e42c6cd7..00000000 --- a/contracts/blob_store/sources/e2etest.move +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -module blob_store::e2e_test { - use blob_store::committee::{Self, CreateCommitteeCap}; - use blob_store::storage_node; - - public struct CommitteeCapHolder has key, store { - id: UID, - cap: CreateCommitteeCap, - } - - // NOTE: the function below is means to be used as part of a PTB to construct a committee - // The PTB contains a number of `create_storage_node_info` invocations, then - // a `MakeMoveVec` invocation, and finally a `make_committee` invocation. - - /// Create a committee given a capability and a list of storage nodes - public fun make_committee( - cap: &CommitteeCapHolder, - epoch: u64, - storage_nodes: vector, - ): committee::Committee { - committee::create_committee( - &cap.cap, - epoch, - storage_nodes, - ) - } - - fun init(ctx: &mut TxContext) { - // Create a committee caps - let committee_cap = committee::create_committee_cap(); - - // We send the wrapped cap to the creator of the package - transfer::public_transfer( - CommitteeCapHolder { id: object::new(ctx), cap: committee_cap }, - ctx.sender(), - ); - } -} diff --git a/contracts/blob_store/sources/encoding.move b/contracts/blob_store/sources/encoding.move deleted file mode 100644 index 7b67c375..00000000 --- a/contracts/blob_store/sources/encoding.move +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -module blob_store::encoding { - use blob_store::redstuff; - - // Supported Encoding Types - const RED_STUFF_ENCODING: u8 = 0; - - // Errors - const EInvalidEncoding: u64 = 0; - - /// Computes the encoded length of a blob given its unencoded length, encoding type - /// and number of shards `n_shards`. - public fun encoded_blob_length(unencoded_length: u64, encoding_type: u8, n_shards: u16): u64 { - // Currently only supports a single encoding type - assert!(encoding_type == RED_STUFF_ENCODING, EInvalidEncoding); - redstuff::encoded_blob_length(unencoded_length, n_shards) - } -} diff --git a/contracts/blob_store/sources/redstuff.move b/contracts/blob_store/sources/redstuff.move deleted file mode 100644 index ca0208ca..00000000 --- a/contracts/blob_store/sources/redstuff.move +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -module blob_store::redstuff { - // The length of a hash used for the Red Stuff metadata - const DIGEST_LEN: u64 = 32; - - // The length of a blob id in the stored metadata - const BLOB_ID_LEN: u64 = 32; - - /// Computes the encoded length of a blob for the Red Stuff encoding, given its - /// unencoded size and the number of shards. The output length includes the - /// size of the metadata hashes and the blob ID. - public(package) fun encoded_blob_length(unencoded_length: u64, n_shards: u16): u64 { - let slivers_size = (source_symbols_primary(n_shards) as u64 - + (source_symbols_secondary(n_shards) as u64)) - * (symbol_size(unencoded_length, n_shards) as u64); - - (n_shards as u64) * (slivers_size + metadata_size(n_shards)) - } - - /// The number of primary source symbols per sliver given `n_shards`. - fun source_symbols_primary(n_shards: u16): u16 { - n_shards - max_byzantine(n_shards) - decoding_safety_limit(n_shards) - } - - /// The number of secondary source symbols per sliver given `n_shards`. - fun source_symbols_secondary(n_shards: u16): u16 { - n_shards - 2 * max_byzantine(n_shards) - decoding_safety_limit(n_shards) - } - - /// The total number of source symbols given `n_shards`. - fun n_source_symbols(n_shards: u16): u64 { - (source_symbols_primary(n_shards) as u64) * (source_symbols_secondary(n_shards) as u64) - } - - /// Computes the symbol size given the `unencoded_length` and number of shards - /// `n_shards`. If the resulting symbols would be larger than a `u16`, this - /// results in an Error. - fun symbol_size(mut unencoded_length: u64, n_shards: u16): u16 { - if (unencoded_length == 0) { - unencoded_length = 1; - }; - let n_symbols = n_source_symbols(n_shards); - ((unencoded_length - 1) / n_symbols + 1) as u16 - } - - /// The size of the metadata, i.e. sliver root hashes and blob_id. - fun metadata_size(n_shards: u16): u64 { - (n_shards as u64) * DIGEST_LEN * 2 + BLOB_ID_LEN - } - - /// Returns the decoding safety limit. - fun decoding_safety_limit(n_shards: u16): u16 { - // These ranges are chosen to ensure that the safety limit is at most 20% of f, - // up to a safety limit of 5. - min_u16(max_byzantine(n_shards) / 5, 5) - } - - /// Maximum number of byzantine shards, given `n_shards`. - fun max_byzantine(n_shards: u16): u16 { - (n_shards - 1) / 3 - } - - fun min_u16(a: u16, b: u16): u16 { - if (a < b) { - a - } else { - b - } - } - - // Tests - - #[test_only] - fun assert_encoded_size(unencoded_length: u64, n_shards: u16, encoded_size: u64) { - assert!(encoded_blob_length(unencoded_length, n_shards) == encoded_size, 0); - } - - #[test] - fun test_encoded_size() { - assert_encoded_size(1, 10, 10 * ((4 + 7) + 10 * 2 * 32 + 32)); - assert_encoded_size(1, 1000, 1000 * ((329 + 662) + 1000 * 2 * 32 + 32)); - assert_encoded_size((4 * 7) * 100, 10, 10 * ((4 + 7) * 100 + 10 * 2 * 32 + 32)); - assert_encoded_size( - (329 * 662) * 100, - 1000, - 1000 * ((329 + 662) * 100 + 1000 * 2 * 32 + 32), - ); - } - - #[test] - fun test_zero_size() { - //test should fail here - encoded_blob_length(0, 10); - } - - #[test,expected_failure] - fun test_symbol_too_large() { - let n_shards = 100; - // Create an unencoded length for which each symbol must be larger than the maximum size - let unencoded_length = (0xffff + 1) * n_source_symbols(n_shards); - // Test should fail here - let _ = symbol_size(unencoded_length, n_shards); - } -} diff --git a/contracts/blob_store/sources/storage_accounting.move b/contracts/blob_store/sources/storage_accounting.move deleted file mode 100644 index f971f1bf..00000000 --- a/contracts/blob_store/sources/storage_accounting.move +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -module blob_store::storage_accounting { - use sui::balance::{Self, Balance}; - - // Errors - const EIndexOutOfBounds: u64 = 3; - - /// Holds information about a future epoch, namely how much - /// storage needs to be reclaimed and the rewards to be distributed. - public struct FutureAccounting has store { - epoch: u64, - storage_to_reclaim: u64, - rewards_to_distribute: Balance, - } - - /// Constructor for FutureAccounting - public fun new_future_accounting( - epoch: u64, - storage_to_reclaim: u64, - rewards_to_distribute: Balance, - ): FutureAccounting { - FutureAccounting { epoch, storage_to_reclaim, rewards_to_distribute } - } - - /// Accessor for epoch, read-only - public fun epoch(accounting: &FutureAccounting): u64 { - *&accounting.epoch - } - - /// Accessor for storage_to_reclaim, mutable. - public fun storage_to_reclaim(accounting: &mut FutureAccounting): u64 { - accounting.storage_to_reclaim - } - - /// Increase storage to reclaim - public fun increase_storage_to_reclaim( - accounting: &mut FutureAccounting, - amount: u64, - ) { - accounting.storage_to_reclaim = accounting.storage_to_reclaim + amount; - } - - /// Accessor for rewards_to_distribute, mutable. - public fun rewards_to_distribute( - accounting: &mut FutureAccounting, - ): &mut Balance { - &mut accounting.rewards_to_distribute - } - - /// Destructor for FutureAccounting, when empty. - public fun delete_empty_future_accounting(self: FutureAccounting) { - let FutureAccounting { - epoch: _, - storage_to_reclaim: _, - rewards_to_distribute, - } = self; - - rewards_to_distribute.destroy_zero() - } - - #[test_only] - public fun burn_for_testing(self: FutureAccounting) { - let FutureAccounting { - epoch: _, - storage_to_reclaim: _, - rewards_to_distribute, - } = self; - - rewards_to_distribute.destroy_for_testing(); - } - - /// A ring buffer holding future accounts for a continuous range of epochs. - public struct FutureAccountingRingBuffer has store { - current_index: u64, - length: u64, - ring_buffer: vector>, - } - - /// Constructor for FutureAccountingRingBuffer - public fun ring_new(length: u64): FutureAccountingRingBuffer { - let mut ring_buffer: vector> = vector::empty(); - let mut i = 0; - while (i < length) { - ring_buffer.push_back(FutureAccounting { - epoch: i, - storage_to_reclaim: 0, - rewards_to_distribute: balance::zero(), - }); - i = i + 1; - }; - - FutureAccountingRingBuffer { current_index: 0, length: length, ring_buffer: ring_buffer } - } - - /// Lookup an entry a number of epochs in the future. - public fun ring_lookup_mut( - self: &mut FutureAccountingRingBuffer, - epochs_in_future: u64, - ): &mut FutureAccounting { - // Check for out-of-bounds access. - assert!(epochs_in_future < self.length, EIndexOutOfBounds); - - let actual_index = (epochs_in_future + self.current_index) % self.length; - &mut self.ring_buffer[actual_index] - } - - public fun ring_pop_expand( - self: &mut FutureAccountingRingBuffer, - ): FutureAccounting { - // Get current epoch - let current_index = self.current_index; - let current_epoch = self.ring_buffer[current_index].epoch; - - // Expand the ring buffer - self - .ring_buffer - .push_back(FutureAccounting { - epoch: current_epoch + self.length, - storage_to_reclaim: 0, - rewards_to_distribute: balance::zero(), - }); - - // Now swap remove the current element and increment the current_index - let accounting = self.ring_buffer.swap_remove(current_index); - self.current_index = (current_index + 1) % self.length; - - accounting - } -} diff --git a/contracts/blob_store/sources/storage_node.move b/contracts/blob_store/sources/storage_node.move deleted file mode 100644 index bbd6b9f1..00000000 --- a/contracts/blob_store/sources/storage_node.move +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -module blob_store::storage_node { - use std::string::String; - use sui::group_ops::Element; - use sui::bls12381::{G1, g1_from_bytes}; - - // Error codes - const EInvalidNetworkPublicKey: u64 = 1; - - /// Represents a storage node and its meta-data. - /// - /// Creation and deletion of storage node info is an - /// uncontrolled operation, but it lacks key so cannot - /// be stored outside the context of another object. - public struct StorageNodeInfo has store, drop { - name: String, - network_address: String, - public_key: Element, - network_public_key: vector, - shard_ids: vector, - } - - /// A public constructor for the StorageNodeInfo. - public fun create_storage_node_info( - name: String, - network_address: String, - public_key: vector, - network_public_key: vector, - shard_ids: vector, - ): StorageNodeInfo { - assert!(network_public_key.length() == 32, EInvalidNetworkPublicKey); - StorageNodeInfo { - name, - network_address, - public_key: g1_from_bytes(&public_key), - network_public_key, - shard_ids - } - } - - public fun public_key(self: &StorageNodeInfo): &Element { - &self.public_key - } - - public fun network_public_key(self: &StorageNodeInfo): &vector { - &self.network_public_key - } - - public fun shard_ids(self: &StorageNodeInfo): &vector { - &self.shard_ids - } - - public fun weight(self: &StorageNodeInfo): u16 { - self.shard_ids.length() as u16 - } - - #[test_only] - /// Create a storage node with dummy name & address - public fun new_for_testing(public_key: vector, weight: u16): StorageNodeInfo { - let mut i: u16 = 0; - let mut shard_ids = vector[]; - while (i < weight) { - shard_ids.push_back(i); - i = i + 1; - }; - StorageNodeInfo { - name: b"node".to_string(), - network_address: b"127.0.0.1".to_string(), - public_key: g1_from_bytes(&public_key), - network_public_key: x"820e2b273530a00de66c9727c40f48be985da684286983f398ef7695b8a44677", - shard_ids, - } - } -} diff --git a/contracts/blob_store/sources/storage_resource.move b/contracts/blob_store/sources/storage_resource.move deleted file mode 100644 index 7e7fc1a0..00000000 --- a/contracts/blob_store/sources/storage_resource.move +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -module blob_store::storage_resource { - const EInvalidEpoch: u64 = 0; - const EIncompatibleEpochs: u64 = 1; - const EIncompatibleAmount: u64 = 2; - - /// Reservation for storage for a given period, which is inclusive start, exclusive end. - public struct Storage has key, store { - id: UID, - start_epoch: u64, - end_epoch: u64, - storage_size: u64, - } - - public fun start_epoch(self: &Storage): u64 { - self.start_epoch - } - - public fun end_epoch(self: &Storage): u64 { - self.end_epoch - } - - public fun storage_size(self: &Storage): u64 { - self.storage_size - } - - /// Constructor for [Storage] objects. - /// Necessary to allow `blob_store::system` to create storage objects. - /// Cannot be called outside of the current module and [blob_store::system]. - public(package) fun create_storage( - start_epoch: u64, - end_epoch: u64, - storage_size: u64, - ctx: &mut TxContext, - ): Storage { - Storage { id: object::new(ctx), start_epoch, end_epoch, storage_size } - } - - /// Split the storage object into two based on `split_epoch` - /// - /// `storage` is modified to cover the period from `start_epoch` to `split_epoch` - /// and a new storage object covering `split_epoch` to `end_epoch` is returned. - public fun split_by_epoch( - storage: &mut Storage, - split_epoch: u64, - ctx: &mut TxContext, - ): Storage { - assert!( - split_epoch >= storage.start_epoch && split_epoch <= storage.end_epoch, - EInvalidEpoch, - ); - let end_epoch = storage.end_epoch; - storage.end_epoch = split_epoch; - Storage { - id: object::new(ctx), - start_epoch: split_epoch, - end_epoch, - storage_size: storage.storage_size, - } - } - - /// Split the storage object into two based on `split_size` - /// - /// `storage` is modified to cover `split_size` and a new object covering - /// `storage.storage_size - split_size` is created. - public fun split_by_size(storage: &mut Storage, split_size: u64, ctx: &mut TxContext): Storage { - let storage_size = storage.storage_size - split_size; - storage.storage_size = split_size; - Storage { - id: object::new(ctx), - start_epoch: storage.start_epoch, - end_epoch: storage.end_epoch, - storage_size, - } - } - - /// Fuse two storage objects that cover adjacent periods with the same storage size. - public fun fuse_periods(first: &mut Storage, second: Storage) { - let Storage { - id, - start_epoch: second_start, - end_epoch: second_end, - storage_size: second_size, - } = second; - id.delete(); - assert!(first.storage_size == second_size, EIncompatibleAmount); - if (first.end_epoch == second_start) { - first.end_epoch = second_end; - } else { - assert!(first.start_epoch == second_end, EIncompatibleEpochs); - first.start_epoch = second_start; - } - } - - /// Fuse two storage objects that cover the same period - public fun fuse_amount(first: &mut Storage, second: Storage) { - let Storage { - id, - start_epoch: second_start, - end_epoch: second_end, - storage_size: second_size, - } = second; - id.delete(); - assert!( - first.start_epoch == second_start && first.end_epoch == second_end, - EIncompatibleEpochs, - ); - first.storage_size = first.storage_size + second_size; - } - - /// Fuse two storage objects that either cover the same period - /// or adjacent periods with the same storage size. - public fun fuse(first: &mut Storage, second: Storage) { - if (first.start_epoch == second.start_epoch) { - // Fuse by storage_size - fuse_amount(first, second); - } else { - // Fuse by period - fuse_periods(first, second); - } - } - - #[test_only] - /// Constructor for [Storage] objects for tests - public fun create_for_test( - start_epoch: u64, - end_epoch: u64, - storage_size: u64, - ctx: &mut TxContext, - ): Storage { - Storage { id: object::new(ctx), start_epoch, end_epoch, storage_size } - } - - /// Destructor for [Storage] objects - public fun destroy(storage: Storage) { - let Storage { - id, - start_epoch: _, - end_epoch: _, - storage_size: _, - } = storage; - id.delete(); - } -} diff --git a/contracts/blob_store/sources/system.move b/contracts/blob_store/sources/system.move deleted file mode 100644 index 50927f89..00000000 --- a/contracts/blob_store/sources/system.move +++ /dev/null @@ -1,370 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -module blob_store::system { - use sui::coin::Coin; - use sui::event; - use sui::table::{Self, Table}; - use sui::bcs; - - use blob_store::committee::{Self, Committee}; - use blob_store::storage_accounting::{Self, FutureAccounting, FutureAccountingRingBuffer}; - use blob_store::storage_resource::{Self, Storage}; - use blob_store::blob_events::emit_invalid_blob_id; - - // Errors - const EIncorrectCommittee: u64 = 0; - const ESyncEpochChange: u64 = 1; - const EInvalidPeriodsAhead: u64 = 2; - const EStorageExceeded: u64 = 3; - const EInvalidMsgType: u64 = 4; - const EInvalidIdEpoch: u64 = 5; - - // Message types: - const EPOCH_DONE_MSG_TYPE: u8 = 0; - const INVALID_BLOB_ID_MSG_TYPE: u8 = 2; - - // Epoch status values - #[allow(unused_const)] - const EPOCH_STATUS_DONE: u8 = 0; - #[allow(unused_const)] - const EPOCH_STATUS_SYNC: u8 = 1; - - /// The maximum number of periods ahead we allow for storage reservations. - /// This number is a placeholder, and assumes an epoch is a week, - /// and therefore 2 x 52 weeks = 2 years. - const MAX_PERIODS_AHEAD: u64 = 104; - - // Keep in sync with the same constant in `crates/walrus-sui/utils.rs`. - const BYTES_PER_UNIT_SIZE: u64 = 1_024; - - // Event types - - /// Signals an epoch change, and entering the SYNC state for the new epoch. - public struct EpochChangeSync has copy, drop { - epoch: u64, - total_capacity_size: u64, - used_capacity_size: u64, - } - - /// Signals that the epoch change is DONE now. - public struct EpochChangeDone has copy, drop { - epoch: u64, - } - - // Object definitions - - #[allow(unused_field)] - public struct System has key, store { - id: UID, - /// The current committee, with the current epoch. - /// The option is always Some, but need it for swap. - current_committee: Option, - /// When we first enter the current epoch we SYNC, - /// and then we are DONE after a cert from a quorum. - epoch_status: u8, - // Some accounting - total_capacity_size: u64, - used_capacity_size: u64, - /// The price per unit size of storage. - price_per_unit_size: u64, - /// Tables about the future and the past. - past_committees: Table, - future_accounting: FutureAccountingRingBuffer, - } - - /// Get epoch. Uses the committee to get the epoch. - public fun epoch(self: &System): u64 { - committee::epoch(option::borrow(&self.current_committee)) - } - - /// Accessor for total capacity size. - public fun total_capacity_size(self: &System): u64 { - self.total_capacity_size - } - - /// Accessor for used capacity size. - public fun used_capacity_size(self: &System): u64 { - self.used_capacity_size - } - - /// A privileged constructor for an initial system object, - /// at epoch 0 with a given committee, and a given - /// capacity and price. Here ownership of a committee at time 0 - /// acts as a capability to create a init a new system object. - public fun new( - first_committee: Committee, - capacity: u64, - price: u64, - ctx: &mut TxContext, - ): System { - assert!(first_committee.epoch() == 0, EIncorrectCommittee); - - // We emit both sync and done events for the first epoch. - event::emit(EpochChangeSync { - epoch: 0, - total_capacity_size: capacity, - used_capacity_size: 0, - }); - event::emit(EpochChangeDone { epoch: 0 }); - - System { - id: object::new(ctx), - current_committee: option::some(first_committee), - epoch_status: EPOCH_STATUS_DONE, - total_capacity_size: capacity, - used_capacity_size: 0, - price_per_unit_size: price, - past_committees: table::new(ctx), - future_accounting: storage_accounting::ring_new(MAX_PERIODS_AHEAD), - } - } - - // We actually create a new objects that does not exist before, so all is good. - #[allow(lint(share_owned))] - /// Create and share a new system object, using ownership of a committee - /// at epoch 0 as a capability to create a new system object. - public fun share_new( - first_committee: Committee, - capacity: u64, - price: u64, - ctx: &mut TxContext, - ) { - let sys: System = new(first_committee, capacity, price, ctx); - transfer::share_object(sys); - } - - /// An accessor for the current committee. - public fun current_committee(self: &System): &Committee { - self.current_committee.borrow() - } - - public fun n_shards(self: &System): u16 { - current_committee(self).n_shards() - } - - /// Update epoch to next epoch, and also update the committee, price and capacity. - public fun next_epoch( - self: &mut System, - new_committee: Committee, - new_capacity: u64, - new_price: u64, - ): FutureAccounting { - // Must be in DONE state to move epochs. This is the way. - assert!(self.epoch_status == EPOCH_STATUS_DONE, ESyncEpochChange); - - // Check new committee is valid, the existence of a committee for the next epoch - // is proof that the time has come to move epochs. - let old_epoch = epoch(self); - let new_epoch = old_epoch + 1; - assert!(new_committee.epoch() == new_epoch, EIncorrectCommittee); - let old_committee = self.current_committee.swap(new_committee); - - // Add the old committee to the past_committees table. - self.past_committees.add(old_epoch, old_committee); - - // Update the system object. - self.total_capacity_size = new_capacity; - self.price_per_unit_size = new_price; - self.epoch_status = EPOCH_STATUS_SYNC; - - let mut accounts_old_epoch = self.future_accounting.ring_pop_expand(); - assert!(accounts_old_epoch.epoch() == old_epoch, ESyncEpochChange); - - // Update storage based on the accounts data. - self.used_capacity_size = self.used_capacity_size - accounts_old_epoch.storage_to_reclaim(); - - // Emit Sync event. - event::emit(EpochChangeSync { - epoch: new_epoch, - total_capacity_size: self.total_capacity_size, - used_capacity_size: self.used_capacity_size, - }); - - accounts_old_epoch - } - - /// Allow buying a storage reservation for a given period of epochs. - public fun reserve_space( - self: &mut System, - storage_amount: u64, - periods_ahead: u64, - mut payment: Coin, - ctx: &mut TxContext, - ): (Storage, Coin) { - // Check the period is within the allowed range. - assert!(periods_ahead > 0, EInvalidPeriodsAhead); - assert!(periods_ahead <= MAX_PERIODS_AHEAD, EInvalidPeriodsAhead); - - // Check capacity is available. - assert!( - self.used_capacity_size + storage_amount <= self.total_capacity_size, - EStorageExceeded, - ); - - // Pay rewards for each future epoch into the future accounting. - let storage_units = (storage_amount + BYTES_PER_UNIT_SIZE - 1) / BYTES_PER_UNIT_SIZE; - let period_payment_due = self.price_per_unit_size * storage_units; - let coin_balance = payment.balance_mut(); - - let mut i = 0; - while (i < periods_ahead) { - let accounts = self.future_accounting.ring_lookup_mut(i); - - // Distribute rewards - let rewards_balance = accounts.rewards_to_distribute(); - // Note this will abort if the balance is not enough. - let epoch_payment = coin_balance.split(period_payment_due); - rewards_balance.join(epoch_payment); - - i = i + 1; - }; - - // Update the storage accounting. - self.used_capacity_size = self.used_capacity_size + storage_amount; - - // Account the space to reclaim in the future. - let final_account = self.future_accounting.ring_lookup_mut(periods_ahead - 1); - final_account.increase_storage_to_reclaim(storage_amount); - - let self_epoch = epoch(self); - ( - storage_resource::create_storage( - self_epoch, - self_epoch + periods_ahead, - storage_amount, - ctx, - ), - payment, - ) - } - - #[test_only] - public fun set_done_for_testing(self: &mut System) { - self.epoch_status = EPOCH_STATUS_DONE; - } - - // The logic to move epoch from SYNC to DONE. - - /// Define a message type for the SyncDone message. - /// It may only be constructed when a valid certified message is - /// passed in. - public struct CertifiedSyncDone has drop { - epoch: u64, - } - - /// Construct the certified sync done message, note that constructing - /// implies a certified message, that is already checked. - public fun certify_sync_done_message(message: committee::CertifiedMessage): CertifiedSyncDone { - // Assert type is correct - assert!(message.intent_type() == EPOCH_DONE_MSG_TYPE, EInvalidMsgType); - - // The SyncDone message has no payload besides the epoch. - // Which happens to already be parsed in the header of the - // certified message. - - CertifiedSyncDone { epoch: message.cert_epoch() } - } - - // make a test only certified message. - #[test_only] - public fun make_sync_done_message_for_testing(epoch: u64): CertifiedSyncDone { - CertifiedSyncDone { epoch } - } - - /// Use the certified message to advance the epoch status to DONE. - public fun sync_done_for_epoch(system: &mut System, message: CertifiedSyncDone) { - // Assert the epoch is correct. - assert!(message.epoch == epoch(system), ESyncEpochChange); - - // Assert we are in the sync state. - assert!(system.epoch_status == EPOCH_STATUS_SYNC, ESyncEpochChange); - - // Move to done state. - system.epoch_status = EPOCH_STATUS_DONE; - - event::emit(EpochChangeDone { epoch: message.epoch }); - } - - // The logic to register an invalid Blob ID - - /// Define a message type for the InvalidBlobID message. - /// It may only be constructed when a valid certified message is - /// passed in. - public struct CertifiedInvalidBlobID has drop { - epoch: u64, - blob_id: u256, - } - - // read the blob id - public fun invalid_blob_id(self: &CertifiedInvalidBlobID): u256 { - self.blob_id - } - - /// Construct the certified invalid Blob ID message, note that constructing - /// implies a certified message, that is already checked. - public fun invalid_blob_id_message( - message: committee::CertifiedMessage, - ): CertifiedInvalidBlobID { - // Assert type is correct - assert!( - message.intent_type() == INVALID_BLOB_ID_MSG_TYPE, - EInvalidMsgType, - ); - - // The InvalidBlobID message has no payload besides the blob_id. - // The certified blob message contain a blob_id : u256 - let epoch = message.cert_epoch(); - let message_body = message.into_message(); - - let mut bcs_body = bcs::new(message_body); - let blob_id = bcs_body.peel_u256(); - - // This output is provided as a service in case anything else needs to rely on - // certified invalid blob ID information in the future. But out base design only - // uses the event emitted here. - CertifiedInvalidBlobID { epoch, blob_id } - } - - /// Private System call to process invalid blob id message. This checks that the epoch - /// in which the message was certified is correct, before emitting an event. Correct - /// nodes will only certify invalid blob ids within their period of validity, and this - /// endures we are not flooded with invalid events from past epochs. - public(package) fun inner_declare_invalid_blob_id( - system: &System, - message: CertifiedInvalidBlobID, - ) { - // Assert the epoch is correct. - let epoch = message.epoch; - assert!(epoch == epoch(system), EInvalidIdEpoch); - - // Emit the event about a blob id being invalid here. - emit_invalid_blob_id( - epoch, - message.blob_id, - ); - } - - /// Public system call to process invalid blob id message. Will check the - /// the certificate in the current committee and ensure that the epoch is - /// correct as well. - public fun invalidate_blob_id( - system: &System, - signature: vector, - members: vector, - message: vector, - ): u256 { - let committee = system.current_committee.borrow(); - - let certified_message = committee.verify_quorum_in_epoch( - signature, - members, - message, - ); - - let invalid_blob_message = invalid_blob_id_message(certified_message); - let blob_id = invalid_blob_message.blob_id; - inner_declare_invalid_blob_id(system, invalid_blob_message); - blob_id - } -} diff --git a/contracts/blob_store/sources/tests/blob_tests.move b/contracts/blob_store/sources/tests/blob_tests.move deleted file mode 100644 index 75a834d4..00000000 --- a/contracts/blob_store/sources/tests/blob_tests.move +++ /dev/null @@ -1,647 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -#[test_only] -module blob_store::blob_tests { - use sui::coin; - use sui::bcs; - - use std::string; - - use blob_store::committee; - use blob_store::system; - use blob_store::storage_accounting as sa; - use blob_store::blob; - use blob_store::storage_node; - - use blob_store::storage_resource::{split_by_epoch, destroy}; - - const RED_STUFF: u8 = 0; - const NETWORK_PUBLIC_KEY: vector = - x"820e2b273530a00de66c9727c40f48be985da684286983f398ef7695b8a44677"; - - public struct TESTWAL has store, drop {} - - #[test] - public fun test_blob_register_happy_path(): system::System { - let mut ctx = tx_context::dummy(); - - // A test coin. - let fake_coin = coin::mint_for_testing(100000000, &mut ctx); - - // Create a new committee - let committee = committee::committee_for_testing(0); - - // Create a new system object - let mut system: system::System = system::new(committee, 1000000000, 5, &mut ctx); - - // Get some space for a few epochs - let (storage, fake_coin) = system::reserve_space( - &mut system, - 1_000_000, - 3, - fake_coin, - &mut ctx, - ); - - // Register a Blob - let blob_id = blob::derive_blob_id(0xABC, RED_STUFF, 5000); - let blob1 = blob::register(&system, storage, blob_id, 0xABC, 5000, RED_STUFF, &mut ctx); - - coin::burn_for_testing(fake_coin); - blob::drop_for_testing(blob1); - system - } - - #[test, expected_failure(abort_code=blob::EResourceSize)] - public fun test_blob_insufficient_space(): system::System { - let mut ctx = tx_context::dummy(); - - // A test coin. - let fake_coin = coin::mint_for_testing(100000000, &mut ctx); - - // Create a new committee - let committee = committee::committee_for_testing(0); - - // Create a new system object - let mut system: system::System = system::new(committee, 1000000000, 5, &mut ctx); - - // Get some space for a few epochs - TOO LITTLE SPACE - let (storage, fake_coin) = system::reserve_space( - &mut system, - 5000, - 3, - fake_coin, - &mut ctx, - ); - - // Register a Blob - let blob_id = blob::derive_blob_id(0xABC, RED_STUFF, 5000); - let blob1 = blob::register(&system, storage, blob_id, 0xABC, 5000, RED_STUFF, &mut ctx); - - coin::burn_for_testing(fake_coin); - blob::drop_for_testing(blob1); - system - } - - #[test] - public fun test_blob_certify_happy_path(): system::System { - let mut ctx = tx_context::dummy(); - - // A test coin. - let fake_coin = coin::mint_for_testing(100000000, &mut ctx); - - // Create a new committee - let committee = committee::committee_for_testing(0); - - // Create a new system object - let mut system: system::System = system::new(committee, 1000000000, 5, &mut ctx); - - // Get some space for a few epochs - let (storage, fake_coin) = system::reserve_space( - &mut system, - 1_000_000, - 3, - fake_coin, - &mut ctx, - ); - - // Register a Blob - let blob_id = blob::derive_blob_id(0xABC, RED_STUFF, 5000); - let mut blob1 = blob::register(&system, storage, blob_id, 0xABC, 5000, RED_STUFF, &mut ctx); - - let certify_message = blob::certified_blob_message_for_testing(0, blob_id); - - // Set certify - blob::certify_with_certified_msg(&system, certify_message, &mut blob1); - - // Assert certified - assert!(option::is_some(blob::certified_epoch(&blob1)), 0); - - coin::burn_for_testing(fake_coin); - blob::drop_for_testing(blob1); - system - } - - #[test] - public fun test_blob_certify_single_function(): system::System { - let mut ctx = tx_context::dummy(); - - // Derive blob ID and root_hash from bytes - let root_hash_vec = vector[ - 1, 2, 3, 4, 5, 6, 7, 8, - 1, 2, 3, 4, 5, 6, 7, 8, - 1, 2, 3, 4, 5, 6, 7, 8, - 1, 2, 3, 4, 5, 6, 7, 8, - ]; - - let mut encode = bcs::new(root_hash_vec); - let root_hash = bcs::peel_u256(&mut encode); - - let blob_id_vec = vector[ - 119, 174, 25, 167, 128, 57, 96, 1, - 163, 56, 61, 132, 191, 35, 44, 18, - 231, 224, 79, 178, 85, 51, 69, 53, - 214, 95, 198, 203, 56, 221, 111, 83 - ]; - - let mut encode = bcs::new(blob_id_vec); - let blob_id = bcs::peel_u256(&mut encode); - - // Derive and check blob ID - let blob_id_bis = blob::derive_blob_id(root_hash, RED_STUFF, 10000); - assert!(blob_id == blob_id_bis, 0); - - // BCS confirmation message for epoch 0 and blob id `blob_id` with intents - let confirmation = vector[ - 1, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, - 119, 174, 25, 167, 128, 57, 96, 1, 163, 56, 61, 132, - 191, 35, 44, 18, 231, 224, 79, 178, 85, 51, 69, 53, 214, - 95, 198, 203, 56, 221, 111, 83 - ]; - // Signature from private key scalar(117) on `confirmation` - let signature = vector[ - 184, 138, 78, 92, 221, 170, 180, 107, 75, 249, 222, 177, 183, 25, 107, 214, 237, - 214, 213, 12, 239, 65, 88, 112, 65, 229, 225, 23, 62, 158, 144, 67, 206, 37, 148, - 1, 69, 64, 190, 180, 121, 153, 39, 149, 41, 2, 112, 69, 23, 68, 69, 159, 192, 116, - 41, 113, 21, 116, 123, 169, 204, 165, 232, 70, 146, 1, 175, 70, 126, 14, 20, 206, - 113, 234, 141, 195, 218, 52, 172, 56, 78, 168, 114, 213, 241, 83, 188, 215, 123, - 191, 111, 136, 26, 193, 60, 246 - ]; - - // A test coin. - let fake_coin = coin::mint_for_testing(100000000, &mut ctx); - - // Create storage node - // Pk corresponding to secret key scalar(117) - let public_key = vector[ - 149, 234, 204, 58, 220, 9, 200, 39, 89, 63, 88, 30, 142, 45, - 224, 104, 191, 76, 245, 208, 192, 235, 41, 229, 55, 47, 13, 35, 54, 71, 136, 238, 15, - 155, 235, 17, 44, 138, 126, 156, 47, 12, 114, 4, 51, 112, 92, 240 - ]; - let storage_node = storage_node::create_storage_node_info( - string::utf8(b"node"), - string::utf8(b"127.0.0.1"), - public_key, - NETWORK_PUBLIC_KEY, - vector[0, 1, 2, 3, 4, 5], - ); - - // Create a new committee - let cap = committee::create_committee_cap_for_tests(); - let committee = committee::create_committee(&cap, 0, vector[storage_node]); - - // Create a new system object - let mut system: system::System = system::new(committee, 1000000000, 5, &mut ctx); - - // Get some space for a few epochs - let (storage, fake_coin) = system::reserve_space( - &mut system, - 1_000_000, - 3, - fake_coin, - &mut ctx, - ); - - // Register a Blob - let mut blob1 = blob::register( - &system, - storage, - blob_id, - root_hash, - 10000, - RED_STUFF, - &mut ctx, - ); - - // Set certify - blob::certify(&system, &mut blob1, signature, vector[0], confirmation); - - // Assert certified - assert!(option::is_some(blob::certified_epoch(&blob1)), 0); - - coin::burn_for_testing(fake_coin); - blob::drop_for_testing(blob1); - system - } - - #[test, expected_failure(abort_code=blob::EWrongEpoch)] - public fun test_blob_certify_bad_epoch(): system::System { - let mut ctx = tx_context::dummy(); - - // A test coin. - let fake_coin = coin::mint_for_testing(100000000, &mut ctx); - - // Create a new committee - let committee = committee::committee_for_testing(0); - - // Create a new system object - let mut system: system::System = system::new(committee, 1000000000, 5, &mut ctx); - - // Get some space for a few epochs - let (storage, fake_coin) = system::reserve_space( - &mut system, - 1_000_000, - 3, - fake_coin, - &mut ctx, - ); - - // Register a Blob - let blob_id = blob::derive_blob_id(0xABC, RED_STUFF, 5000); - let mut blob1 = blob::register(&system, storage, blob_id, 0xABC, 5000, RED_STUFF, &mut ctx); - - // Set INCORRECT EPOCH TO 1 - let certify_message = blob::certified_blob_message_for_testing(1, blob_id); - - // Set certify - blob::certify_with_certified_msg(&system, certify_message, &mut blob1); - - coin::burn_for_testing(fake_coin); - blob::drop_for_testing(blob1); - system - } - - #[test, expected_failure(abort_code=blob::EInvalidBlobId)] - public fun test_blob_certify_bad_blob_id(): system::System { - let mut ctx = tx_context::dummy(); - - // A test coin. - let fake_coin = coin::mint_for_testing(100000000, &mut ctx); - - // Create a new committee - let committee = committee::committee_for_testing(0); - - // Create a new system object - let mut system: system::System = system::new(committee, 1000000000, 5, &mut ctx); - - // Get some space for a few epochs - let (storage, fake_coin) = system::reserve_space( - &mut system, - 1_000_000, - 3, - fake_coin, - &mut ctx, - ); - - // Register a Blob - let blob_id = blob::derive_blob_id(0xABC, RED_STUFF, 5000); - let mut blob1 = blob::register(&system, storage, blob_id, 0xABC, 5000, RED_STUFF, &mut ctx); - - // DIFFERENT blob id - let certify_message = blob::certified_blob_message_for_testing(0, 0xFFF); - - // Set certify - blob::certify_with_certified_msg(&system, certify_message, &mut blob1); - - coin::burn_for_testing(fake_coin); - blob::drop_for_testing(blob1); - system - } - - #[test, expected_failure(abort_code=blob::EResourceBounds)] - public fun test_blob_certify_past_epoch(): system::System { - let mut ctx = tx_context::dummy(); - - // A test coin. - let fake_coin = coin::mint_for_testing(100000000, &mut ctx); - - // Create a new committee - let committee = committee::committee_for_testing(0); - - // Create a new system object - let mut system: system::System = system::new(committee, 1000000000, 5, &mut ctx); - - // Get some space for a few epochs - let (storage, fake_coin) = system::reserve_space( - &mut system, - 1_000_000, - 3, - fake_coin, - &mut ctx, - ); - - // Register a Blob - let blob_id = blob::derive_blob_id(0xABC, RED_STUFF, 5000); - let mut blob1 = blob::register(&system, storage, blob_id, 0xABC, 5000, RED_STUFF, &mut ctx); - - // Advance epoch -- to epoch 1 - let committee = committee::committee_for_testing(1); - let epoch_accounts = system::next_epoch(&mut system, committee, 1000, 3); - sa::burn_for_testing(epoch_accounts); - - // Advance epoch -- to epoch 2 - system::set_done_for_testing(&mut system); - let committee = committee::committee_for_testing(2); - let epoch_accounts = system::next_epoch(&mut system, committee, 1000, 3); - sa::burn_for_testing(epoch_accounts); - - // Advance epoch -- to epoch 3 - system::set_done_for_testing(&mut system); - let committee = committee::committee_for_testing(3); - let epoch_accounts = system::next_epoch(&mut system, committee, 1000, 3); - sa::burn_for_testing(epoch_accounts); - - // Set certify -- EPOCH BEYOND RESOURCE BOUND - let certify_message = blob::certified_blob_message_for_testing(3, blob_id); - blob::certify_with_certified_msg(&system, certify_message, &mut blob1); - - coin::burn_for_testing(fake_coin); - blob::drop_for_testing(blob1); - system - } - - #[test] - public fun test_blob_happy_destroy(): system::System { - let mut ctx = tx_context::dummy(); - - // A test coin. - let fake_coin = coin::mint_for_testing(100000000, &mut ctx); - - // Create a new committee - let committee = committee::committee_for_testing(0); - - // Create a new system object - let mut system: system::System = system::new(committee, 1000000000, 5, &mut ctx); - - // Get some space for a few epochs - let (storage, fake_coin) = system::reserve_space( - &mut system, - 1_000_000, - 3, - fake_coin, - &mut ctx, - ); - - // Register a Blob - let blob_id = blob::derive_blob_id(0xABC, RED_STUFF, 5000); - let mut blob1 = blob::register(&system, storage, blob_id, 0xABC, 5000, RED_STUFF, &mut ctx); - - // Set certify - let certify_message = blob::certified_blob_message_for_testing(0, blob_id); - blob::certify_with_certified_msg(&system, certify_message, &mut blob1); - - // Advance epoch -- to epoch 1 - let committee = committee::committee_for_testing(1); - let epoch_accounts = system::next_epoch(&mut system, committee, 1000, 3); - sa::burn_for_testing(epoch_accounts); - - // Advance epoch -- to epoch 2 - system::set_done_for_testing(&mut system); - let committee = committee::committee_for_testing(2); - let epoch_accounts = system::next_epoch(&mut system, committee, 1000, 3); - sa::burn_for_testing(epoch_accounts); - - // Advance epoch -- to epoch 3 - system::set_done_for_testing(&mut system); - let committee = committee::committee_for_testing(3); - let epoch_accounts = system::next_epoch(&mut system, committee, 1000, 3); - sa::burn_for_testing(epoch_accounts); - - // Destroy the blob - blob::destroy_blob(&system, blob1); - - coin::burn_for_testing(fake_coin); - system - } - - #[test, expected_failure(abort_code=blob::EResourceBounds)] - public fun test_blob_unhappy_destroy(): system::System { - let mut ctx = tx_context::dummy(); - - // A test coin. - let fake_coin = coin::mint_for_testing(100000000, &mut ctx); - - // Create a new committee - let committee = committee::committee_for_testing(0); - - // Create a new system object - let mut system: system::System = system::new(committee, 1000000000, 5, &mut ctx); - - // Get some space for a few epochs - let (storage, fake_coin) = system::reserve_space( - &mut system, - 1_000_000, - 3, - fake_coin, - &mut ctx, - ); - - // Register a Blob - let blob_id = blob::derive_blob_id(0xABC, RED_STUFF, 5000); - let blob1 = blob::register(&system, storage, blob_id, 0xABC, 5000, RED_STUFF, &mut ctx); - - // Destroy the blob - blob::destroy_blob(&system, blob1); - - coin::burn_for_testing(fake_coin); - system - } - - #[test] - public fun test_certified_blob_message() { - let msg = committee::certified_message_for_testing( - 1, 0, 10, 100, vector[ - 0xAA, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - ] - ); - - let message = blob::certify_blob_message(msg); - assert!(blob::message_blob_id(&message) == 0xAA, 0); - } - - #[test, expected_failure] - public fun test_certified_blob_message_too_short() { - let msg = committee::certified_message_for_testing( - 1, 0, 10, 100, vector[ - 0xAA, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, - ], - ); - - let message = blob::certify_blob_message(msg); - assert!(blob::message_blob_id(&message) == 0xAA, 0); - } - - #[test] - public fun test_blob_extend_happy_path(): system::System { - let mut ctx = tx_context::dummy(); - - // A test coin. - let fake_coin = coin::mint_for_testing(100000000, &mut ctx); - - // Create a new committee - let committee = committee::committee_for_testing(0); - - // Create a new system object - let mut system: system::System = system::new(committee, 1000000000, 5, &mut ctx); - - // Get some space for a few epochs - let (storage, fake_coin) = system::reserve_space( - &mut system, - 1_000_000, - 3, - fake_coin, - &mut ctx, - ); - - // Get a longer storage period - let (mut storage_long, fake_coin) = system::reserve_space( - &mut system, - 1_000_000, - 5, - fake_coin, - &mut ctx, - ); - - // Split by period - let trailing_storage = split_by_epoch(&mut storage_long, 3, &mut ctx); - - // Register a Blob - let blob_id = blob::derive_blob_id(0xABC, RED_STUFF, 5000); - let mut blob1 = blob::register(&system, storage, blob_id, 0xABC, 5000, RED_STUFF, &mut ctx); - let certify_message = blob::certified_blob_message_for_testing(0, blob_id); - - // Set certify - blob::certify_with_certified_msg(&system, certify_message, &mut blob1); - - // Now extend the blob - blob::extend(&system, &mut blob1, trailing_storage); - - // Assert certified - assert!(option::is_some(blob::certified_epoch(&blob1)), 0); - - destroy(storage_long); - coin::burn_for_testing(fake_coin); - blob::drop_for_testing(blob1); - system - } - - #[test, expected_failure] - public fun test_blob_extend_bad_period(): system::System { - let mut ctx = tx_context::dummy(); - - // A test coin. - let fake_coin = coin::mint_for_testing(100000000, &mut ctx); - - // Create a new committee - let committee = committee::committee_for_testing(0); - - // Create a new system object - let mut system: system::System = system::new(committee, 1000000000, 5, &mut ctx); - - // Get some space for a few epochs - let (storage, fake_coin) = system::reserve_space( - &mut system, - 1_000_000, - 3, - fake_coin, - &mut ctx, - ); - - // Get a longer storage period - let (mut storage_long, fake_coin) = system::reserve_space( - &mut system, - 1_000_000, - 5, - fake_coin, - &mut ctx, - ); - - // Split by period - let trailing_storage = split_by_epoch(&mut storage_long, 4, &mut ctx); - - // Register a Blob - let blob_id = blob::derive_blob_id(0xABC, RED_STUFF, 5000); - let mut blob1 = blob::register(&system, storage, blob_id, 0xABC, 5000, RED_STUFF, &mut ctx); - let certify_message = blob::certified_blob_message_for_testing(0, 0xABC); - - // Set certify - blob::certify_with_certified_msg(&system, certify_message, &mut blob1); - - // Now extend the blob // ITS THE WRONG PERIOD - blob::extend(&system, &mut blob1, trailing_storage); - - destroy(storage_long); - coin::burn_for_testing(fake_coin); - blob::drop_for_testing(blob1); - system - } - - #[test,expected_failure(abort_code=blob::EResourceBounds)] - public fun test_blob_unhappy_extend(): system::System { - let mut ctx = tx_context::dummy(); - - // A test coin. - let fake_coin = coin::mint_for_testing(100000000, &mut ctx); - - // Create a new committee - let committee = committee::committee_for_testing(0); - - // Create a new system object - let mut system: system::System = system::new(committee, 1000000000, 5, &mut ctx); - - // Get some space for a few epochs - let (storage, fake_coin) = system::reserve_space( - &mut system, - 1_000_000, - 3, - fake_coin, - &mut ctx, - ); - - // Get a longer storage period - let (mut storage_long, fake_coin) = system::reserve_space( - &mut system, - 1_000_000, - 5, - fake_coin, - &mut ctx, - ); - - // Split by period - let trailing_storage = split_by_epoch(&mut storage_long, 3, &mut ctx); - - // Register a Blob - let blob_id = blob::derive_blob_id(0xABC, RED_STUFF, 5000); - let mut blob1 = blob::register(&system, storage, blob_id, 0xABC, 5000, RED_STUFF, &mut ctx); - - // Set certify - let certify_message = blob::certified_blob_message_for_testing(0, blob_id); - blob::certify_with_certified_msg(&system, certify_message, &mut blob1); - - // Advance epoch -- to epoch 1 - let committee = committee::committee_for_testing(1); - let epoch_accounts = system::next_epoch(&mut system, committee, 1000, 3); - sa::burn_for_testing(epoch_accounts); - - // Advance epoch -- to epoch 2 - system::set_done_for_testing(&mut system); - let committee = committee::committee_for_testing(2); - let epoch_accounts = system::next_epoch(&mut system, committee, 1000, 3); - sa::burn_for_testing(epoch_accounts); - - // Advance epoch -- to epoch 3 - system::set_done_for_testing(&mut system); - let committee = committee::committee_for_testing(3); - let epoch_accounts = system::next_epoch(&mut system, committee, 1000, 3); - sa::burn_for_testing(epoch_accounts); - - // Try to extend after expiry. - - // Now extend the blo - blob::extend(&system, &mut blob1, trailing_storage); - - // Destroy the blob - blob::destroy_blob(&system, blob1); - - destroy(storage_long); - coin::burn_for_testing(fake_coin); - system - } -} diff --git a/contracts/blob_store/sources/tests/bls_tests.move b/contracts/blob_store/sources/tests/bls_tests.move deleted file mode 100644 index 27a39aa5..00000000 --- a/contracts/blob_store/sources/tests/bls_tests.move +++ /dev/null @@ -1,205 +0,0 @@ -// editorconfig-checker-disable-file -// Data here autogenerated by python file - -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -#[test_only] -module blob_store::bls_tests { - - use sui::bls12381::bls12381_min_pk_verify; - - use blob_store::bls_aggregate::{Self, BlsCommittee, new_bls_committee, verify_certificate}; - use blob_store::storage_node; - - #[test] - public fun test_basic_compatibility(){ - - // Check the basic python compatibility - - let pub_key_bytes = vector[142, 78, 70, 3, 179, 142, 145, 75, 170, 36, 5, 232, 153, 164, 205, 57, 24, 216, 208, 34, 87, 213, 225, 76, 5, 157, 212, 88, 161, 34, 75, 145, 206, 144, 85, 11, 197, 110, 75, 175, 215, 194, 78, 51, 192, 196, 59, 204]; - let message = vector[104, 101, 108, 108, 111]; - let signature = vector[167, 32, 44, 82, 208, 22, 233, 67, 235, 217, 254, 68, 183, 43, 226, 203, 148, 213, 13, 105, 152, 28, 1, 169, 159, 62, 217, 47, 175, 237, 162, 94, 2, 38, 239, 56, 181, 123, 19, 123, 93, 253, 16, 64, 9, 109, 42, 3, 14, 11, 80, 109, 92, 8, 61, 88, 246, 66, 65, 15, 235, 232, 216, 240, 96, 192, 77, 134, 179, 40, 232, 125, 35, 136, 196, 16, 24, 52, 145, 128, 9, 42, 206, 191, 49, 91, 139, 252, 25, 5, 167, 199, 132, 203, 25, 154]; - - assert!(bls12381_min_pk_verify( - &signature, - &pub_key_bytes, - &message), 0); - - } - - #[test] - public fun test_check_aggregate(): BlsCommittee { - let pk0 = vector[166, 14, 117, 25, 14, 98, 182, 165, 65, 66, 209, 71, 40, 154, 115, 92, 76, 225, 26, 157, 153, 117, 67, 218, 83, 154, 61, 181, 125, 239, 94, 216, 59, 164, 11, 116, 229, 80, 101, 240, 43, 53, 170, 29, 80, 76, 64, 75]; - let pk1 = vector[174, 18, 3, 148, 89, 198, 4, 145, 103, 43, 106, 98, 130, 53, 93, 135, 101, 186, 98, 114, 56, 127, 185, 26, 62, 150, 4, 250, 42, 129, 69, 12, 241, 107, 135, 11, 180, 70, 252, 58, 62, 10, 24, 127, 255, 111, 137, 69]; - let pk2 = vector[148, 123, 50, 124, 138, 21, 179, 150, 52, 164, 38, 175, 112, 192, 98, 181, 6, 50, 167, 68, 237, 221, 65, 181, 164, 104, 100, 20, 239, 76, 217, 116, 107, 177, 29, 10, 83, 198, 194, 255, 33, 187, 207, 51, 30, 7, 172, 146]; - let pk3 = vector[133, 252, 74, 229, 67, 202, 22, 36, 116, 88, 110, 118, 215, 44, 71, 208, 21, 28, 60, 183, 183, 126, 130, 200, 126, 85, 74, 191, 114, 84, 142, 46, 116, 107, 198, 117, 128, 91, 104, 139, 80, 22, 38, 158, 24, 255, 66, 80]; - let pk4 = vector[140, 170, 13, 232, 98, 121, 62, 86, 124, 96, 80, 170, 130, 45, 178, 214, 203, 43, 82, 11, 198, 43, 109, 188, 186, 126, 119, 48, 103, 237, 9, 199, 186, 2, 130, 215, 194, 14, 1, 80, 12, 108, 47, 167, 100, 8, 173, 237]; - let pk5 = vector[170, 39, 63, 208, 83, 35, 225, 56, 30, 16, 233, 62, 104, 60, 52, 100, 115, 40, 18, 112, 32, 179, 80, 127, 200, 205, 220, 51, 112, 56, 227, 63, 189, 122, 153, 239, 13, 44, 123, 106, 39, 141, 127, 129, 22, 22, 37, 96]; - let pk6 = vector[143, 206, 207, 249, 174, 4, 144, 247, 35, 18, 56, 34, 198, 111, 54, 153, 109, 35, 116, 144, 214, 118, 158, 230, 143, 159, 122, 125, 161, 198, 186, 200, 181, 195, 208, 196, 52, 142, 140, 232, 252, 61, 81, 89, 248, 51, 52, 132]; - let pk7 = vector[143, 79, 254, 129, 165, 12, 241, 23, 6, 156, 154, 102, 173, 159, 39, 118, 238, 234, 233, 79, 224, 43, 162, 160, 249, 89, 108, 183, 152, 249, 229, 189, 244, 113, 159, 206, 170, 97, 116, 111, 254, 36, 8, 242, 91, 86, 217, 110]; - let pk8 = vector[135, 133, 64, 95, 39, 94, 226, 253, 147, 78, 131, 131, 90, 121, 186, 101, 31, 128, 176, 244, 50, 223, 27, 128, 99, 80, 220, 148, 156, 22, 156, 96, 230, 7, 103, 228, 31, 174, 216, 234, 172, 94, 208, 233, 226, 16, 120, 124]; - let pk9 = vector[128, 173, 226, 9, 19, 120, 41, 58, 99, 213, 83, 40, 206, 242, 55, 54, 244, 219, 220, 73, 189, 60, 7, 135, 184, 193, 140, 214, 168, 221, 194, 212, 42, 39, 146, 66, 232, 123, 34, 209, 144, 159, 63, 29, 85, 229, 218, 102]; - let message = vector[104, 101, 108, 108, 111]; - - // This is the aggregate sig for keys 0, 1, 2, 3, 4, 5, 6 - let agg_sig = vector[134, 145, 54, 247, 223, 68, 1, 65, 112, 10, 160, 125, 172, 100, 93, 62, 192, 216, 7, 129, 27, 180, 99, 101, 45, 248, 123, 114, 102, 97, 180, 101, 8, 246, 118, 94, 149, 82, 158, 181, 134, 28, 177, 85, 241, 53, 152, 176, 22, 227, 147, 88, 180, 160, 138, 174, 97, 9, 70, 172, 29, 128, 192, 254, 252, 43, 131, 182, 120, 126, 203, 191, 202, 186, 23, 179, 170, 184, 146, 236, 83, 21, 7, 2, 177, 103, 103, 138, 13, 41, 47, 180, 1, 156, 29, 162]; - - // Make a new committee - let committee = new_bls_committee( - vector[ - storage_node::new_for_testing(pk0, 1), storage_node::new_for_testing(pk1, 1), storage_node::new_for_testing(pk2, 1), storage_node::new_for_testing(pk3, 1), storage_node::new_for_testing(pk4, 1), storage_node::new_for_testing(pk5, 1), storage_node::new_for_testing(pk6, 1), storage_node::new_for_testing(pk7, 1), storage_node::new_for_testing(pk8, 1), storage_node::new_for_testing(pk9, 1) - ] - ); - - // Verify the aggregate signature - verify_certificate( - &committee, - &agg_sig, - &vector[0, 1, 2, 3, 4, 5, 6], - &message - ); - - committee - - } - - #[test, expected_failure(abort_code = bls_aggregate::ESigVerification) ] - public fun test_add_members_error(): BlsCommittee { - let pk0 = vector[166, 14, 117, 25, 14, 98, 182, 165, 65, 66, 209, 71, 40, 154, 115, 92, 76, 225, 26, 157, 153, 117, 67, 218, 83, 154, 61, 181, 125, 239, 94, 216, 59, 164, 11, 116, 229, 80, 101, 240, 43, 53, 170, 29, 80, 76, 64, 75]; - let pk1 = vector[174, 18, 3, 148, 89, 198, 4, 145, 103, 43, 106, 98, 130, 53, 93, 135, 101, 186, 98, 114, 56, 127, 185, 26, 62, 150, 4, 250, 42, 129, 69, 12, 241, 107, 135, 11, 180, 70, 252, 58, 62, 10, 24, 127, 255, 111, 137, 69]; - let pk2 = vector[148, 123, 50, 124, 138, 21, 179, 150, 52, 164, 38, 175, 112, 192, 98, 181, 6, 50, 167, 68, 237, 221, 65, 181, 164, 104, 100, 20, 239, 76, 217, 116, 107, 177, 29, 10, 83, 198, 194, 255, 33, 187, 207, 51, 30, 7, 172, 146]; - let pk3 = vector[133, 252, 74, 229, 67, 202, 22, 36, 116, 88, 110, 118, 215, 44, 71, 208, 21, 28, 60, 183, 183, 126, 130, 200, 126, 85, 74, 191, 114, 84, 142, 46, 116, 107, 198, 117, 128, 91, 104, 139, 80, 22, 38, 158, 24, 255, 66, 80]; - let pk4 = vector[140, 170, 13, 232, 98, 121, 62, 86, 124, 96, 80, 170, 130, 45, 178, 214, 203, 43, 82, 11, 198, 43, 109, 188, 186, 126, 119, 48, 103, 237, 9, 199, 186, 2, 130, 215, 194, 14, 1, 80, 12, 108, 47, 167, 100, 8, 173, 237]; - let pk5 = vector[170, 39, 63, 208, 83, 35, 225, 56, 30, 16, 233, 62, 104, 60, 52, 100, 115, 40, 18, 112, 32, 179, 80, 127, 200, 205, 220, 51, 112, 56, 227, 63, 189, 122, 153, 239, 13, 44, 123, 106, 39, 141, 127, 129, 22, 22, 37, 96]; - let pk6 = vector[143, 206, 207, 249, 174, 4, 144, 247, 35, 18, 56, 34, 198, 111, 54, 153, 109, 35, 116, 144, 214, 118, 158, 230, 143, 159, 122, 125, 161, 198, 186, 200, 181, 195, 208, 196, 52, 142, 140, 232, 252, 61, 81, 89, 248, 51, 52, 132]; - let pk7 = vector[143, 79, 254, 129, 165, 12, 241, 23, 6, 156, 154, 102, 173, 159, 39, 118, 238, 234, 233, 79, 224, 43, 162, 160, 249, 89, 108, 183, 152, 249, 229, 189, 244, 113, 159, 206, 170, 97, 116, 111, 254, 36, 8, 242, 91, 86, 217, 110]; - let pk8 = vector[135, 133, 64, 95, 39, 94, 226, 253, 147, 78, 131, 131, 90, 121, 186, 101, 31, 128, 176, 244, 50, 223, 27, 128, 99, 80, 220, 148, 156, 22, 156, 96, 230, 7, 103, 228, 31, 174, 216, 234, 172, 94, 208, 233, 226, 16, 120, 124]; - let pk9 = vector[128, 173, 226, 9, 19, 120, 41, 58, 99, 213, 83, 40, 206, 242, 55, 54, 244, 219, 220, 73, 189, 60, 7, 135, 184, 193, 140, 214, 168, 221, 194, 212, 42, 39, 146, 66, 232, 123, 34, 209, 144, 159, 63, 29, 85, 229, 218, 102]; - let message = vector[104, 101, 108, 108, 111]; - let agg_sig = vector[134, 145, 54, 247, 223, 68, 1, 65, 112, 10, 160, 125, 172, 100, 93, 62, 192, 216, 7, 129, 27, 180, 99, 101, 45, 248, 123, 114, 102, 97, 180, 101, 8, 246, 118, 94, 149, 82, 158, 181, 134, 28, 177, 85, 241, 53, 152, 176, 22, 227, 147, 88, 180, 160, 138, 174, 97, 9, 70, 172, 29, 128, 192, 254, 252, 43, 131, 182, 120, 126, 203, 191, 202, 186, 23, 179, 170, 184, 146, 236, 83, 21, 7, 2, 177, 103, 103, 138, 13, 41, 47, 180, 1, 156, 29, 162]; - - // Make a new committee - let committee = new_bls_committee( - vector[ - storage_node::new_for_testing(pk0, 1), storage_node::new_for_testing(pk1, 1), storage_node::new_for_testing(pk2, 1), storage_node::new_for_testing(pk3, 1), storage_node::new_for_testing(pk4, 1), storage_node::new_for_testing(pk5, 1), storage_node::new_for_testing(pk6, 1), storage_node::new_for_testing(pk7, 1), storage_node::new_for_testing(pk8, 1), storage_node::new_for_testing(pk9, 1) - ] - ); - - // Verify the aggregate signature - verify_certificate( - &committee, - &agg_sig, - &vector[0, 1, 2, 3, 4, 5, 6, 7], - &message - ); - - committee - - } - - #[test, expected_failure(abort_code = bls_aggregate::ESigVerification) ] - public fun test_incorrect_signature_error(): BlsCommittee { - let pk0 = vector[166, 14, 117, 25, 14, 98, 182, 165, 65, 66, 209, 71, 40, 154, 115, 92, 76, 225, 26, 157, 153, 117, 67, 218, 83, 154, 61, 181, 125, 239, 94, 216, 59, 164, 11, 116, 229, 80, 101, 240, 43, 53, 170, 29, 80, 76, 64, 75]; - let pk1 = vector[174, 18, 3, 148, 89, 198, 4, 145, 103, 43, 106, 98, 130, 53, 93, 135, 101, 186, 98, 114, 56, 127, 185, 26, 62, 150, 4, 250, 42, 129, 69, 12, 241, 107, 135, 11, 180, 70, 252, 58, 62, 10, 24, 127, 255, 111, 137, 69]; - let pk2 = vector[148, 123, 50, 124, 138, 21, 179, 150, 52, 164, 38, 175, 112, 192, 98, 181, 6, 50, 167, 68, 237, 221, 65, 181, 164, 104, 100, 20, 239, 76, 217, 116, 107, 177, 29, 10, 83, 198, 194, 255, 33, 187, 207, 51, 30, 7, 172, 146]; - let pk3 = vector[133, 252, 74, 229, 67, 202, 22, 36, 116, 88, 110, 118, 215, 44, 71, 208, 21, 28, 60, 183, 183, 126, 130, 200, 126, 85, 74, 191, 114, 84, 142, 46, 116, 107, 198, 117, 128, 91, 104, 139, 80, 22, 38, 158, 24, 255, 66, 80]; - let pk4 = vector[140, 170, 13, 232, 98, 121, 62, 86, 124, 96, 80, 170, 130, 45, 178, 214, 203, 43, 82, 11, 198, 43, 109, 188, 186, 126, 119, 48, 103, 237, 9, 199, 186, 2, 130, 215, 194, 14, 1, 80, 12, 108, 47, 167, 100, 8, 173, 237]; - let pk5 = vector[170, 39, 63, 208, 83, 35, 225, 56, 30, 16, 233, 62, 104, 60, 52, 100, 115, 40, 18, 112, 32, 179, 80, 127, 200, 205, 220, 51, 112, 56, 227, 63, 189, 122, 153, 239, 13, 44, 123, 106, 39, 141, 127, 129, 22, 22, 37, 96]; - let pk6 = vector[143, 206, 207, 249, 174, 4, 144, 247, 35, 18, 56, 34, 198, 111, 54, 153, 109, 35, 116, 144, 214, 118, 158, 230, 143, 159, 122, 125, 161, 198, 186, 200, 181, 195, 208, 196, 52, 142, 140, 232, 252, 61, 81, 89, 248, 51, 52, 132]; - let pk7 = vector[143, 79, 254, 129, 165, 12, 241, 23, 6, 156, 154, 102, 173, 159, 39, 118, 238, 234, 233, 79, 224, 43, 162, 160, 249, 89, 108, 183, 152, 249, 229, 189, 244, 113, 159, 206, 170, 97, 116, 111, 254, 36, 8, 242, 91, 86, 217, 110]; - let pk8 = vector[135, 133, 64, 95, 39, 94, 226, 253, 147, 78, 131, 131, 90, 121, 186, 101, 31, 128, 176, 244, 50, 223, 27, 128, 99, 80, 220, 148, 156, 22, 156, 96, 230, 7, 103, 228, 31, 174, 216, 234, 172, 94, 208, 233, 226, 16, 120, 124]; - let pk9 = vector[128, 173, 226, 9, 19, 120, 41, 58, 99, 213, 83, 40, 206, 242, 55, 54, 244, 219, 220, 73, 189, 60, 7, 135, 184, 193, 140, 214, 168, 221, 194, 212, 42, 39, 146, 66, 232, 123, 34, 209, 144, 159, 63, 29, 85, 229, 218, 102]; - let message = vector[104, 101, 108, 108, 111]; - // BAD SIGNATURE - let agg_sig = vector[133, 145, 54, 247, 223, 68, 1, 65, 112, 10, 160, 125, 172, 100, 93, 62, 192, 216, 7, 129, 27, 180, 99, 101, 45, 248, 123, 114, 102, 97, 180, 101, 8, 246, 118, 94, 149, 82, 158, 181, 134, 28, 177, 85, 241, 53, 152, 176, 22, 227, 147, 88, 180, 160, 138, 174, 97, 9, 70, 172, 29, 128, 192, 254, 252, 43, 131, 182, 120, 126, 203, 191, 202, 186, 23, 179, 170, 184, 146, 236, 83, 21, 7, 2, 177, 103, 103, 138, 13, 41, 47, 180, 1, 156, 29, 162]; - - // Make a new committee - let committee = new_bls_committee( - vector[ - storage_node::new_for_testing(pk0, 1), storage_node::new_for_testing(pk1, 1), storage_node::new_for_testing(pk2, 1), storage_node::new_for_testing(pk3, 1), storage_node::new_for_testing(pk4, 1), storage_node::new_for_testing(pk5, 1), storage_node::new_for_testing(pk6, 1), storage_node::new_for_testing(pk7, 1), storage_node::new_for_testing(pk8, 1), storage_node::new_for_testing(pk9, 1) - ] - ); - - // Verify the aggregate signature - verify_certificate( - &committee, - &agg_sig, - &vector[0, 1, 2, 3, 4, 5, 6], - &message - ); - - committee - - } - - #[test, expected_failure(abort_code = bls_aggregate::ETotalMemberOrder) ] - public fun test_duplicate_member_error(): BlsCommittee { - let pk0 = vector[166, 14, 117, 25, 14, 98, 182, 165, 65, 66, 209, 71, 40, 154, 115, 92, 76, 225, 26, 157, 153, 117, 67, 218, 83, 154, 61, 181, 125, 239, 94, 216, 59, 164, 11, 116, 229, 80, 101, 240, 43, 53, 170, 29, 80, 76, 64, 75]; - let pk1 = vector[174, 18, 3, 148, 89, 198, 4, 145, 103, 43, 106, 98, 130, 53, 93, 135, 101, 186, 98, 114, 56, 127, 185, 26, 62, 150, 4, 250, 42, 129, 69, 12, 241, 107, 135, 11, 180, 70, 252, 58, 62, 10, 24, 127, 255, 111, 137, 69]; - let pk2 = vector[148, 123, 50, 124, 138, 21, 179, 150, 52, 164, 38, 175, 112, 192, 98, 181, 6, 50, 167, 68, 237, 221, 65, 181, 164, 104, 100, 20, 239, 76, 217, 116, 107, 177, 29, 10, 83, 198, 194, 255, 33, 187, 207, 51, 30, 7, 172, 146]; - let pk3 = vector[133, 252, 74, 229, 67, 202, 22, 36, 116, 88, 110, 118, 215, 44, 71, 208, 21, 28, 60, 183, 183, 126, 130, 200, 126, 85, 74, 191, 114, 84, 142, 46, 116, 107, 198, 117, 128, 91, 104, 139, 80, 22, 38, 158, 24, 255, 66, 80]; - let pk4 = vector[140, 170, 13, 232, 98, 121, 62, 86, 124, 96, 80, 170, 130, 45, 178, 214, 203, 43, 82, 11, 198, 43, 109, 188, 186, 126, 119, 48, 103, 237, 9, 199, 186, 2, 130, 215, 194, 14, 1, 80, 12, 108, 47, 167, 100, 8, 173, 237]; - let pk5 = vector[170, 39, 63, 208, 83, 35, 225, 56, 30, 16, 233, 62, 104, 60, 52, 100, 115, 40, 18, 112, 32, 179, 80, 127, 200, 205, 220, 51, 112, 56, 227, 63, 189, 122, 153, 239, 13, 44, 123, 106, 39, 141, 127, 129, 22, 22, 37, 96]; - let pk6 = vector[143, 206, 207, 249, 174, 4, 144, 247, 35, 18, 56, 34, 198, 111, 54, 153, 109, 35, 116, 144, 214, 118, 158, 230, 143, 159, 122, 125, 161, 198, 186, 200, 181, 195, 208, 196, 52, 142, 140, 232, 252, 61, 81, 89, 248, 51, 52, 132]; - let pk7 = vector[143, 79, 254, 129, 165, 12, 241, 23, 6, 156, 154, 102, 173, 159, 39, 118, 238, 234, 233, 79, 224, 43, 162, 160, 249, 89, 108, 183, 152, 249, 229, 189, 244, 113, 159, 206, 170, 97, 116, 111, 254, 36, 8, 242, 91, 86, 217, 110]; - let pk8 = vector[135, 133, 64, 95, 39, 94, 226, 253, 147, 78, 131, 131, 90, 121, 186, 101, 31, 128, 176, 244, 50, 223, 27, 128, 99, 80, 220, 148, 156, 22, 156, 96, 230, 7, 103, 228, 31, 174, 216, 234, 172, 94, 208, 233, 226, 16, 120, 124]; - let pk9 = vector[128, 173, 226, 9, 19, 120, 41, 58, 99, 213, 83, 40, 206, 242, 55, 54, 244, 219, 220, 73, 189, 60, 7, 135, 184, 193, 140, 214, 168, 221, 194, 212, 42, 39, 146, 66, 232, 123, 34, 209, 144, 159, 63, 29, 85, 229, 218, 102]; - let message = vector[104, 101, 108, 108, 111]; - // BAD SIGNATURE - let agg_sig = vector[134, 145, 54, 247, 223, 68, 1, 65, 112, 10, 160, 125, 172, 100, 93, 62, 192, 216, 7, 129, 27, 180, 99, 101, 45, 248, 123, 114, 102, 97, 180, 101, 8, 246, 118, 94, 149, 82, 158, 181, 134, 28, 177, 85, 241, 53, 152, 176, 22, 227, 147, 88, 180, 160, 138, 174, 97, 9, 70, 172, 29, 128, 192, 254, 252, 43, 131, 182, 120, 126, 203, 191, 202, 186, 23, 179, 170, 184, 146, 236, 83, 21, 7, 2, 177, 103, 103, 138, 13, 41, 47, 180, 1, 156, 29, 162]; - - // Make a new committee - let committee = new_bls_committee( - vector[ - storage_node::new_for_testing(pk0, 1), storage_node::new_for_testing(pk1, 1), storage_node::new_for_testing(pk2, 1), storage_node::new_for_testing(pk3, 1), storage_node::new_for_testing(pk4, 1), storage_node::new_for_testing(pk5, 1), storage_node::new_for_testing(pk6, 1), storage_node::new_for_testing(pk7, 1), storage_node::new_for_testing(pk8, 1), storage_node::new_for_testing(pk9, 1) - ] - ); - - // Verify the aggregate signature - verify_certificate( - &committee, - &agg_sig, - &vector[0, 1, 2, 3, 3, 5, 6], - &message - ); - - committee - - } - - #[test, expected_failure(abort_code = bls_aggregate::ENotEnoughStake) ] - public fun test_incorrect_stake_error(): BlsCommittee { - let pk0 = vector[166, 14, 117, 25, 14, 98, 182, 165, 65, 66, 209, 71, 40, 154, 115, 92, 76, 225, 26, 157, 153, 117, 67, 218, 83, 154, 61, 181, 125, 239, 94, 216, 59, 164, 11, 116, 229, 80, 101, 240, 43, 53, 170, 29, 80, 76, 64, 75]; - let pk1 = vector[174, 18, 3, 148, 89, 198, 4, 145, 103, 43, 106, 98, 130, 53, 93, 135, 101, 186, 98, 114, 56, 127, 185, 26, 62, 150, 4, 250, 42, 129, 69, 12, 241, 107, 135, 11, 180, 70, 252, 58, 62, 10, 24, 127, 255, 111, 137, 69]; - let pk2 = vector[148, 123, 50, 124, 138, 21, 179, 150, 52, 164, 38, 175, 112, 192, 98, 181, 6, 50, 167, 68, 237, 221, 65, 181, 164, 104, 100, 20, 239, 76, 217, 116, 107, 177, 29, 10, 83, 198, 194, 255, 33, 187, 207, 51, 30, 7, 172, 146]; - let pk3 = vector[133, 252, 74, 229, 67, 202, 22, 36, 116, 88, 110, 118, 215, 44, 71, 208, 21, 28, 60, 183, 183, 126, 130, 200, 126, 85, 74, 191, 114, 84, 142, 46, 116, 107, 198, 117, 128, 91, 104, 139, 80, 22, 38, 158, 24, 255, 66, 80]; - let pk4 = vector[140, 170, 13, 232, 98, 121, 62, 86, 124, 96, 80, 170, 130, 45, 178, 214, 203, 43, 82, 11, 198, 43, 109, 188, 186, 126, 119, 48, 103, 237, 9, 199, 186, 2, 130, 215, 194, 14, 1, 80, 12, 108, 47, 167, 100, 8, 173, 237]; - let pk5 = vector[170, 39, 63, 208, 83, 35, 225, 56, 30, 16, 233, 62, 104, 60, 52, 100, 115, 40, 18, 112, 32, 179, 80, 127, 200, 205, 220, 51, 112, 56, 227, 63, 189, 122, 153, 239, 13, 44, 123, 106, 39, 141, 127, 129, 22, 22, 37, 96]; - let pk6 = vector[143, 206, 207, 249, 174, 4, 144, 247, 35, 18, 56, 34, 198, 111, 54, 153, 109, 35, 116, 144, 214, 118, 158, 230, 143, 159, 122, 125, 161, 198, 186, 200, 181, 195, 208, 196, 52, 142, 140, 232, 252, 61, 81, 89, 248, 51, 52, 132]; - let pk7 = vector[143, 79, 254, 129, 165, 12, 241, 23, 6, 156, 154, 102, 173, 159, 39, 118, 238, 234, 233, 79, 224, 43, 162, 160, 249, 89, 108, 183, 152, 249, 229, 189, 244, 113, 159, 206, 170, 97, 116, 111, 254, 36, 8, 242, 91, 86, 217, 110]; - let pk8 = vector[135, 133, 64, 95, 39, 94, 226, 253, 147, 78, 131, 131, 90, 121, 186, 101, 31, 128, 176, 244, 50, 223, 27, 128, 99, 80, 220, 148, 156, 22, 156, 96, 230, 7, 103, 228, 31, 174, 216, 234, 172, 94, 208, 233, 226, 16, 120, 124]; - let pk9 = vector[128, 173, 226, 9, 19, 120, 41, 58, 99, 213, 83, 40, 206, 242, 55, 54, 244, 219, 220, 73, 189, 60, 7, 135, 184, 193, 140, 214, 168, 221, 194, 212, 42, 39, 146, 66, 232, 123, 34, 209, 144, 159, 63, 29, 85, 229, 218, 102]; - let message = vector[104, 101, 108, 108, 111]; - // BAD SIGNATURE - let agg_sig = vector[134, 145, 54, 247, 223, 68, 1, 65, 112, 10, 160, 125, 172, 100, 93, 62, 192, 216, 7, 129, 27, 180, 99, 101, 45, 248, 123, 114, 102, 97, 180, 101, 8, 246, 118, 94, 149, 82, 158, 181, 134, 28, 177, 85, 241, 53, 152, 176, 22, 227, 147, 88, 180, 160, 138, 174, 97, 9, 70, 172, 29, 128, 192, 254, 252, 43, 131, 182, 120, 126, 203, 191, 202, 186, 23, 179, 170, 184, 146, 236, 83, 21, 7, 2, 177, 103, 103, 138, 13, 41, 47, 180, 1, 156, 29, 162]; - - // Make a new committee - let committee = new_bls_committee( - vector[ - storage_node::new_for_testing(pk0, 1), storage_node::new_for_testing(pk1, 2), storage_node::new_for_testing(pk2, 2), storage_node::new_for_testing(pk3, 2), storage_node::new_for_testing(pk4, 2), storage_node::new_for_testing(pk5, 2), storage_node::new_for_testing(pk6, 2), storage_node::new_for_testing(pk7, 2), storage_node::new_for_testing(pk8, 2), storage_node::new_for_testing(pk9, 3) - ] - ); - - // Verify the aggregate signature - verify_certificate( - &committee, - &agg_sig, - &vector[0, 1, 2, 3, 4, 5, 6], - &message - ); - - committee - - } -} diff --git a/contracts/blob_store/sources/tests/committee_cert_tests.move b/contracts/blob_store/sources/tests/committee_cert_tests.move deleted file mode 100644 index 17f84f13..00000000 --- a/contracts/blob_store/sources/tests/committee_cert_tests.move +++ /dev/null @@ -1,75 +0,0 @@ -// editorconfig-checker-disable-file -// Data here autogenerated by python file - -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -#[test_only] -module blob_store::committee_cert_tests { - - use sui::bls12381::{bls12381_min_pk_verify}; - - use blob_store::bls_aggregate::new_bls_committee; - use blob_store::committee::{Self, Committee, committee_for_testing_with_bls , verify_quorum_in_epoch}; - use blob_store::storage_node; - - #[test] - public fun test_basic_correct() : Committee { - - let pub_key_bytes = vector[142, 78, 70, 3, 179, 142, 145, 75, 170, 36, 5, 232, 153, 164, 205, 57, 24, 216, 208, 34, 87, 213, 225, 76, 5, 157, 212, 88, 161, 34, 75, 145, 206, 144, 85, 11, 197, 110, 75, 175, 215, 194, 78, 51, 192, 196, 59, 204]; - let message = vector[1, 0, 3, 5, 0, 0, 0, 0, 0, 0, 0, 104, 101, 108, 108, 111]; - let signature = vector[173, 231, 27, 143, 41, 154, 49, 14, 85, 88, 187, 65, 86, 190, 161, 255, 219, 210, 78, 88, 179, 53, 11, 104, 168, 220, 40, 13, 91, 254, 191, 116, 161, 252, 196, 19, 24, 153, 126, 248, 68, 136, 245, 85, 144, 17, 163, 161, 10, 195, 145, 26, 88, 205, 255, 211, 19, 42, 132, 34, 230, 155, 148, 10, 173, 151, 182, 93, 50, 73, 126, 112, 119, 153, 116, 80, 198, 215, 82, 228, 9, 186, 90, 83, 85, 143, 155, 191, 109, 190, 84, 129, 178, 100, 228, 118]; - - assert!(bls12381_min_pk_verify( - &signature, - &pub_key_bytes, - &message), 0); - - // Make a new committee - let bls_committee = new_bls_committee( - vector[ - storage_node::new_for_testing(pub_key_bytes, 10) - ] - ); - - // actual committee - - let committee_at_5 : Committee = committee_for_testing_with_bls(5, bls_committee); - let cert = verify_quorum_in_epoch(&committee_at_5, signature, vector[0], message); - - assert!(committee::intent_type(&cert) == 1, 0); - - committee_at_5 - } - - #[test, expected_failure] - public fun test_incorrect_epoch() : Committee { - - let pub_key_bytes = vector[142, 78, 70, 3, 179, 142, 145, 75, 170, 36, 5, 232, 153, 164, 205, 57, 24, 216, 208, 34, 87, 213, 225, 76, 5, 157, 212, 88, 161, 34, 75, 145, 206, 144, 85, 11, 197, 110, 75, 175, 215, 194, 78, 51, 192, 196, 59, 204]; - let message = vector[1, 0, 3, 5, 0, 0, 0, 0, 0, 0, 0, 104, 101, 108, 108, 111]; - let signature = vector[173, 231, 27, 143, 41, 154, 49, 14, 85, 88, 187, 65, 86, 190, 161, 255, 219, 210, 78, 88, 179, 53, 11, 104, 168, 220, 40, 13, 91, 254, 191, 116, 161, 252, 196, 19, 24, 153, 126, 248, 68, 136, 245, 85, 144, 17, 163, 161, 10, 195, 145, 26, 88, 205, 255, 211, 19, 42, 132, 34, 230, 155, 148, 10, 173, 151, 182, 93, 50, 73, 126, 112, 119, 153, 116, 80, 198, 215, 82, 228, 9, 186, 90, 83, 85, 143, 155, 191, 109, 190, 84, 129, 178, 100, 228, 118]; - - assert!(bls12381_min_pk_verify( - &signature, - &pub_key_bytes, - &message), 0); - - // Make a new committee - let bls_committee = new_bls_committee( - vector[ - storage_node::new_for_testing(pub_key_bytes, 10), - ] - ); - - // actual committee - - // INCORRECT EPOCH - let committee_at_6 : Committee = committee_for_testing_with_bls(6, bls_committee); - let cert = verify_quorum_in_epoch(&committee_at_6, signature, vector[0], message); - - assert!(committee::intent_type(&cert) == 1, 0); - - committee_at_6 - } - -} diff --git a/contracts/blob_store/sources/tests/epoch_change_tests.move b/contracts/blob_store/sources/tests/epoch_change_tests.move deleted file mode 100644 index c74d577c..00000000 --- a/contracts/blob_store/sources/tests/epoch_change_tests.move +++ /dev/null @@ -1,288 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -#[test_only] -module blob_store::epoch_change_tests { - use sui::coin; - use sui::balance; - - use blob_store::committee; - use blob_store::system; - use blob_store::storage_accounting as sa; - use blob_store::storage_resource as sr; - - // Keep in sync with the same constant in `blob_store::system` - const BYTES_PER_UNIT_SIZE: u64 = 1_024; - - public struct TESTWAL has store, drop {} - - // ------------- TESTS -------------------- - - #[test] - public fun test_use_system(): system::System { - let mut ctx = tx_context::dummy(); - - // A test coin. - let fake_coin = coin::mint_for_testing(100, &mut ctx); - - // Create a new committee - let committee = committee::committee_for_testing(0); - - // Create a new system object - let mut system: system::System = system::new( - committee, - 1_000 * BYTES_PER_UNIT_SIZE, - 2, - &mut ctx, - ); - - // Get some space for a few epochs - let (storage, fake_coin) = system::reserve_space( - &mut system, - 10 * BYTES_PER_UNIT_SIZE, - 3, - fake_coin, - &mut ctx, - ); - sr::destroy(storage); - - // Check things about the system - assert!(system::epoch(&system) == 0, 0); - - // The value of the coin should be 100 - 60 - assert!(coin::value(&fake_coin) == 40, 0); - - // Space is reduced by 10 - assert!(system::used_capacity_size(&system) == 10 * BYTES_PER_UNIT_SIZE, 0); - - // Advance epoch -- to epoch 1 - let committee = committee::committee_for_testing(1); - let mut epoch_accounts = system::next_epoch( - &mut system, - committee, - 1_000 * BYTES_PER_UNIT_SIZE, - 3, - ); - assert!(balance::value(sa::rewards_to_distribute(&mut epoch_accounts)) == 20, 0); - - // Get some space for a few epochs - let (storage, fake_coin) = system::reserve_space( - &mut system, - 5 * BYTES_PER_UNIT_SIZE, - 1, - fake_coin, - &mut ctx, - ); - sr::destroy(storage); - // The value of the coin should be 40 - 3 x 5 - assert!(coin::value(&fake_coin) == 25, 0); - sa::burn_for_testing(epoch_accounts); - - assert!(system::used_capacity_size(&system) == 15 * BYTES_PER_UNIT_SIZE, 0); - - // Advance epoch -- to epoch 2 - system::set_done_for_testing(&mut system); - let committee = committee::committee_for_testing(2); - let mut epoch_accounts = system::next_epoch( - &mut system, - committee, - 1_000 * BYTES_PER_UNIT_SIZE, - 3, - ); - assert!(balance::value(sa::rewards_to_distribute(&mut epoch_accounts)) == 35, 0); - sa::burn_for_testing(epoch_accounts); - - assert!(system::used_capacity_size(&system) == 10 * BYTES_PER_UNIT_SIZE, 0); - - // Advance epoch -- to epoch 3 - system::set_done_for_testing(&mut system); - let committee = committee::committee_for_testing(3); - let mut epoch_accounts = system::next_epoch( - &mut system, - committee, - 1_000 * BYTES_PER_UNIT_SIZE, - 3, - ); - assert!(balance::value(sa::rewards_to_distribute(&mut epoch_accounts)) == 20, 0); - sa::burn_for_testing(epoch_accounts); - - // check all space is reclaimed - assert!(system::used_capacity_size(&system) == 0, 0); - - // Advance epoch -- to epoch 4 - system::set_done_for_testing(&mut system); - let committee = committee::committee_for_testing(4); - let mut epoch_accounts = system::next_epoch( - &mut system, - committee, - 1_000 * BYTES_PER_UNIT_SIZE, - 3, - ); - assert!(balance::value(sa::rewards_to_distribute(&mut epoch_accounts)) == 0, 0); - sa::burn_for_testing(epoch_accounts); - - // check all space is reclaimed - assert!(system::used_capacity_size(&system) == 0, 0); - - coin::burn_for_testing(fake_coin); - - system - } - - #[test, expected_failure(abort_code=system::ESyncEpochChange)] - public fun test_move_sync_err_system(): system::System { - let mut ctx = tx_context::dummy(); - - // A test coin. - let fake_coin = coin::mint_for_testing(100, &mut ctx); - - // Create a new committee - let committee = committee::committee_for_testing(0); - - // Create a new system object - let mut system: system::System = system::new(committee, 1000, 2, &mut ctx); - - // Advance epoch -- to epoch 1 - let committee = committee::committee_for_testing(1); - let epoch_accounts1 = system::next_epoch(&mut system, committee, 1000, 3); - - // Advance epoch -- to epoch 2 - let committee = committee::committee_for_testing(2); - // FAIL HERE BECAUSE WE ARE IN SYNC MODE NOT DONE! - let epoch_accounts2 = system::next_epoch(&mut system, committee, 1000, 3); - - coin::burn_for_testing(fake_coin); - sa::burn_for_testing(epoch_accounts1); - sa::burn_for_testing(epoch_accounts2); - - system - } - - #[test, expected_failure(abort_code=system::EStorageExceeded)] - public fun test_fail_capacity_system(): system::System { - let mut ctx = tx_context::dummy(); - - // A test coin. - let fake_coin = coin::mint_for_testing(100, &mut ctx); - - // Create a new committee - let committee = committee::committee_for_testing(0); - - // Create a new system object - let mut system: system::System = system::new(committee, 1000, 2, &mut ctx); - - // Get some space for a few epochs - let (storage, fake_coin) = system::reserve_space(&mut system, 10, 3, fake_coin, &mut ctx); - sr::destroy(storage); - - // Advance epoch -- to epoch 1 - let committee = committee::committee_for_testing(1); - let epoch_accounts = system::next_epoch(&mut system, committee, 1000, 3); - - // Get some space for a few epochs - let (storage, fake_coin) = system::reserve_space(&mut system, 995, 1, fake_coin, &mut ctx); - sr::destroy(storage); - // The value of the coin should be 40 - 3 x 5 - sa::burn_for_testing(epoch_accounts); - - coin::burn_for_testing(fake_coin); - - system - } - - #[test] - public fun test_sync_done_happy(): system::System { - let mut ctx = tx_context::dummy(); - - // A test coin. - let fake_coin = coin::mint_for_testing(100, &mut ctx); - - // Create a new committee - let committee = committee::committee_for_testing(0); - - // Create a new system object - let mut system: system::System = system::new(committee, 1000, 2, &mut ctx); - - // Advance epoch -- to epoch 1 - let committee = committee::committee_for_testing(1); - let epoch_accounts1 = system::next_epoch(&mut system, committee, 1000, 3); - - // Construct a test sync_done test message - let test_sync_done_msg = system::make_sync_done_message_for_testing(1); - - // Feed it into the logic to advance state - system::sync_done_for_epoch(&mut system, test_sync_done_msg); - - // Advance epoch -- to epoch 2 - let committee = committee::committee_for_testing(2); - // We are in done state and this works - let epoch_accounts2 = system::next_epoch(&mut system, committee, 1000, 3); - - coin::burn_for_testing(fake_coin); - sa::burn_for_testing(epoch_accounts1); - sa::burn_for_testing(epoch_accounts2); - - system - } - - #[test, expected_failure(abort_code=system::ESyncEpochChange)] - public fun test_sync_done_unhappy(): system::System { - let mut ctx = tx_context::dummy(); - - // A test coin. - let fake_coin = coin::mint_for_testing(100, &mut ctx); - - // Create a new committee - let committee = committee::committee_for_testing(0); - - // Create a new system object - let mut system: system::System = system::new(committee, 1000, 2, &mut ctx); - - // Advance epoch -- to epoch 1 - let committee = committee::committee_for_testing(1); - let epoch_accounts1 = system::next_epoch(&mut system, committee, 1000, 3); - - // Construct a test sync_done test message -- INCORRECT EPOCH - let test_sync_done_msg = system::make_sync_done_message_for_testing(4); - - // Feed it into the logic to advance state - system::sync_done_for_epoch(&mut system, test_sync_done_msg); - - coin::burn_for_testing(fake_coin); - sa::burn_for_testing(epoch_accounts1); - - system - } - - #[test, expected_failure(abort_code=system::ESyncEpochChange)] - public fun test_twice_unhappy(): system::System { - let mut ctx = tx_context::dummy(); - - // A test coin. - let fake_coin = coin::mint_for_testing(100, &mut ctx); - - // Create a new committee - let committee = committee::committee_for_testing(0); - - // Create a new system object - let mut system: system::System = system::new(committee, 1000, 2, &mut ctx); - - // Advance epoch -- to epoch 1 - let committee = committee::committee_for_testing(1); - let epoch_accounts1 = system::next_epoch(&mut system, committee, 1000, 3); - - // Construct a test sync_done test message - // Feed it into the logic to advance state - let test_sync_done_msg = system::make_sync_done_message_for_testing(1); - system::sync_done_for_epoch(&mut system, test_sync_done_msg); - - // SECOND TIME -- FAILS - let test_sync_done_msg = system::make_sync_done_message_for_testing(1); - system::sync_done_for_epoch(&mut system, test_sync_done_msg); - - coin::burn_for_testing(fake_coin); - sa::burn_for_testing(epoch_accounts1); - - system - } -} diff --git a/contracts/blob_store/sources/tests/invalid_tests.move b/contracts/blob_store/sources/tests/invalid_tests.move deleted file mode 100644 index 90765d54..00000000 --- a/contracts/blob_store/sources/tests/invalid_tests.move +++ /dev/null @@ -1,303 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -#[test_only] -module blob_store::invalid_tests { - - use std::string; - - use blob_store::committee; - use blob_store::system; - use blob_store::storage_node; - use blob_store::storage_accounting as sa; - - const NETWORK_PUBLIC_KEY: vector = - x"820e2b273530a00de66c9727c40f48be985da684286983f398ef7695b8a44677"; - public struct TESTWAL has store, drop {} - - - #[test] - public fun test_invalid_blob_ok() : committee::Committee { - - let blob_id : u256 = 0xabababababababababababababababababababababababababababababababab; - - // BCS confirmation message for epoch 0 and blob id `blob_id` with intents - let invalid_message : vector = vector[2, 0, 3, 5, 0, 0, 0, 0, 0, 0, 0, - 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, - 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171]; - - // Signature from private key scalar(117) on `invalid message` - let message_signature : vector = vector[ - 143, 92, 248, 128, 87, 79, 148, 183, 217, 204, 80, 23, 165, 20, 177, 244, 195, 58, 211, - 68, 96, 54, 23, 17, 187, 131, 69, 35, 243, 61, 209, 23, 11, 75, 236, 235, 199, 245, 53, - 10, 120, 47, 152, 39, 205, 152, 188, 230, 12, 213, 35, 133, 121, 27, 238, 80, 93, 35, - 241, 26, 55, 151, 38, 190, 131, 149, 149, 89, 134, 115, 85, 8, 133, 11, 220, 82, 100, - 14, 214, 146, 147, 200, 192, 155, 181, 143, 199, 38, 202, 125, 25, 22, 246, 117, 30, 82 - ]; - - // Create storage node - // Pk corresponding to secret key scalar(117) - let public_key : vector = vector[ - 149, 234, 204, 58, 220, 9, 200, 39, 89, 63, 88, 30, 142, 45, - 224, 104, 191, 76, 245, 208, 192, 235, 41, 229, 55, 47, 13, 35, 54, 71, 136, 238, 15, - 155, 235, 17, 44, 138, 126, 156, 47, 12, 114, 4, 51, 112, 92, 240]; - let storage_node = storage_node::create_storage_node_info( - string::utf8(b"node"), - string::utf8(b"127.0.0.1"), - public_key, - NETWORK_PUBLIC_KEY, - vector[0, 1, 2, 3, 4, 5] - ); - - // Create a new committee - let cap = committee::create_committee_cap_for_tests(); - let committee = committee::create_committee(&cap, 5, vector[storage_node]); - - let certified_message = committee::verify_quorum_in_epoch( - &committee, - message_signature, - vector[0], - invalid_message,); - - // Now check this is a invalid blob message - let invalid_blob = system::invalid_blob_id_message(certified_message); - assert!(system::invalid_blob_id(&invalid_blob) == blob_id, 0); - - committee - } - - #[test] - public fun test_system_invalid_id_happy() : system::System { - - let mut ctx = tx_context::dummy(); - - // Create storage node - // Pk corresponding to secret key scalar(117) - let public_key : vector = vector[ - 149, 234, 204, 58, 220, 9, 200, 39, 89, 63, 88, 30, 142, 45, - 224, 104, 191, 76, 245, 208, 192, 235, 41, 229, 55, 47, 13, 35, 54, 71, 136, 238, 15, - 155, 235, 17, 44, 138, 126, 156, 47, 12, 114, 4, 51, 112, 92, 240]; - let storage_node = storage_node::create_storage_node_info( - string::utf8(b"node"), - string::utf8(b"127.0.0.1"), - public_key, - NETWORK_PUBLIC_KEY, - vector[0, 1, 2, 3, 4, 5] - ); - - // Create a new committee - let cap = committee::create_committee_cap_for_tests(); - let committee = committee::create_committee(&cap, 0, vector[storage_node]); - - // Create a new system object - let mut system : system::System = system::new(committee, - 1000000000, 5, &mut ctx); - - let mut epoch = 0; - - loop { - - epoch = epoch + 1; - - let storage_node = storage_node::create_storage_node_info( - string::utf8(b"node"), - string::utf8(b"127.0.0.1"), - public_key, - NETWORK_PUBLIC_KEY, - vector[0, 1, 2, 3, 4, 5] - ); - let committee = committee::create_committee(&cap, epoch, vector[storage_node]); - let epoch_accounts = system::next_epoch(&mut system, committee, 1000000000, 5); - system::set_done_for_testing(&mut system); - sa::burn_for_testing(epoch_accounts); - - if (epoch == 5) { - break - } - - }; - - let certified_message = committee::certified_message_for_testing( - 2, // Intent type - 0, // Intent version - 5, // Epoch - 6, // Stake support - // Data - vector[171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, - 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171], - - ); - - let blob_id : u256 = 0xabababababababababababababababababababababababababababababababab; - - // Now check this is a invalid blob message - let invalid_blob = system::invalid_blob_id_message(certified_message); - assert!(system::invalid_blob_id(&invalid_blob) == blob_id, 0); - - // Now use the system to check the invalid blob - system::inner_declare_invalid_blob_id(&system, invalid_blob); - - system - } - - #[test] - public fun test_invalidate_happy() : system::System { - - let mut ctx = tx_context::dummy(); - - // Create storage node - // Pk corresponding to secret key scalar(117) - let public_key : vector = vector[ - 149, 234, 204, 58, 220, 9, 200, 39, 89, 63, 88, 30, 142, 45, - 224, 104, 191, 76, 245, 208, 192, 235, 41, 229, 55, 47, 13, 35, 54, 71, 136, 238, 15, - 155, 235, 17, 44, 138, 126, 156, 47, 12, 114, 4, 51, 112, 92, 240]; - let storage_node = storage_node::create_storage_node_info( - string::utf8(b"node"), - string::utf8(b"127.0.0.1"), - public_key, - NETWORK_PUBLIC_KEY, - vector[0, 1, 2, 3, 4, 5] - ); - - // Create a new committee - let cap = committee::create_committee_cap_for_tests(); - let committee = committee::create_committee(&cap, 0, vector[storage_node]); - - // Create a new system object - let mut system : system::System = system::new(committee, - 1000000000, 5, &mut ctx); - - let mut epoch = 0; - - loop { - - epoch = epoch + 1; - - let storage_node = storage_node::create_storage_node_info( - string::utf8(b"node"), - string::utf8(b"127.0.0.1"), - public_key, - NETWORK_PUBLIC_KEY, - vector[0, 1, 2, 3, 4, 5] - ); - let committee = committee::create_committee(&cap, epoch, vector[storage_node]); - let epoch_accounts = system::next_epoch(&mut system, committee, 1000000000, 5); - system::set_done_for_testing(&mut system); - sa::burn_for_testing(epoch_accounts); - - if (epoch == 5) { - break - } - - }; - - // BCS confirmation message for epoch 0 and blob id `blob_id` with intents - let invalid_message : vector = vector[2, 0, 3, 5, 0, 0, 0, 0, 0, 0, 0, - 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, - 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171]; - - // Signature from private key scalar(117) on `invalid message` - let message_signature : vector = vector[ - 143, 92, 248, 128, 87, 79, 148, 183, 217, 204, 80, 23, 165, 20, 177, 244, 195, 58, 211, - 68, 96, 54, 23, 17, 187, 131, 69, 35, 243, 61, 209, 23, 11, 75, 236, 235, 199, 245, 53, - 10, 120, 47, 152, 39, 205, 152, 188, 230, 12, 213, 35, 133, 121, 27, 238, 80, 93, 35, - 241, 26, 55, 151, 38, 190, 131, 149, 149, 89, 134, 115, 85, 8, 133, 11, 220, 82, 100, - 14, 214, 146, 147, 200, 192, 155, 181, 143, 199, 38, 202, 125, 25, 22, 246, 117, 30, 82 - ]; - - - - let expected_blob_id : u256 - = 0xabababababababababababababababababababababababababababababababab; - - // Now check this is a invalid blob message - let blob_id = system::invalidate_blob_id( - &system, - message_signature, - vector[0], - invalid_message); - - assert!(blob_id == expected_blob_id, 0); - - system - } - - - #[test, expected_failure(abort_code=system::EInvalidIdEpoch)] - public fun test_system_invalid_id_wrong_epoch() : system::System { - - let mut ctx = tx_context::dummy(); - - // Create storage node - // Pk corresponding to secret key scalar(117) - let public_key : vector = vector[ - 149, 234, 204, 58, 220, 9, 200, 39, 89, 63, 88, 30, 142, 45, - 224, 104, 191, 76, 245, 208, 192, 235, 41, 229, 55, 47, 13, 35, 54, 71, 136, 238, 15, - 155, 235, 17, 44, 138, 126, 156, 47, 12, 114, 4, 51, 112, 92, 240]; - let storage_node = storage_node::create_storage_node_info( - string::utf8(b"node"), - string::utf8(b"127.0.0.1"), - public_key, - NETWORK_PUBLIC_KEY, - vector[0, 1, 2, 3, 4, 5] - ); - - // Create a new committee - let cap = committee::create_committee_cap_for_tests(); - let committee = committee::create_committee(&cap, 0, vector[storage_node]); - - // Create a new system object - let mut system : system::System = system::new(committee, - 1000000000, 5, &mut ctx); - - let mut epoch = 0; - - loop { - - epoch = epoch + 1; - - let storage_node = storage_node::create_storage_node_info( - string::utf8(b"node"), - string::utf8(b"127.0.0.1"), - public_key, - NETWORK_PUBLIC_KEY, - vector[0, 1, 2, 3, 4, 5] - ); - let committee = committee::create_committee(&cap, epoch, vector[storage_node]); - let epoch_accounts = system::next_epoch(&mut system, committee, 1000000000, 5); - system::set_done_for_testing(&mut system); - sa::burn_for_testing(epoch_accounts); - - if (epoch == 5) { - break - } - - }; - - let certified_message = committee::certified_message_for_testing( - 2, // Intent type - 0, // Intent version - 50, // Epoch WRONG - 6, // Stake support - // Data - vector[171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, - 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171], - - ); - - let blob_id : u256 = 0xabababababababababababababababababababababababababababababababab; - - // Now check this is a invalid blob message - let invalid_blob = system::invalid_blob_id_message(certified_message); - assert!(system::invalid_blob_id(&invalid_blob) == blob_id, 0); - - // Now use the system to check the invalid blob - // BLOWS UP HERE DUE TO WRONG EPOCH - system::inner_declare_invalid_blob_id(&system, invalid_blob); - - system - } - - - -} diff --git a/contracts/blob_store/sources/tests/ringbuffer_tests.move b/contracts/blob_store/sources/tests/ringbuffer_tests.move deleted file mode 100644 index 6e820d19..00000000 --- a/contracts/blob_store/sources/tests/ringbuffer_tests.move +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -#[test_only] -module blob_store::ringbuffer_tests { - public struct TESTCOIN has store, drop {} - - use blob_store::storage_accounting as sa; - use blob_store::storage_accounting::FutureAccountingRingBuffer; - - // ------------- TESTS -------------------- - - #[test] - public fun test_basic_ring_buffer(): FutureAccountingRingBuffer { - let mut buffer: FutureAccountingRingBuffer = sa::ring_new(3); - - assert!(sa::epoch(sa::ring_lookup_mut(&mut buffer, 0)) == 0, 100); - assert!(sa::epoch(sa::ring_lookup_mut(&mut buffer, 1)) == 1, 100); - assert!(sa::epoch(sa::ring_lookup_mut(&mut buffer, 2)) == 2, 100); - - let entry = sa::ring_pop_expand(&mut buffer); - assert!(sa::epoch(&entry) == 0, 100); - sa::delete_empty_future_accounting(entry); - - let entry = sa::ring_pop_expand(&mut buffer); - assert!(sa::epoch(&entry) == 1, 100); - sa::delete_empty_future_accounting(entry); - - assert!(sa::epoch(sa::ring_lookup_mut(&mut buffer, 0)) == 2, 100); - assert!(sa::epoch(sa::ring_lookup_mut(&mut buffer, 1)) == 3, 100); - assert!(sa::epoch(sa::ring_lookup_mut(&mut buffer, 2)) == 4, 100); - - buffer - } - - #[test, expected_failure] - public fun test_oob_fail_ring_buffer(): FutureAccountingRingBuffer { - let mut buffer: FutureAccountingRingBuffer = sa::ring_new(3); - - sa::epoch(sa::ring_lookup_mut(&mut buffer, 3)); - - buffer - } -} diff --git a/contracts/blob_store/sources/tests/storage_resource_tests.move b/contracts/blob_store/sources/tests/storage_resource_tests.move deleted file mode 100644 index 5a0b64cd..00000000 --- a/contracts/blob_store/sources/tests/storage_resource_tests.move +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -#[test_only] -module blob_store::storage_resource_tests { - use blob_store::storage_resource::{ - fuse, - split_by_epoch, - split_by_size, - create_for_test, - destroy, - start_epoch, - end_epoch, - storage_size, - EInvalidEpoch, - EIncompatibleAmount, - EIncompatibleEpochs, - }; - - #[test] - public fun test_split_epoch() { - let ctx = &mut tx_context::dummy(); - let storage_amount = 5_000_000; - let mut storage = create_for_test(0, 10, storage_amount, ctx); - let new_storage = split_by_epoch(&mut storage, 7, ctx); - assert!( - start_epoch(&storage) == 0 && end_epoch(&storage) == 7 && - start_epoch(&new_storage) == 7 && - end_epoch(&new_storage) == 10, - 0, - ); - assert!( - storage_size(&storage) == storage_amount && - storage_size(&new_storage) == storage_amount, - 0, - ); - destroy(storage); - destroy(new_storage); - } - - #[test] - public fun test_split_size() { - let ctx = &mut tx_context::dummy(); - let mut storage = create_for_test(0, 10, 5_000_000, ctx); - let new_storage = split_by_size(&mut storage, 1_000_000, ctx); - assert!( - start_epoch(&storage) == 0 && end_epoch(&storage) == 10 && - start_epoch(&new_storage) == 0 && - end_epoch(&new_storage) == 10, - 0, - ); - assert!(storage_size(&storage) == 1_000_000 && storage_size(&new_storage) == 4_000_000, 0); - destroy(storage); - destroy(new_storage); - } - - #[test] - #[expected_failure(abort_code=EInvalidEpoch)] - public fun test_split_epoch_invalid_end() { - let ctx = &mut tx_context::dummy(); - let mut storage = create_for_test(0, 10, 5_000_000, ctx); - let new_storage = split_by_epoch(&mut storage, 11, ctx); - destroy(storage); - destroy(new_storage); - } - - #[test] - #[expected_failure(abort_code=EInvalidEpoch)] - public fun test_split_epoch_invalid_start() { - let ctx = &mut tx_context::dummy(); - let mut storage = create_for_test(1, 10, 5_000_000, ctx); - let new_storage = split_by_epoch(&mut storage, 0, ctx); - destroy(storage); - destroy(new_storage); - } - - #[test] - public fun test_fuse_size() { - let ctx = &mut tx_context::dummy(); - let mut first = create_for_test(0, 10, 1_000_000, ctx); - let second = create_for_test(0, 10, 2_000_000, ctx); - fuse(&mut first, second); - assert!(start_epoch(&first) == 0 && end_epoch(&first) == 10, 0); - assert!(storage_size(&first) == 3_000_000, 0); - destroy(first); - } - - #[test] - public fun test_fuse_epochs() { - let ctx = &mut tx_context::dummy(); - let mut first = create_for_test(0, 5, 1_000_000, ctx); - let second = create_for_test(5, 10, 1_000_000, ctx); - // list the `earlier` resource first - fuse(&mut first, second); - assert!(start_epoch(&first) == 0 && end_epoch(&first) == 10, 0); - assert!(storage_size(&first) == 1_000_000, 0); - - let mut second = create_for_test(10, 15, 1_000_000, ctx); - // list the `latter` resource first - fuse(&mut second, first); - assert!(start_epoch(&second) == 0 && end_epoch(&second) == 15, 0); - assert!(storage_size(&second) == 1_000_000, 0); - destroy(second); - } - - #[test] - #[expected_failure(abort_code=EIncompatibleAmount)] - public fun test_fuse_incompatible_size() { - let ctx = &mut tx_context::dummy(); - let mut first = create_for_test(0, 5, 1_000_000, ctx); - let second = create_for_test(5, 10, 2_000_000, ctx); - fuse(&mut first, second); - destroy(first); - } - - #[test] - #[expected_failure(abort_code=EIncompatibleEpochs)] - public fun test_fuse_incompatible_epochs() { - let ctx = &mut tx_context::dummy(); - let mut first = create_for_test(0, 6, 1_000_000, ctx); - let second = create_for_test(5, 10, 1_000_000, ctx); - fuse(&mut first, second); - destroy(first); - } -} diff --git a/contracts/walrus/Move.lock b/contracts/walrus/Move.lock new file mode 100644 index 00000000..bc645eec --- /dev/null +++ b/contracts/walrus/Move.lock @@ -0,0 +1,45 @@ +# @generated by Move, please check-in and do not edit manually. + +[move] +version = 3 +manifest_digest = "A8705FC4DA1640884D71727CDCE9E5C95451481B9AEAEFF0DF4EBC83042B042A" +deps_digest = "060AD7E57DFB13104F21BE5F5C3759D03F0553FC3229247D9A7A6B45F50D03A3" +dependencies = [ + { id = "Sui", name = "Sui" }, + { id = "WAL", name = "WAL" }, + { id = "WAL_exchange", name = "WAL_exchange" }, +] + +[[move.package]] +id = "MoveStdlib" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "testnet-v1.35.0", subdir = "crates/sui-framework/packages/move-stdlib" } + +[[move.package]] +id = "Sui" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "testnet-v1.35.0", subdir = "crates/sui-framework/packages/sui-framework" } + +dependencies = [ + { id = "MoveStdlib", name = "MoveStdlib" }, +] + +[[move.package]] +id = "WAL" +source = { local = "../wal" } + +dependencies = [ + { id = "Sui", name = "Sui" }, +] + +[[move.package]] +id = "WAL_exchange" +source = { local = "../wal_exchange" } + +dependencies = [ + { id = "Sui", name = "Sui" }, + { id = "WAL", name = "WAL" }, +] + +[move.toolchain-version] +compiler-version = "1.35.0" +edition = "2024.beta" +flavor = "sui" diff --git a/contracts/blob_store/Move.toml b/contracts/walrus/Move.toml similarity index 50% rename from contracts/blob_store/Move.toml rename to contracts/walrus/Move.toml index 4855e94f..aec968ed 100644 --- a/contracts/blob_store/Move.toml +++ b/contracts/walrus/Move.toml @@ -1,10 +1,13 @@ [package] -name = "blob_store" +name = "Walrus" +license = "Apache-2.0" +authors = ["Mysten Labs "] edition = "2024.beta" [dependencies] -Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "testnet-v1.31.1" } +Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "testnet-v1.35.0" } +WAL = { local = "../wal" } +WAL_exchange = { local = "../wal_exchange" } [addresses] -blob_store = "0x0" -sui = "0x2" +walrus = "0x0" diff --git a/contracts/blob_store/README.md b/contracts/walrus/README.md similarity index 62% rename from contracts/blob_store/README.md rename to contracts/walrus/README.md index 3ee9bae6..f80eaa07 100644 --- a/contracts/blob_store/README.md +++ b/contracts/walrus/README.md @@ -1,11 +1,11 @@ # Walrus Testnet Move contracts -> TODO: directory still contains the Devnet contracts, these and the package ID below need to be -> updated for Testnet. + This is the Move source code for the Walrus Testnet instance. We provide this so developers can -experiment with building Walrus apps that require Move extensions. This code is published on Sui -Testnet at package ID `0x7e12d67a52106ddd5f26c6ff4fe740ba5dea7cfc138d5b1d33863ba9098aa6fe`. +experiment with building Walrus apps that require Move extensions. A slightly different version of +these contracts is deployed on Sui Testnet as package +`0x668fb342c7ea45a3a8d645efefbb41d6b732a5fd4ead552f58df7fabe443c12e`. **A word of caution:** Walrus Mainnet will use new Move packages with struct layouts and function signatures that may not be compatible with this package. Move code that builds against this package diff --git a/contracts/blob_store/docs/msg_formats.txt b/contracts/walrus/docs/msg_formats.txt similarity index 82% rename from contracts/blob_store/docs/msg_formats.txt rename to contracts/walrus/docs/msg_formats.txt index 5a4bacfa..48db5879 100644 --- a/contracts/blob_store/docs/msg_formats.txt +++ b/contracts/walrus/docs/msg_formats.txt @@ -16,9 +16,9 @@ Signatures are 96 byte vector. Signed Message Header --------------------- -All messages MUST start with a header of 3 + 8 bytes: +All messages MUST start with a header of 3 + 4 bytes: - (Intent_type, Intent_version, Intent_app_id) : (u8, u8, u8) -- epoch: u64 +- epoch: u32 - body: remaining vec, no length prefix The intent types are enumerated below, the version is as of now 0, @@ -27,14 +27,15 @@ signing storage node is in when signing the message. Intent types (add here): -const SYNC_DONE_MSG_TYPE: u8 = 0; +const PROOF_OF_POSSESSION_MSG_TYPE: u8 = 0; const BLOB_CERT_MSG_TYPE: u8 = 1; const INVALID_BLOB_ID_MSG_TYPE : u8 = 2; -SYNC_DONE message +PROOF_OF_POSSESSION message ----------------- -The body is empty, and the epoch for which sync is done is the epoch in the header. +The body contains the sui address followed by the bls public key (encoded as 48 byte fixed +size array) of the signer. BLOB_CERT message diff --git a/contracts/walrus/sources/init.move b/contracts/walrus/sources/init.move new file mode 100644 index 00000000..a73665d1 --- /dev/null +++ b/contracts/walrus/sources/init.move @@ -0,0 +1,49 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module walrus::init; + +use sui::clock::Clock; +use walrus::{staking, system}; + +/// Must only be created by `init`. +public struct InitCap has key, store { + id: UID, +} + +/// Init function, creates an init cap and transfers it to the sender. +/// This allows the sender to call the function to actually initialize the system +/// with the corresponding parameters. Once that function is called, the cap is destroyed. +fun init(ctx: &mut TxContext) { + let id = object::new(ctx); + let init_cap = InitCap { id }; + transfer::transfer(init_cap, ctx.sender()); +} + +/// Function to initialize walrus and share the system and staking objects. +/// This can only be called once, after which the `InitCap` is destroyed. +public fun initialize_walrus( + cap: InitCap, + epoch_zero_duration: u64, + epoch_duration: u64, + n_shards: u16, + max_epochs_ahead: u32, + clock: &Clock, + ctx: &mut TxContext, +) { + system::create_empty(max_epochs_ahead, ctx); + staking::create(epoch_zero_duration, epoch_duration, n_shards, clock, ctx); + cap.destroy(); +} + +fun destroy(cap: InitCap) { + let InitCap { id } = cap; + id.delete(); +} + +// === Test only === + +#[test_only] +public fun init_for_testing(ctx: &mut TxContext) { + init(ctx); +} diff --git a/contracts/walrus/sources/staking.move b/contracts/walrus/sources/staking.move new file mode 100644 index 00000000..69e99336 --- /dev/null +++ b/contracts/walrus/sources/staking.move @@ -0,0 +1,280 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[allow(unused_variable, unused_function, unused_field, unused_mut_parameter)] +/// Module: staking +module walrus::staking; + +use std::string::String; +use sui::{clock::Clock, coin::Coin, dynamic_object_field as df}; +use wal::wal::WAL; +use walrus::{ + staked_wal::StakedWal, + staking_inner::{Self, StakingInnerV1}, + storage_node::{Self, StorageNodeCap}, + system::System +}; + +/// Flag to indicate the version of the Walrus system. +const VERSION: u64 = 0; + +/// The one and only staking object. +public struct Staking has key { + id: UID, + version: u64, +} + +/// Creates and shares a new staking object. +/// Must only be called by the initialization function. +public(package) fun create( + epoch_zero_duration: u64, + epoch_duration: u64, + n_shards: u16, + clock: &Clock, + ctx: &mut TxContext, +) { + let mut staking = Staking { id: object::new(ctx), version: VERSION }; + df::add( + &mut staking.id, + VERSION, + staking_inner::new( + epoch_zero_duration, + epoch_duration, + n_shards, + clock, + ctx, + ), + ); + transfer::share_object(staking) +} + +// === Public API: Storage Node === + +/// Creates a staking pool for the candidate, registers the candidate as a storage node. +public fun register_candidate( + staking: &mut Staking, + // node info + name: String, + network_address: String, + public_key: vector, + network_public_key: vector, + proof_of_possession: vector, + // voting parameters + commission_rate: u64, + storage_price: u64, + write_price: u64, + node_capacity: u64, + ctx: &mut TxContext, +): StorageNodeCap { + // use the Pool Object ID as the identifier of the storage node + let node_id = staking + .inner_mut() + .create_pool( + name, + network_address, + public_key, + network_public_key, + proof_of_possession, + commission_rate, + storage_price, + write_price, + node_capacity, + ctx, + ); + + storage_node::new_cap(node_id, ctx) +} + +/// Blocks staking for the nodes staking pool +/// Marks node as "withdrawing", +/// - excludes it from the next committee selection +/// - still has to remain active while it is part of the committee and until all shards have +/// been transferred to its successor +/// - The staking pool is deleted once the last funds have been withdrawn from it by its stakers +public fun withdraw_node(staking: &mut Staking, cap: &mut StorageNodeCap) { + staking.inner_mut().set_withdrawing(cap.node_id()); + staking.inner_mut().withdraw_node(cap); +} + +/// Sets next_commission in the staking pool, which will then take effect as commission rate +/// one epoch after setting the value (to allow stakers to react to setting this). +public fun set_next_commission(staking: &mut Staking, cap: &StorageNodeCap, commission_rate: u64) { + staking.inner_mut().set_next_commission(cap, commission_rate); +} + +/// Returns the accumulated commission for the storage node. +public fun collect_commission(staking: &mut Staking, cap: &StorageNodeCap): Coin { + staking.inner_mut().collect_commission(cap) +} + +// === Voting === + +/// Sets the storage price vote for the pool. +public fun set_storage_price_vote(self: &mut Staking, cap: &StorageNodeCap, storage_price: u64) { + self.inner_mut().set_storage_price_vote(cap, storage_price); +} + +/// Sets the write price vote for the pool. +public fun set_write_price_vote(self: &mut Staking, cap: &StorageNodeCap, write_price: u64) { + self.inner_mut().set_write_price_vote(cap, write_price); +} + +/// Sets the node capacity vote for the pool. +public fun set_node_capacity_vote(self: &mut Staking, cap: &StorageNodeCap, node_capacity: u64) { + self.inner_mut().set_node_capacity_vote(cap, node_capacity); +} + +// === Update Node Parameters === + +/// Sets the public key of a node to be used starting from the next epoch for which the node is +/// selected. +public fun set_next_public_key( + self: &mut Staking, + cap: &StorageNodeCap, + public_key: vector, + proof_of_possession: vector, + ctx: &mut TxContext, +) { + self.inner_mut().set_next_public_key(cap, public_key, proof_of_possession, ctx); +} + +/// Sets the name of a storage node. +public fun set_name(self: &mut Staking, cap: &StorageNodeCap, name: String) { + self.inner_mut().set_name(cap, name); +} + +/// Sets the network address or host of a storage node. +public fun set_network_address(self: &mut Staking, cap: &StorageNodeCap, network_address: String) { + self.inner_mut().set_network_address(cap, network_address); +} + +/// Sets the public key used for TLS communication for a node. +public fun set_network_public_key( + self: &mut Staking, + cap: &StorageNodeCap, + network_public_key: vector, +) { + self.inner_mut().set_network_public_key(cap, network_public_key); +} + +// === Epoch Change === + +/// Ends the voting period and runs the apportionment if the current time allows. +/// Permissionless, can be called by anyone. +/// Emits: `EpochParametersSelected` event. +public fun voting_end(staking: &mut Staking, clock: &Clock) { + staking.inner_mut().voting_end(clock) +} + +/// Initiates the epoch change if the current time allows. +/// Emits: `EpochChangeStart` event. +public fun initiate_epoch_change(staking: &mut Staking, system: &mut System, clock: &Clock) { + let staking_inner = staking.inner_mut(); + let rewards = system.advance_epoch( + staking_inner.next_bls_committee(), + staking_inner.next_epoch_params(), + ); + + staking_inner.initiate_epoch_change(clock, rewards); +} + +/// Checks if the node should either have received the specified shards from the specified node +/// or vice-versa. +/// +/// - also checks that for the provided shards, this function has not been called before +/// - if so, slashes both nodes and emits an event that allows the receiving node to start +/// shard recovery +public fun shard_transfer_failed( + staking: &mut Staking, + cap: &StorageNodeCap, + other_node_id: ID, + shard_ids: vector, +) { + staking.inner_mut().shard_transfer_failed(cap, other_node_id, shard_ids); +} + +/// Signals to the contract that the node has received all its shards for the new epoch. +public fun epoch_sync_done( + staking: &mut Staking, + cap: &mut StorageNodeCap, + epoch: u32, + clock: &Clock, +) { + staking.inner_mut().epoch_sync_done(cap, epoch, clock); +} + +// === Public API: Staking === + +/// Stake `Coin` with the staking pool. +public fun stake_with_pool( + staking: &mut Staking, + to_stake: Coin, + node_id: ID, + ctx: &mut TxContext, +): StakedWal { + staking.inner_mut().stake_with_pool(to_stake, node_id, ctx) +} + +/// Marks the amount as a withdrawal to be processed and removes it from the stake weight of the +/// node. Allows the user to call withdraw_stake after the epoch change to the next epoch and +/// shard transfer is done. +public fun request_withdraw_stake( + staking: &mut Staking, + staked_wal: &mut StakedWal, + ctx: &mut TxContext, +) { + staking.inner_mut().request_withdraw_stake(staked_wal, ctx); +} + +#[allow(lint(self_transfer))] +/// Withdraws the staked amount from the staking pool. +public fun withdraw_stake( + staking: &mut Staking, + staked_wal: StakedWal, + ctx: &mut TxContext, +): Coin { + staking.inner_mut().withdraw_stake(staked_wal, ctx) +} + +// === Internals === + +/// Get a mutable reference to `StakingInner` from the `Staking`. +fun inner_mut(staking: &mut Staking): &mut StakingInnerV1 { + assert!(staking.version == VERSION); + df::borrow_mut(&mut staking.id, VERSION) +} + +/// Get an immutable reference to `StakingInner` from the `Staking`. +fun inner(staking: &Staking): &StakingInnerV1 { + assert!(staking.version == VERSION); + df::borrow(&staking.id, VERSION) +} + +// === Tests === + +#[test_only] +use sui::clock; + +#[test_only] +public(package) fun inner_for_testing(staking: &Staking): &StakingInnerV1 { + staking.inner() +} + +#[test_only] +public(package) fun new_for_testing(ctx: &mut TxContext): Staking { + let clock = clock::create_for_testing(ctx); + let mut staking = Staking { id: object::new(ctx), version: VERSION }; + df::add(&mut staking.id, VERSION, staking_inner::new(0, 10, 1000, &clock, ctx)); + clock.destroy_for_testing(); + staking +} + +#[test_only] +public(package) fun is_epoch_sync_done(self: &Staking): bool { + self.inner().is_epoch_sync_done() +} + +#[test_only] +fun new_id(ctx: &mut TxContext): ID { + ctx.fresh_object_address().to_id() +} diff --git a/contracts/walrus/sources/staking/active_set.move b/contracts/walrus/sources/staking/active_set.move new file mode 100644 index 00000000..206ae80a --- /dev/null +++ b/contracts/walrus/sources/staking/active_set.move @@ -0,0 +1,281 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Contains an active set of storage nodes. The active set is a smart collection +/// that only stores up to a 1000 nodes. The nodes are sorted by the amount of +/// staked WAL. Additionally, the active set tracks the total amount of staked +/// WAL to make the calculation of the rewards and voting power distribution easier. +module walrus::active_set; + +public struct ActiveSetEntry has store, copy, drop { + node_id: ID, + staked_amount: u64, +} + +/// The active set of storage nodes, a smart collection that only stores up +/// to a 1000 nodes. The nodes are sorted by the amount of staked WAL. +/// Additionally, the active set tracks the total amount of staked WAL to make +/// the calculation of the rewards and voting power distribution easier. +public struct ActiveSet has store, copy, drop { + /// The maximum number of storage nodes in the active set. + /// Potentially remove this field. + max_size: u16, + /// The minimum amount of staked WAL needed to enter the active set. This is used to + /// determine if a storage node can be added to the active set. + threshold_stake: u64, + /// The list of storage nodes in the active set and their stake. + nodes: vector, + /// The total amount of staked WAL in the active set. + total_stake: u64, +} + +/// Creates a new active set with the given `size` and `threshold_stake`. The +/// latter is used to filter out storage nodes that do not have enough staked +/// WAL to be included in the active set initially. +public(package) fun new(max_size: u16, threshold_stake: u64): ActiveSet { + assert!(max_size > 0); + ActiveSet { + max_size, + threshold_stake, + nodes: vector[], + total_stake: 0, + } +} + +/// Inserts the node if it is not already in the active set, otherwise updates its stake. +/// Returns true if the node is in the set after the operation, false otherwise. +public(package) fun insert_or_update(set: &mut ActiveSet, node_id: ID, staked_amount: u64): bool { + if (set.update(node_id, staked_amount)) { + return true + }; + set.insert(node_id, staked_amount) +} + +/// Updates the staked amount of the storage node with the given `node_id` in +/// the active set. Returns true if the node is in the set. +public(package) fun update(set: &mut ActiveSet, node_id: ID, staked_amount: u64): bool { + let index = set.nodes.find_index!(|entry| entry.node_id == node_id); + if (index.is_none()) { + return false + }; + index.do!(|idx| { + set.total_stake = set.total_stake - set.nodes[idx].staked_amount + staked_amount; + set.nodes[idx].staked_amount = staked_amount; + }); + true +} + +/// Inserts a storage node with the given `node_id` and `staked_amount` into the +/// active set. The node is only added if it has enough staked WAL to be included +/// in the active set. If the active set is full, the node with the smallest +/// staked WAL is removed to make space for the new node. +/// Returns true if the node was inserted, false otherwise. +public(package) fun insert(set: &mut ActiveSet, node_id: ID, staked_amount: u64): bool { + assert!(set.nodes.find_index!(|entry| entry.node_id == node_id ).is_none()); + + // Check if the staked amount is enough to be included in the active set. + if (staked_amount < set.threshold_stake) return false; + + // If the nodes are less than the max size, insert the node. + if (set.nodes.length() as u16 < set.max_size) { + set.total_stake = set.total_stake + staked_amount; + set.nodes.push_back(ActiveSetEntry {node_id, staked_amount}); + true + } else { + // Find the node with the smallest amount of stake and less than the new node. + let mut min_stake = staked_amount; + let mut min_idx = option::none(); + set.nodes.length().do!(|i| { + if (set.nodes[i].staked_amount < min_stake) { + min_idx = option::some(i); + min_stake = set.nodes[i].staked_amount; + } + }); + // If there is such a node, replace it in the list. + if (min_idx.is_some()) { + let min_idx = min_idx.extract(); + set.total_stake = set.total_stake - min_stake + staked_amount; + *&mut set.nodes[min_idx] = ActiveSetEntry { node_id, staked_amount }; + true + } else { + false + } + } +} + +/// Removes the storage node with the given `node_id` from the active set. +public(package) fun remove(set: &mut ActiveSet, node_id: ID) { + let index = set.nodes.find_index!(|entry| entry.node_id == node_id); + index.do!(|idx| { + let entry = set.nodes.swap_remove(idx); + set.total_stake = set.total_stake - entry.staked_amount; + }); +} + +/// The maximum size of the active set. +public(package) fun max_size(set: &ActiveSet): u16 { set.max_size } + +/// The current size of the active set. +public(package) fun size(set: &ActiveSet): u16 { set.nodes.length() as u16 } + +/// The IDs of the nodes in the active set. +public(package) fun active_ids(set: &ActiveSet): vector { + set.nodes.map_ref!(|node| node.node_id) +} + +/// The IDs and stake of the nodes in the active set. +public(package) fun active_ids_and_stake(set: &ActiveSet): (vector, vector) { + let mut active_ids = vector[]; + let mut stake = vector[]; + set.nodes.do_ref!(|entry| { + active_ids.push_back(entry.node_id); + stake.push_back(entry.staked_amount); + }); + (active_ids, stake) +} + +/// The minimum amount of staked WAL in the active set. +public(package) fun threshold_stake(set: &ActiveSet): u64 { set.threshold_stake } + +/// The total amount of staked WAL in the active set. +public(package) fun total_stake(set: &ActiveSet): u64 { set.total_stake } + +/// Current minimum stake needed to be in the active set. +/// If the active set is full, the minimum stake is the stake of the node with the smallest stake. +/// Otherwise, the minimum stake is the threshold stake. +/// Test only to discourage using this since it iterates over all nodes. When the `min_stake` is +/// needed within [`ActiveSet`], prefer inlining/integrating it in other loops. +#[test_only] +public(package) fun cur_min_stake(set: &ActiveSet): u64 { + if (set.nodes.length() == set.max_size as u64) { + let mut min_stake = std::u64::max_value!(); + set.nodes.length().do!(|i| { + if (set.nodes[i].staked_amount < min_stake) { + min_stake = set.nodes[i].staked_amount; + } + }); + min_stake + } else { + set.threshold_stake + } +} + +#[test_only] +public fun stake_for_node(set: &ActiveSet, node_id: ID): u64 { + set.nodes.find_index!(|entry| entry.node_id == node_id) + .map!(|index| set.nodes[index].staked_amount ).destroy_with_default(0) +} + +// === Test === + +#[test] +fun test_evict_correct_node_simple() { + let mut set = new(5, 0); + set.insert_or_update(object::id_from_address(@1), 10); + set.insert_or_update(object::id_from_address(@2), 9); + set.insert_or_update(object::id_from_address(@3), 8); + set.insert_or_update(object::id_from_address(@4), 7); + set.insert_or_update(object::id_from_address(@5), 6); + + let mut total_stake = 10 + 9 + 8 + 7 + 6; + + assert!(set.total_stake == total_stake); + + // insert another node which should eject node 5 + set.insert_or_update(object::id_from_address(@6), 11); + + // check if total stake was updated correctly + total_stake = total_stake - 6 + 11; + assert!(set.total_stake == total_stake); + + let active_ids = set.active_ids(); + + // node 5 should not be part of the set + assert!(!active_ids.contains(&object::id_from_address(@5))); + + // all other nodes should be + assert!(active_ids.contains(&object::id_from_address(@1))); + assert!(active_ids.contains(&object::id_from_address(@2))); + assert!(active_ids.contains(&object::id_from_address(@3))); + assert!(active_ids.contains(&object::id_from_address(@4))); + assert!(active_ids.contains(&object::id_from_address(@6))); +} + +#[test] +fun test_evict_correct_node_with_updates() { + let nodes = vector[ + object::id_from_address(@1), + object::id_from_address(@2), + object::id_from_address(@3), + object::id_from_address(@4), + object::id_from_address(@5), + object::id_from_address(@6), + ]; + + let mut set = new(5, 0); + set.insert_or_update(nodes[3], 7); + set.insert_or_update(nodes[0], 10); + set.insert_or_update(nodes[2], 8); + set.insert_or_update(nodes[1], 9); + set.insert_or_update(nodes[4], 6); + + let mut total_stake = 10 + 9 + 8 + 7 + 6; + + assert!(set.total_stake == total_stake); + + // update nodes again + set.insert_or_update(nodes[0], 12); + // check if total stake was updated correctly + total_stake = total_stake - 10 + 12; + assert!(set.total_stake == total_stake); + // check if the stake of the node was updated correctly + assert!(set.stake_for_node(nodes[0]) == 12); + + set.insert_or_update(nodes[2], 13); + // check if total stake was updated correctly + total_stake = total_stake - 8 + 13; + assert!(set.total_stake == total_stake); + // check if the stake of the node was updated correctly + assert!(set.stake_for_node(nodes[2]) == 13); + + set.insert_or_update(nodes[3], 9); + // check if total stake was updated correctly + total_stake = total_stake - 7 + 9; + assert!(set.total_stake == total_stake); + // check if the stake of the node was updated correctly + assert!(set.stake_for_node(nodes[3]) == 9); + + set.insert_or_update(nodes[1], 10); + // check if total stake was updated correctly + total_stake = total_stake - 9 + 10; + assert!(set.total_stake == total_stake); + // check if the stake of the node was updated correctly + assert!(set.stake_for_node(nodes[1]) == 10); + + set.insert_or_update(nodes[4], 7); + // check if total stake was updated correctly + total_stake = total_stake - 6 + 7; + assert!(set.total_stake == total_stake); + // check if the stake of the node was updated correctly + assert!(set.stake_for_node(nodes[4]) == 7); + + // insert another node which should eject nodes[4] (address @5) + set.insert_or_update(nodes[5], 11); + // check if total stake was updated correctly + total_stake = total_stake - 7 + 11; + assert!(set.total_stake == total_stake); + // check if the stake of the node was updated correctly + assert!(set.stake_for_node(nodes[5]) == 11); + + let active_ids = set.active_ids(); + + // node 5 should not be part of the set + assert!(!active_ids.contains(&nodes[4])); + + // all other nodes should be + assert!(active_ids.contains(&nodes[0])); + assert!(active_ids.contains(&nodes[1])); + assert!(active_ids.contains(&nodes[2])); + assert!(active_ids.contains(&nodes[3])); + assert!(active_ids.contains(&nodes[5])); +} diff --git a/contracts/walrus/sources/staking/committee.move b/contracts/walrus/sources/staking/committee.move new file mode 100644 index 00000000..cf61f6ce --- /dev/null +++ b/contracts/walrus/sources/staking/committee.move @@ -0,0 +1,135 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// This module defines the `Committee` struct which stores the current +/// committee with shard assignments. Additionally, it manages transitions / +/// transfers of shards between committees with the least amount of changes. +module walrus::committee; + +use sui::vec_map::{Self, VecMap}; + +/// Represents the current committee in the system. Each node in the committee +/// has assigned shard IDs. +public struct Committee(VecMap>) has store, copy, drop; + +/// Creates an empty committee. Only relevant for epoch 0, when no nodes are +/// assigned any shards. +public(package) fun empty(): Committee { Committee(vec_map::empty()) } + +/// Initializes the committee with the given `assigned_number` of shards per +/// node. Shards are assigned sequentially to each node. +public(package) fun initialize(assigned_number: VecMap): Committee { + let mut shard_idx: u16 = 0; + let (keys, values) = assigned_number.into_keys_values(); + let cmt = vec_map::from_keys_values( + keys, + values.map!(|v| vector::tabulate!(v as u64, |_| { + let res = shard_idx; + shard_idx = shard_idx + 1; + res + })), + ); + + Committee(cmt) +} + +/// Transitions the current committee to the new committee with the given shard +/// assignments. The function tries to minimize the number of changes by keeping +/// as many shards in place as possible. +/// +/// This assumes that the number of shards in the new committee is equal to the +/// number of shards in the current committee. Check for this is not performed. +public(package) fun transition(cmt: &Committee, mut new_assignments: VecMap): Committee { + let mut new_cmt = vec_map::empty(); + let mut to_move = vector[]; + let size = cmt.0.size(); + + size.do!(|idx| { + let (node_id, prev_shards) = cmt.0.get_entry_by_idx(idx); + let node_id = *node_id; + let assigned_len = new_assignments.get_idx_opt(&node_id).map!(|idx| { + let (_, value) = new_assignments.remove_entry_by_idx(idx); + value as u64 + }); + + // if the node is not in the new committee, remove all shards, make + // them available for reassignment + if (assigned_len.is_none() || assigned_len.borrow() == &0) { + let shards = cmt.0.get(&node_id); + to_move.append(*shards); + return + }; + + let curr_len = prev_shards.length(); + let assigned_len = assigned_len.destroy_some(); + + // node stays the same, we copy the shards over, best scenario + if (curr_len == assigned_len) { + new_cmt.insert(node_id, *prev_shards); + }; + + // if the node is in the new committee, check if the number of shards + // assigned to the node has decreased. If so, remove the extra shards, + // and move the node to the new committee + if (curr_len > assigned_len) { + let mut node_shards = *prev_shards; + (curr_len - assigned_len).do!(|_| to_move.push_back(node_shards.pop_back())); + new_cmt.insert(node_id, node_shards); + }; + + // if the node is in the new committee, and we already freed enough + // shards from other nodes, perform the reassignment. Alternatively, + // mark the node as needing more shards, so when we free up enough + // shards, we can assign them to this node + if (curr_len < assigned_len) { + let diff = assigned_len - curr_len; + if (to_move.length() >= diff) { + let mut node_shards = *prev_shards; + diff.do!(|_| node_shards.push_back(to_move.pop_back())); + new_cmt.insert(node_id, node_shards); + } else { + // insert it back, we didn't have enough shards to assign + new_assignments.insert(node_id, assigned_len as u16); + }; + }; + }); + + // Now the `new_assignments` only contains nodes for which we didn't have + // enough shards to assign, and the nodes that were not part of the old + // committee. + let (keys, values) = new_assignments.into_keys_values(); + keys.zip_do!(values, |key, value| { + if (value == 0) return; // ignore nodes with 0 shards + + let mut current_shards = cmt.0.try_get(&key).destroy_or!(vector[]); + current_shards + .length() + .diff(value as u64) + .do!(|_| current_shards.push_back(to_move.pop_back())); + + new_cmt.insert(key, current_shards); + }); + + Committee(new_cmt) +} + +#[syntax(index)] +/// Get the shards assigned to the given `node_id`. +public(package) fun shards(cmt: &Committee, node_id: &ID): &vector { + cmt.0.get(node_id) +} + +/// Get the number of nodes in the committee. +public(package) fun size(cmt: &Committee): u64 { + cmt.0.size() +} + +/// Get the inner representation of the committee. +public(package) fun inner(cmt: &Committee): &VecMap> { + &cmt.0 +} + +/// Copy the inner representation of the committee. +public(package) fun to_inner(cmt: &Committee): VecMap> { + cmt.0 +} diff --git a/contracts/walrus/sources/staking/exchange_rate.move b/contracts/walrus/sources/staking/exchange_rate.move new file mode 100644 index 00000000..57b0e3c9 --- /dev/null +++ b/contracts/walrus/sources/staking/exchange_rate.move @@ -0,0 +1,56 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// A utility module which implements an `ExchangeRate` struct and its methods. +/// It stores a fixed point exchange rate between the Wal token and pool token. +module walrus::pool_exchange_rate; + +/// Represents the exchange rate for the staking pool. +public struct PoolExchangeRate has store, copy, drop { + /// Amount of staked WAL tokens this epoch. + wal_amount: u128, + /// Amount of total tokens in the pool this epoch. + pool_token_amount: u128, +} + +/// Create an empty exchange rate. +public(package) fun empty(): PoolExchangeRate { + PoolExchangeRate { + wal_amount: 0, + pool_token_amount: 0, + } +} + +/// Create a new exchange rate with the given amounts. +public(package) fun new(wal_amount: u64, pool_token_amount: u64): PoolExchangeRate { + PoolExchangeRate { + wal_amount: (wal_amount as u128), + pool_token_amount: (pool_token_amount as u128), + } +} + +public(package) fun get_wal_amount(exchange_rate: &PoolExchangeRate, token_amount: u64): u64 { + // When either amount is 0, that means we have no stakes with this pool. + // The other amount might be non-zero when there's dust left in the pool. + if (exchange_rate.wal_amount == 0 || exchange_rate.pool_token_amount == 0) { + return token_amount + }; + + let token_amount = (token_amount as u128); + let res = token_amount * exchange_rate.wal_amount / exchange_rate.pool_token_amount; + + res as u64 +} + +public(package) fun get_token_amount(exchange_rate: &PoolExchangeRate, wal_amount: u64): u64 { + // When either amount is 0, that means we have no stakes with this pool. + // The other amount might be non-zero when there's dust left in the pool. + if (exchange_rate.wal_amount == 0 || exchange_rate.pool_token_amount == 0) { + return wal_amount + }; + + let wal_amount = (wal_amount as u128); + let res = wal_amount * exchange_rate.pool_token_amount / exchange_rate.wal_amount; + + res as u64 +} diff --git a/contracts/walrus/sources/staking/pending_values.move b/contracts/walrus/sources/staking/pending_values.move new file mode 100644 index 00000000..b9327304 --- /dev/null +++ b/contracts/walrus/sources/staking/pending_values.move @@ -0,0 +1,79 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module walrus::pending_values; + +use sui::vec_map::{Self, VecMap}; + +/// Represents a map of pending values. The key is the epoch when the value is +/// pending, and the value is the amount of WALs or pool tokens. +public struct PendingValues(VecMap) has store, drop, copy; + +/// Create a new empty `PendingValues` instance. +public(package) fun empty(): PendingValues { PendingValues(vec_map::empty()) } + +public(package) fun insert_or_add(self: &mut PendingValues, epoch: u32, value: u64) { + let map = &mut self.0; + if (!map.contains(&epoch)) { + map.insert(epoch, value); + } else { + let curr = map[&epoch]; + *&mut map[&epoch] = curr + value; + }; +} + +/// Get the total value of the pending values up to the given epoch. +public(package) fun value_at(self: &PendingValues, epoch: u32): u64 { + self.0.keys().fold!(0, |mut value, e| { + if (e <= epoch) value = value + self.0[&e]; + value + }) +} + +/// Reduce the pending values to the given epoch. This method removes all the +/// values that are pending for epochs less than or equal to the given epoch. +public(package) fun flush(self: &mut PendingValues, to_epoch: u32): u64 { + let mut value = 0; + self.0.keys().do!(|epoch| if (epoch <= to_epoch) { + let (_, epoch_value) = self.0.remove(&epoch); + value = value + epoch_value; + }); + value +} + +/// Unwrap the `PendingValues` into a `VecMap`. +public(package) fun unwrap(self: PendingValues): VecMap { + let PendingValues(map) = self; + map +} + +/// Check if the `PendingValues` is empty. +public(package) fun is_empty(self: &PendingValues): bool { self.0.is_empty() } + +#[test] +fun test_pending_values() { + use std::unit_test::assert_eq; + + let mut pending = empty(); + assert!(pending.is_empty()); + + pending.insert_or_add(0, 10); + pending.insert_or_add(0, 10); + pending.insert_or_add(1, 20); + + // test reads + assert_eq!(pending.value_at(0), 20); + assert_eq!(pending.value_at(1), 40); + + // test flushing, and reads after flushing + assert_eq!(pending.flush(0), 20); + assert_eq!(pending.value_at(0), 0); + + // flush the rest of the values and check if the map is empty + assert_eq!(pending.value_at(1), 20); + assert_eq!(pending.flush(1), 20); + assert!(pending.is_empty()); + + // unwrap the pending values + let _ = pending.unwrap(); +} diff --git a/contracts/walrus/sources/staking/staked_wal.move b/contracts/walrus/sources/staking/staked_wal.move new file mode 100644 index 00000000..42b8bb09 --- /dev/null +++ b/contracts/walrus/sources/staking/staked_wal.move @@ -0,0 +1,172 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Module: `staked_wal` +/// +/// Implements the `StakedWal` functionality - a staked WAL is an object that +/// represents a staked amount of WALs in a staking pool. It is created in the +/// `staking_pool` on staking and can be split, joined, and burned. The burning +/// is performed via the `withdraw_stake` method in the `staking_pool`. +module walrus::staked_wal; + +use sui::balance::Balance; +use wal::wal::WAL; + +// Keep errors in `walrus-sui/types/move_errors.rs` up to date with changes here. +const ENotWithdrawing: u64 = 0; +const EMetadataMismatch: u64 = 1; +const EInvalidAmount: u64 = 2; +const ENonZeroPrincipal: u64 = 3; +const ECantJoinWithdrawing: u64 = 4; +const ECantSplitWithdrawing: u64 = 5; + +/// The state of the staked WAL. It can be either `Staked` or `Withdrawing`. +/// The `Withdrawing` state contains the epoch when the staked WAL can be +/// +public enum StakedWalState has store, copy, drop { + // Default state of the staked WAL - it is staked in the staking pool. + Staked, + // The staked WAL is in the process of withdrawing. The value inside the + // variant is the epoch when the staked WAL can be withdrawn. + Withdrawing { withdraw_epoch: u32, pool_token_amount: u64 }, +} + +/// Represents a staked WAL, does not store the `Balance` inside, but uses +/// `u64` to represent the staked amount. Behaves similarly to `Balance` and +/// `Coin` providing methods to `split` and `join`. +public struct StakedWal has key, store { + id: UID, + /// Whether the staked WAL is active or withdrawing. + state: StakedWalState, + /// ID of the staking pool. + node_id: ID, + /// The staked amount. + principal: Balance, + /// The Walrus epoch when the staked WAL was activated. + activation_epoch: u32, +} + +/// Protected method to create a new staked WAL. +public(package) fun mint( + node_id: ID, + principal: Balance, + activation_epoch: u32, + ctx: &mut TxContext, +): StakedWal { + StakedWal { + id: object::new(ctx), + state: StakedWalState::Staked, + node_id, + principal, + activation_epoch, + } +} + +/// Burns the staked WAL and returns the `principal`. +public(package) fun into_balance(sw: StakedWal): Balance { + let StakedWal { id, principal, .. } = sw; + id.delete(); + principal +} + +/// Sets the staked WAL state to `Withdrawing` +public(package) fun set_withdrawing( + sw: &mut StakedWal, + withdraw_epoch: u32, + pool_token_amount: u64, +) { + sw.state = StakedWalState::Withdrawing { withdraw_epoch, pool_token_amount }; +} + +// === Accessors === + +/// Returns the `node_id` of the staked WAL. +public fun node_id(sw: &StakedWal): ID { sw.node_id } + +/// Returns the `principal` of the staked WAL. Called `value` to be consistent +/// with `Coin`. +public fun value(sw: &StakedWal): u64 { sw.principal.value() } + +/// Returns the `activation_epoch` of the staked WAL. +public fun activation_epoch(sw: &StakedWal): u32 { sw.activation_epoch } + +/// Returns true if the staked WAL is in the `Staked` state. +public fun is_staked(sw: &StakedWal): bool { sw.state == StakedWalState::Staked } + +/// Checks whether the staked WAL is in the `Withdrawing` state. +public fun is_withdrawing(sw: &StakedWal): bool { + match (sw.state) { + StakedWalState::Withdrawing { .. } => true, + _ => false, + } +} + +/// Returns the `withdraw_epoch` of the staked WAL if it is in the `Withdrawing`. +/// Aborts otherwise. +public fun withdraw_epoch(sw: &StakedWal): u32 { + match (sw.state) { + StakedWalState::Withdrawing { withdraw_epoch, .. } => withdraw_epoch, + _ => abort ENotWithdrawing, + } +} + +/// Return the `withdraw_amount` of the staked WAL if it is in the `Withdrawing`. +/// Aborts otherwise. +public fun pool_token_amount(sw: &StakedWal): u64 { + match (sw.state) { + StakedWalState::Withdrawing { pool_token_amount, .. } => pool_token_amount, + _ => abort ENotWithdrawing, + } +} + +// === Public APIs === + +/// Joins the staked WAL with another staked WAL, adding the `principal` of the +/// `other` staked WAL to the current staked WAL. +/// +/// Aborts if the `node_id` or `activation_epoch` of the staked WALs do not match. +public fun join(sw: &mut StakedWal, other: StakedWal) { + let StakedWal { id, state, node_id, activation_epoch, principal } = other; + assert!(sw.state == state, EMetadataMismatch); + assert!(sw.node_id == node_id, EMetadataMismatch); + assert!(!sw.is_withdrawing(), ECantJoinWithdrawing); + assert!(sw.activation_epoch == activation_epoch, EMetadataMismatch); + + id.delete(); + + sw.principal.join(principal); +} + +/// Splits the staked WAL into two parts, one with the `amount` and the other +/// with the remaining `principal`. The `node_id`, `activation_epoch` are the +/// same for both the staked WALs. +/// +/// Aborts if the `amount` is greater than the `principal` of the staked WAL. +public fun split(sw: &mut StakedWal, amount: u64, ctx: &mut TxContext): StakedWal { + assert!(sw.principal.value() >= amount, EInvalidAmount); + assert!(!sw.is_withdrawing(), ECantSplitWithdrawing); + + StakedWal { + id: object::new(ctx), + state: sw.state, // state is preserved + node_id: sw.node_id, + principal: sw.principal.split(amount), + activation_epoch: sw.activation_epoch, + } +} + +/// Destroys the staked WAL if the `principal` is zero. Ignores the `node_id` +/// and `activation_epoch` of the staked WAL given that it is zero. +public fun destroy_zero(sw: StakedWal) { + assert!(sw.principal.value() == 0, ENonZeroPrincipal); + let StakedWal { id, principal, .. } = sw; + principal.destroy_zero(); + id.delete(); +} + +#[test_only] +public fun destroy_for_testing(sw: StakedWal) { + let StakedWal { id, principal, .. } = sw; + principal.destroy_for_testing(); + id.delete(); +} diff --git a/contracts/walrus/sources/staking/staking_inner.move b/contracts/walrus/sources/staking/staking_inner.move new file mode 100644 index 00000000..26131df4 --- /dev/null +++ b/contracts/walrus/sources/staking/staking_inner.move @@ -0,0 +1,731 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module walrus::staking_inner; + +use std::string::String; +use sui::{ + balance::{Self, Balance}, + clock::Clock, + coin::Coin, + object_table::{Self, ObjectTable}, + priority_queue::{Self, PriorityQueue}, + vec_map +}; +use wal::wal::WAL; +use walrus::{ + active_set::{Self, ActiveSet}, + bls_aggregate::{Self, BlsCommittee}, + committee::{Self, Committee}, + epoch_parameters::{Self, EpochParams}, + events, + staked_wal::StakedWal, + staking_pool::{Self, StakingPool}, + storage_node::StorageNodeCap, + walrus_context::{Self, WalrusContext} +}; + +/// The minimum amount of staked WAL required to be included in the active set. +const MIN_STAKE: u64 = 0; + +/// Temporary upper limit for the number of storage nodes. +const TEMP_ACTIVE_SET_SIZE_LIMIT: u16 = 100; + +// Keep errors in `walrus-sui/types/move_errors.rs` up to date with changes here. +const EWrongEpochState: u64 = 0; +const EInvalidSyncEpoch: u64 = 1; +const EDuplicateSyncDone: u64 = 2; +const ENoStake: u64 = 3; +const ENotInCommittee: u64 = 4; +const ENotImplemented: u64 = 5; + +/// The epoch state. +public enum EpochState has store, copy, drop { + // Epoch change is currently in progress. Contains the weight of the nodes that + // have already attested that they finished the sync. + EpochChangeSync(u16), + // Epoch change has been completed at the contained timestamp. + EpochChangeDone(u64), + // The parameters for the next epoch have been selected. + // The contained timestamp is the start of the current epoch. + NextParamsSelected(u64), +} + +/// The inner object for the staking part of the system. +public struct StakingInnerV1 has store, key { + /// The object ID + id: UID, + /// The number of shards in the system. + n_shards: u16, + /// The duration of an epoch in ms. Does not affect the first (zero) epoch. + epoch_duration: u64, + /// Special parameter, used only for the first epoch. The timestamp when the + /// first epoch can be started. + first_epoch_start: u64, + /// Stored staking pools, each identified by a unique `ID` and contains + /// the `StakingPool` object. Uses `ObjectTable` to make the pool discovery + /// easier by avoiding wrapping. + /// + /// The key is the ID of the staking pool. + pools: ObjectTable, + /// The current epoch of the Walrus system. The epochs are not the same as + /// the Sui epochs, not to be mistaken with `ctx.epoch()`. + epoch: u32, + /// Stores the active set of storage nodes. Provides automatic sorting and + /// tracks the total amount of staked WAL. + active_set: ActiveSet, + /// The next committee in the system. + next_committee: Option, + /// The current committee in the system. + committee: Committee, + /// The previous committee in the system. + previous_committee: Committee, + /// The next epoch parameters. + next_epoch_params: Option, + /// The state of the current epoch. + epoch_state: EpochState, + /// Rewards left over from the previous epoch that couldn't be distributed due to rounding. + leftover_rewards: Balance, +} + +/// Creates a new `StakingInnerV1` object with default values. +public(package) fun new( + epoch_zero_duration: u64, + epoch_duration: u64, + n_shards: u16, + clock: &Clock, + ctx: &mut TxContext, +): StakingInnerV1 { + StakingInnerV1 { + id: object::new(ctx), + n_shards, + epoch_duration, + first_epoch_start: epoch_zero_duration + clock.timestamp_ms(), + pools: object_table::new(ctx), + epoch: 0, + active_set: active_set::new(TEMP_ACTIVE_SET_SIZE_LIMIT, MIN_STAKE), + next_committee: option::none(), + committee: committee::empty(), + previous_committee: committee::empty(), + next_epoch_params: option::none(), + epoch_state: EpochState::EpochChangeDone(clock.timestamp_ms()), + leftover_rewards: balance::zero(), + } +} + +// === Staking Pool / Storage Node === + +/// Creates a new staking pool with the given `commission_rate`. +public(package) fun create_pool( + self: &mut StakingInnerV1, + name: String, + network_address: String, + public_key: vector, + network_public_key: vector, + proof_of_possession: vector, + commission_rate: u64, + storage_price: u64, + write_price: u64, + node_capacity: u64, + ctx: &mut TxContext, +): ID { + let pool = staking_pool::new( + name, + network_address, + public_key, + network_public_key, + proof_of_possession, + commission_rate, + storage_price, + write_price, + node_capacity, + &self.new_walrus_context(), + ctx, + ); + + let node_id = object::id(&pool); + self.pools.add(node_id, pool); + node_id +} + +/// Blocks staking for the pool, marks it as "withdrawing". +#[allow(unused_mut_parameter)] +public(package) fun withdraw_node(self: &mut StakingInnerV1, cap: &mut StorageNodeCap) { + let wctx = &self.new_walrus_context(); + self.pools[cap.node_id()].set_withdrawing(wctx); +} + +public(package) fun collect_commission(_: &mut StakingInnerV1, _: &StorageNodeCap): Coin { + abort ENotImplemented +} + +public(package) fun voting_end(self: &mut StakingInnerV1, clock: &Clock) { + // Check if it's time to end the voting. + let last_epoch_change = match (self.epoch_state) { + EpochState::EpochChangeDone(last_epoch_change) => last_epoch_change, + _ => abort EWrongEpochState, + }; + + let now = clock.timestamp_ms(); + let param_selection_delta = self.epoch_duration / 2; + + // We don't need a delay for the epoch zero. + if (self.epoch != 0) { + assert!(now >= last_epoch_change + param_selection_delta, EWrongEpochState); + } else { + assert!(now >= self.first_epoch_start, EWrongEpochState); + }; + + // Assign the next epoch committee. + self.select_committee(); + self.next_epoch_params = option::some(self.calculate_votes()); + + // Set the new epoch state. + self.epoch_state = EpochState::NextParamsSelected(last_epoch_change); + + // Emit event that parameters have been selected. + events::emit_epoch_parameters_selected(self.epoch + 1); +} + +/// Calculates the votes for the next epoch parameters. The function sorts the +/// write and storage prices and picks the value that satisfies a quorum of the weight. +public(package) fun calculate_votes(self: &StakingInnerV1): EpochParams { + assert!(self.next_committee.is_some()); + + let size = self.next_committee.borrow().size(); + let inner = self.next_committee.borrow().inner(); + let mut write_prices = priority_queue::new(vector[]); + let mut storage_prices = priority_queue::new(vector[]); + let mut capacity_votes = priority_queue::new(vector[]); + + size.do!(|i| { + let (node_id, shards) = inner.get_entry_by_idx(i); + let pool = &self.pools[*node_id]; + let weight = shards.length(); + write_prices.insert(pool.write_price(), weight); + storage_prices.insert(pool.storage_price(), weight); + // The vote for capacity is determined by the node capacity and number of assigned shards. + let capacity_vote = pool.node_capacity() / weight * (self.n_shards as u64); + capacity_votes.insert(capacity_vote, weight); + }); + + epoch_parameters::new( + quorum_above(&mut capacity_votes, self.n_shards), + quorum_below(&mut storage_prices, self.n_shards), + quorum_below(&mut write_prices, self.n_shards), + ) +} + +/// Take the highest value, s.t. a quorum (2f + 1) voted for a value larger or equal to this. +fun quorum_above(vote_queue: &mut PriorityQueue, n_shards: u16): u64 { + let threshold_weight = (n_shards - (n_shards - 1) / 3) as u64; + take_threshold_value(vote_queue, threshold_weight) +} + +/// Take the lowest value, s.t. a quorum (2f + 1) voted for a value lower or equal to this. +fun quorum_below(vote_queue: &mut PriorityQueue, n_shards: u16): u64 { + let threshold_weight = ((n_shards - 1) / 3 + 1) as u64; + take_threshold_value(vote_queue, threshold_weight) +} + +fun take_threshold_value(vote_queue: &mut PriorityQueue, threshold_weight: u64): u64 { + let mut sum_weight = 0; + // The loop will always succeed if `threshold_weight` is smaller than the total weight. + loop { + let (value, weight) = vote_queue.pop_max(); + sum_weight = sum_weight + weight; + if (sum_weight >= threshold_weight) { + return value + }; + } +} + +// === Voting === + +/// Sets the next commission rate for the pool. +public(package) fun set_next_commission( + self: &mut StakingInnerV1, + cap: &StorageNodeCap, + commission_rate: u64, +) { + self.pools[cap.node_id()].set_next_commission(commission_rate); +} + +/// Sets the storage price vote for the pool. +public(package) fun set_storage_price_vote( + self: &mut StakingInnerV1, + cap: &StorageNodeCap, + storage_price: u64, +) { + self.pools[cap.node_id()].set_next_storage_price(storage_price); +} + +/// Sets the write price vote for the pool. +public(package) fun set_write_price_vote( + self: &mut StakingInnerV1, + cap: &StorageNodeCap, + write_price: u64, +) { + self.pools[cap.node_id()].set_next_write_price(write_price); +} + +/// Sets the node capacity vote for the pool. +public(package) fun set_node_capacity_vote( + self: &mut StakingInnerV1, + cap: &StorageNodeCap, + node_capacity: u64, +) { + self.pools[cap.node_id()].set_next_node_capacity(node_capacity); +} + +// === Update Node Parameters === + +/// Sets the public key of a node to be used starting from the next epoch for which the node is +/// selected. +public(package) fun set_next_public_key( + self: &mut StakingInnerV1, + cap: &StorageNodeCap, + public_key: vector, + proof_of_possession: vector, + ctx: &TxContext, +) { + let wctx = &self.new_walrus_context(); + self.pools[cap.node_id()].set_next_public_key(public_key, proof_of_possession, wctx, ctx); +} + +/// Sets the name of a storage node. +public(package) fun set_name(self: &mut StakingInnerV1, cap: &StorageNodeCap, name: String) { + self.pools[cap.node_id()].set_name(name); +} + +/// Sets the network address or host of a storage node. +public(package) fun set_network_address( + self: &mut StakingInnerV1, + cap: &StorageNodeCap, + network_address: String, +) { + self.pools[cap.node_id()].set_network_address(network_address); +} + +/// Sets the public key used for TLS communication for a node. +public(package) fun set_network_public_key( + self: &mut StakingInnerV1, + cap: &StorageNodeCap, + network_public_key: vector, +) { + self.pools[cap.node_id()].set_network_public_key(network_public_key); +} + +// === Staking === + +/// Blocks staking for the pool, marks it as "withdrawing". +public(package) fun set_withdrawing(self: &mut StakingInnerV1, node_id: ID) { + let wctx = &self.new_walrus_context(); + self.pools[node_id].set_withdrawing(wctx); +} + +/// Destroys the pool if it is empty, after the last stake has been withdrawn. +public(package) fun destroy_empty_pool( + self: &mut StakingInnerV1, + node_id: ID, + _ctx: &mut TxContext, +) { + self.pools.remove(node_id).destroy_empty() +} + +/// Stakes the given amount of `T` with the pool, returning the `StakedWal`. +public(package) fun stake_with_pool( + self: &mut StakingInnerV1, + to_stake: Coin, + node_id: ID, + ctx: &mut TxContext, +): StakedWal { + let wctx = &self.new_walrus_context(); + let pool = &mut self.pools[node_id]; + let staked_wal = pool.stake(to_stake.into_balance(), wctx, ctx); + + // Active set only tracks the stake for the next vote, which either happens for the committee + // in wctx.epoch() + 1, or in wctx.epoch() + 2, depending on whether the vote already happened. + let balance = match (self.epoch_state) { + EpochState::NextParamsSelected(_) => pool.wal_balance_at_epoch(wctx.epoch() + 2), + _ => pool.wal_balance_at_epoch(wctx.epoch() + 1), + }; + self.active_set.insert_or_update(node_id, balance); + staked_wal +} + +/// Requests withdrawal of the given amount from the `StakedWAL`, marking it as +/// `Withdrawing`. Once the epoch is greater than the `withdraw_epoch`, the +/// withdrawal can be performed. +public(package) fun request_withdraw_stake( + self: &mut StakingInnerV1, + staked_wal: &mut StakedWal, + _ctx: &mut TxContext, +) { + let wctx = &self.new_walrus_context(); + self.pools[staked_wal.node_id()].request_withdraw_stake(staked_wal, wctx); +} + +/// Perform the withdrawal of the staked WAL, returning the amount to the caller. +/// The `StakedWal` must be in the `Withdrawing` state, and the epoch must be +/// greater than the `withdraw_epoch`. +public(package) fun withdraw_stake( + self: &mut StakingInnerV1, + staked_wal: StakedWal, + ctx: &mut TxContext, +): Coin { + let wctx = &self.new_walrus_context(); + self.pools[staked_wal.node_id()].withdraw_stake(staked_wal, wctx).into_coin(ctx) +} + +// === System === + +/// Selects the committee for the next epoch. +public(package) fun select_committee(self: &mut StakingInnerV1) { + assert!(self.next_committee.is_none()); + + let (active_ids, shards) = self.apportionment(); + let distribution = vec_map::from_keys_values(active_ids, shards); + + // if we're dealing with the first epoch, we need to assign the shards to the + // nodes in a sequential manner. Assuming there's at least 1 node in the set. + let committee = if (self.committee.size() == 0) committee::initialize(distribution) + else self.committee.transition(distribution); + + self.next_committee = option::some(committee); +} + +fun apportionment(self: &StakingInnerV1): (vector, vector) { + let (active_ids, stake) = self.active_set.active_ids_and_stake(); + let n_nodes = stake.length(); + let priorities = vector::tabulate!(n_nodes, |i| n_nodes - i); + let shards = dhondt(priorities, self.n_shards, stake); + (active_ids, shards) +} + +const DHONDT_TOTAL_STAKE_MAX: u64 = 0xFFFF_FFFF; + +// Implementation of the D'Hondt method (aka Jefferson method) for apportionment. +fun dhondt( + // Priorities for the nodes for tie-breaking. Nodes with a higher priority value + // have a higher precedence. + node_priorities: vector, + n_shards: u16, + stake: vector, +): vector { + use std::fixed_point32::{create_from_rational as from_rational, get_raw_value as to_raw}; + + let total_stake = stake.fold!(0, |acc, x| acc + x); + + let scaling = DHONDT_TOTAL_STAKE_MAX + .max(total_stake) + .divide_and_round_up(DHONDT_TOTAL_STAKE_MAX); + let total_stake = total_stake / scaling; + let stake = stake.map!(|s| s / scaling); + + let n_nodes = stake.length(); + let n_shards = n_shards as u64; + assert!(total_stake > 0, ENoStake); + + // Initial assignment following Hagenbach-Bischoff. + // This assigns an initial number of shards to each node, s.t. this does not exceed the final + // assignment. + // The denominator (`total_stake/(n_shards + 1) + 1`) is called "distribution number" and + // is the amount of stake that guarantees receiving a shard with the d'Hondt method. By + // dividing the stake per node by this distribution number and rounding down (integer + // division), we therefore get a lower bound for the number of shards assigned to the node. + let mut shards = stake.map_ref!(|s| *s / (total_stake/(n_shards + 1) + 1)); + // Set up quotients priority queue. + let mut quotients = priority_queue::new(vector[]); + n_nodes.do!(|index| { + let quotient = from_rational(stake[index], shards[index] + 1); + quotients.insert(quotient.to_raw(), index); + }); + + // Set up a priority queue for the ranking of nodes with equal quotient. + let mut equal_quotient_ranking = priority_queue::new(vector[]); + // Priority_queue currently doesn't allow peeking at the head or checking the length. + let mut equal_quotient_ranking_len = 0; + + if (n_nodes == 0) return vector[]; + let mut n_shards_distributed = shards.fold!(0, |acc, x| acc + x); + // loop until all shards are distributed + while (n_shards_distributed != n_shards) { + let index = if (equal_quotient_ranking_len > 0) { + let (_priority, index) = equal_quotient_ranking.pop_max(); + equal_quotient_ranking_len = equal_quotient_ranking_len - 1; + index + } else { + let (quotient, index) = quotients.pop_max(); + equal_quotient_ranking.insert(node_priorities[index], index); + equal_quotient_ranking_len = equal_quotient_ranking_len + 1; + // Condition ensures that `quotients` is not empty. + while (n_nodes > equal_quotient_ranking_len) { + let (next_quotient, next_index) = quotients.pop_max(); + if (next_quotient == quotient) { + equal_quotient_ranking.insert(node_priorities[next_index], next_index); + equal_quotient_ranking_len = equal_quotient_ranking_len + 1; + } else { + quotients.insert(next_quotient, next_index); + break + } + }; + let (_priority, index) = equal_quotient_ranking.pop_max(); + equal_quotient_ranking_len = equal_quotient_ranking_len - 1; + index + }; + *&mut shards[index] = shards[index] + 1; + let quotient = from_rational(stake[index], shards[index] + 1); + quotients.insert(quotient.to_raw(), index); + n_shards_distributed = n_shards_distributed + 1; + }; + shards.map!(|s| s as u16) +} + +/// Initiates the epoch change if the current time allows. +public(package) fun initiate_epoch_change( + self: &mut StakingInnerV1, + clock: &Clock, + rewards: Balance, +) { + let last_epoch_change = match (self.epoch_state) { + EpochState::NextParamsSelected(last_epoch_change) => last_epoch_change, + _ => abort EWrongEpochState, + }; + + let now = clock.timestamp_ms(); + + if (self.epoch == 0) assert!(now >= self.first_epoch_start, EWrongEpochState) + else assert!(now >= last_epoch_change + self.epoch_duration, EWrongEpochState); + + self.advance_epoch(rewards); +} + +/// Sets the next epoch of the system and emits the epoch change start event. +public(package) fun advance_epoch(self: &mut StakingInnerV1, mut rewards: Balance) { + assert!(self.next_committee.is_some(), EWrongEpochState); + + self.epoch = self.epoch + 1; + self.previous_committee = self.committee; + self.committee = self.next_committee.extract(); // overwrites the current committee + self.epoch_state = EpochState::EpochChangeSync(0); + + let wctx = &self.new_walrus_context(); + + // Distribute the rewards. + + // Add any leftover rewards to the rewards to distribute. + let leftover_value = self.leftover_rewards.value(); + rewards.join(self.leftover_rewards.split(leftover_value)); + let rewards_per_shard = rewards.value() / (self.n_shards as u64); + + // Add any nodes that are new in the committee to the previous shard assignments + // without any shards, s.t. we call advance_epoch on them and update the active set. + let mut prev_shard_assignments = *self.previous_committee.inner(); + self.committee.inner().keys().do!(|node_id| if (!prev_shard_assignments.contains(&node_id)) { + prev_shard_assignments.insert(node_id, vector[]); + }); + let (node_ids, shard_assignments) = prev_shard_assignments.into_keys_values(); + + node_ids.zip_do!(shard_assignments, |node_id, shards| { + self.pools[node_id].advance_epoch(rewards.split(rewards_per_shard * shards.length()), wctx); + self + .active_set + .update(node_id, self.pools[node_id].wal_balance_at_epoch(wctx.epoch() + 1)); + }); + + // Save any leftover rewards due to rounding. + self.leftover_rewards.join(rewards); + + // Emit epoch change start event. + events::emit_epoch_change_start(self.epoch); +} + +/// Signals to the contract that the node has received all its shards for the new epoch. +public(package) fun epoch_sync_done( + self: &mut StakingInnerV1, + cap: &mut StorageNodeCap, + epoch: u32, + clock: &Clock, +) { + // Make sure the node hasn't attested yet, and set the new epoch as the last sync done epoch. + assert!(epoch == self.epoch, EInvalidSyncEpoch); + assert!(cap.last_epoch_sync_done() < self.epoch, EDuplicateSyncDone); + cap.set_last_epoch_sync_done(self.epoch); + + assert!(self.committee.inner().contains(&cap.node_id()), ENotInCommittee); + let node_shards = self.committee.shards(&cap.node_id()); + match (self.epoch_state) { + EpochState::EpochChangeSync(weight) => { + let weight = weight + (node_shards.length() as u16); + if (is_quorum(weight, self.n_shards)) { + self.epoch_state = EpochState::EpochChangeDone(clock.timestamp_ms()); + events::emit_epoch_change_done(self.epoch); + } else { + self.epoch_state = EpochState::EpochChangeSync(weight); + } + }, + _ => {}, + }; + // Emit the event that the node has received all shards. + events::emit_shards_received(self.epoch, *node_shards); +} + +/// Checks if the node should either have received the specified shards from the specified node +/// or vice-versa. +/// +/// - also checks that for the provided shards, this function has not been called before +/// - if so, slashes both nodes and emits an event that allows the receiving node to start +/// shard recovery +public fun shard_transfer_failed( + _staking: &mut StakingInnerV1, + _cap: &StorageNodeCap, + _other_node_id: ID, + _shard_ids: vector, +) { + abort ENotImplemented +} + +// === Accessors === + +/// Returns the Option with next committee. +public(package) fun next_committee(self: &StakingInnerV1): &Option { + &self.next_committee +} + +/// Returns the next epoch parameters if set, otherwise aborts with an error. +public(package) fun next_epoch_params(self: &StakingInnerV1): EpochParams { + *self.next_epoch_params.borrow() +} + +/// Get the current epoch. +public(package) fun epoch(self: &StakingInnerV1): u32 { + self.epoch +} + +/// Get the current committee. +public(package) fun committee(self: &StakingInnerV1): &Committee { + &self.committee +} + +/// Get the previous committee. +public(package) fun previous_committee(self: &StakingInnerV1): &Committee { + &self.previous_committee +} + +/// Construct the BLS committee for the next epoch. +public(package) fun next_bls_committee(self: &StakingInnerV1): BlsCommittee { + let (ids, shard_assignments) = (*self.next_committee.borrow().inner()).into_keys_values(); + let members = ids.zip_map!(shard_assignments, |id, shards| { + let pk = self.pools.borrow(id).node_info().next_epoch_public_key(); + bls_aggregate::new_bls_committee_member(*pk, shards.length() as u16, id) + }); + bls_aggregate::new_bls_committee(self.epoch + 1, members) +} + +/// Check if a node with the given `ID` exists in the staking pools. +public(package) fun has_pool(self: &StakingInnerV1, node_id: ID): bool { + self.pools.contains(node_id) +} + +// === Internal === + +fun new_walrus_context(self: &StakingInnerV1): WalrusContext { + walrus_context::new( + self.epoch, + self.next_committee.is_some(), + self.committee.to_inner(), + ) +} + +fun is_quorum(weight: u16, n_shards: u16): bool { + 3 * (weight as u64) >= 2 * (n_shards as u64) + 1 +} + +// ==== Tests === +#[test_only] +use walrus::test_utils::assert_eq; + +#[test_only] +public(package) fun is_epoch_sync_done(self: &StakingInnerV1): bool { + match (self.epoch_state) { + EpochState::EpochChangeDone(_) => true, + _ => false, + } +} + +#[test_only] +public(package) fun active_set(self: &mut StakingInnerV1): &mut ActiveSet { + &mut self.active_set +} + +#[test_only] +#[syntax(index)] +/// Get the pool with the given `ID`. +public(package) fun borrow(self: &StakingInnerV1, node_id: ID): &StakingPool { + &self.pools[node_id] +} + +#[test_only] +#[syntax(index)] +/// Get mutable reference to the pool with the given `ID`. +public(package) fun borrow_mut(self: &mut StakingInnerV1, node_id: ID): &mut StakingPool { + &mut self.pools[node_id] +} + +#[test_only] +public(package) fun pub_dhondt(n_shards: u16, stake: vector): vector { + let n_nodes = stake.length(); + let priorities = vector::tabulate!(n_nodes, |i| n_nodes - i); + dhondt(priorities, n_shards, stake) +} + +#[test] +fun test_quorum_above() { + let mut queue = priority_queue::new(vector[]); + let votes = vector[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + let weights = vector[5, 5, 4, 6, 3, 7, 2, 8, 1, 9]; + votes.zip_do!(weights, |vote, weight| queue.insert(vote, weight)); + assert_eq!(quorum_above(&mut queue, 50), 4); +} + +#[test] +fun test_quorum_above_all_above() { + let mut queue = priority_queue::new(vector[]); + let votes = vector[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + let weights = vector[17, 1, 1, 1, 3, 7, 2, 8, 1, 9]; + votes.zip_do!(weights, |vote, weight| queue.insert(vote, weight)); + assert_eq!(quorum_above(&mut queue, 50), 1); +} + +#[test] +fun test_quorum_above_one_value() { + let mut queue = priority_queue::new(vector[]); + queue.insert(1, 50); + assert_eq!(quorum_above(&mut queue, 50), 1); +} + +#[test] +fun test_quorum_below() { + let mut queue = priority_queue::new(vector[]); + let votes = vector[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + let weights = vector[5, 5, 4, 6, 3, 7, 4, 6, 1, 9]; + votes.zip_do!(weights, |vote, weight| queue.insert(vote, weight)); + assert_eq!(quorum_below(&mut queue, 50), 7); +} + +#[test] +fun test_quorum_below_all_below() { + let mut queue = priority_queue::new(vector[]); + let votes = vector[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + let weights = vector[5, 5, 4, 6, 3, 7, 1, 1, 1, 17]; + votes.zip_do!(weights, |vote, weight| queue.insert(vote, weight)); + assert_eq!(quorum_below(&mut queue, 50), 10); +} + +#[test] +fun test_quorum_below_one_value() { + let mut queue = priority_queue::new(vector[]); + queue.insert(1, 50); + assert_eq!(quorum_below(&mut queue, 50), 1); +} diff --git a/contracts/walrus/sources/staking/staking_pool.move b/contracts/walrus/sources/staking/staking_pool.move new file mode 100644 index 00000000..fe0c3018 --- /dev/null +++ b/contracts/walrus/sources/staking/staking_pool.move @@ -0,0 +1,480 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Module: staking_pool +module walrus::staking_pool; + +use std::string::String; +use sui::{balance::{Self, Balance}, table::{Self, Table}}; +use wal::wal::WAL; +use walrus::{ + messages, + pending_values::{Self, PendingValues}, + pool_exchange_rate::{Self, PoolExchangeRate}, + staked_wal::{Self, StakedWal}, + storage_node::{Self, StorageNodeInfo}, + walrus_context::WalrusContext +}; + +// Keep errors in `walrus-sui/types/move_errors.rs` up to date with changes here. +const EPoolAlreadyUpdated: u64 = 0; +const ECalculationError: u64 = 1; +const EIncorrectEpochAdvance: u64 = 2; +const EPoolNotEmpty: u64 = 3; +const EInvalidProofOfPossession: u64 = 4; + +/// Represents the state of the staking pool. +public enum PoolState has store, copy, drop { + // The pool is new and awaits the stake to be added. + New, + // The pool is active and can accept stakes. + Active, + // The pool awaits the stake to be withdrawn. The value inside the + // variant is the epoch in which the pool will be withdrawn. + Withdrawing(u32), + // The pool is empty and can be destroyed. + Withdrawn, +} + +/// The parameters for the staking pool. Stored for the next epoch. +public struct VotingParams has store, copy, drop { + /// Voting: storage price for the next epoch. + storage_price: u64, + /// Voting: write price for the next epoch. + write_price: u64, + /// Voting: node capacity for the next epoch. + node_capacity: u64, +} + +/// Represents a single staking pool for a token. Even though it is never +/// transferred or shared, the `key` ability is added for discoverability +/// in the `ObjectTable`. +public struct StakingPool has key, store { + id: UID, + /// The current state of the pool. + state: PoolState, + /// Current epoch's pool parameters. + voting_params: VotingParams, + /// The storage node info for the pool. + node_info: StorageNodeInfo, + /// The epoch when the pool is / will be activated. + /// Serves information purposes only, the checks are performed in the `state` + /// property. + activation_epoch: u32, + /// Epoch when the pool was last updated. + latest_epoch: u32, + /// Currently staked WAL in the pool + rewards pool. + wal_balance: u64, + /// Balance of the pool token in the pool in the current epoch. + pool_token_balance: u64, + /// The amount of the pool token that will be withdrawn in E+1 or E+2. + /// We use this amount to calculate the WAL withdrawal in the + /// `process_pending_stake`. + pending_pool_token_withdraw: PendingValues, + /// The commission rate for the pool. + commission_rate: u64, + /// Historical exchange rates for the pool. The key is the epoch when the + /// exchange rate was set, and the value is the exchange rate (the ratio of + /// the amount of WAL tokens for the pool token). + exchange_rates: Table, + /// The amount of stake that will be added to the `wal_balance`. Can hold + /// up to two keys: E+1 and E+2, due to the differences in the activation + /// epoch. + /// + /// ``` + /// E+1 -> Balance + /// E+2 -> Balance + /// ``` + /// + /// Single key is cleared in the `advance_epoch` function, leaving only the + /// next epoch's stake. + pending_stake: PendingValues, + /// The rewards that the pool has received from being in the committee. + rewards_pool: Balance, +} + +/// Create a new `StakingPool` object. +/// If committee is selected, the pool will be activated in the next epoch. +/// Otherwise, it will be activated in the current epoch. +public(package) fun new( + name: String, + network_address: String, + public_key: vector, + network_public_key: vector, + proof_of_possession: vector, + commission_rate: u64, + storage_price: u64, + write_price: u64, + node_capacity: u64, + wctx: &WalrusContext, + ctx: &mut TxContext, +): StakingPool { + let id = object::new(ctx); + let node_id = id.to_inner(); + + // Verify proof of possession + assert!( + messages::new_proof_of_possession_msg( + wctx.epoch(), + ctx.sender(), + public_key, + ).verify_proof_of_possession(proof_of_possession), + EInvalidProofOfPossession, + ); + + let (activation_epoch, state) = if (wctx.committee_selected()) { + (wctx.epoch() + 1, PoolState::New) + } else { + (wctx.epoch(), PoolState::Active) + }; + + let mut exchange_rates = table::new(ctx); + exchange_rates.add(activation_epoch, pool_exchange_rate::empty()); + + StakingPool { + id, + state, + exchange_rates, + voting_params: VotingParams { + storage_price, + write_price, + node_capacity, + }, + node_info: storage_node::new( + name, + node_id, + network_address, + public_key, + network_public_key, + ), + commission_rate, + activation_epoch, + latest_epoch: wctx.epoch(), + pending_stake: pending_values::empty(), + pending_pool_token_withdraw: pending_values::empty(), + wal_balance: 0, + pool_token_balance: 0, + rewards_pool: balance::zero(), + } +} + +/// Set the state of the pool to `Withdrawing`. +public(package) fun set_withdrawing(pool: &mut StakingPool, wctx: &WalrusContext) { + assert!(!pool.is_withdrawing()); + pool.state = PoolState::Withdrawing(wctx.epoch() + 1); +} + +/// Stake the given amount of WAL in the pool. +public(package) fun stake( + pool: &mut StakingPool, + to_stake: Balance, + wctx: &WalrusContext, + ctx: &mut TxContext, +): StakedWal { + assert!(pool.is_active() || pool.is_new()); + assert!(to_stake.value() > 0); + + let current_epoch = wctx.epoch(); + let activation_epoch = if (wctx.committee_selected()) { + current_epoch + 2 + } else { + current_epoch + 1 + }; + + let staked_amount = to_stake.value(); + let staked_wal = staked_wal::mint( + pool.id.to_inner(), + to_stake, + activation_epoch, + ctx, + ); + + // Add the stake to the pending stake either for E+1 or E+2. + pool.pending_stake.insert_or_add(activation_epoch, staked_amount); + staked_wal +} + +/// Request withdrawal of the given amount from the staked WAL. +/// Marks the `StakedWal` as withdrawing and updates the activation epoch. +public(package) fun request_withdraw_stake( + pool: &mut StakingPool, + staked_wal: &mut StakedWal, + wctx: &WalrusContext, +) { + assert!(!pool.is_new()); + assert!(staked_wal.value() > 0); + assert!(staked_wal.node_id() == pool.id.to_inner()); + assert!(staked_wal.activation_epoch() <= wctx.epoch()); + + // If the node is in the committee, the stake will be withdrawn in E+2, + // otherwise in E+1. + let withdraw_epoch = if (wctx.committee_selected()) { + wctx.epoch() + 2 + } else { + wctx.epoch() + 1 + }; + + let principal_amount = staked_wal.value(); + let token_amount = pool + .exchange_rate_at_epoch(staked_wal.activation_epoch()) + .get_token_amount(principal_amount); + + pool.pending_pool_token_withdraw.insert_or_add(withdraw_epoch, token_amount); + staked_wal.set_withdrawing(withdraw_epoch, token_amount); +} + +/// Perform the withdrawal of the staked WAL, returning the amount to the caller. +public(package) fun withdraw_stake( + pool: &mut StakingPool, + staked_wal: StakedWal, + wctx: &WalrusContext, +): Balance { + assert!(!pool.is_new()); + assert!(staked_wal.value() > 0); + assert!(staked_wal.node_id() == pool.id.to_inner()); + assert!(staked_wal.withdraw_epoch() <= wctx.epoch()); + assert!(staked_wal.activation_epoch() <= wctx.epoch()); + assert!(staked_wal.is_withdrawing()); + + // withdraw epoch and pool token amount are stored in the `StakedWal` + let token_amount = staked_wal.pool_token_amount(); + let withdraw_epoch = staked_wal.withdraw_epoch(); + + // calculate the total amount to withdraw by converting token amount via the exchange rate + let total_amount = pool.exchange_rate_at_epoch(withdraw_epoch).get_wal_amount(token_amount); + let principal = staked_wal.into_balance(); + let rewards_amount = if (total_amount >= principal.value()) { + total_amount - principal.value() + } else 0; + + // withdraw rewards. due to rounding errors, there's a chance that the + // rewards amount is higher than the rewards pool, in this case, we + // withdraw the maximum amount possible + let rewards_amount = rewards_amount.min(pool.rewards_pool.value()); + let mut to_withdraw = pool.rewards_pool.split(rewards_amount); + to_withdraw.join(principal); + to_withdraw +} + +/// Advance epoch for the `StakingPool`. +public(package) fun advance_epoch( + pool: &mut StakingPool, + rewards: Balance, + wctx: &WalrusContext, +) { + // process the pending and withdrawal amounts + let current_epoch = wctx.epoch(); + + assert!(current_epoch > pool.latest_epoch, EPoolAlreadyUpdated); + assert!(rewards.value() == 0 || pool.wal_balance > 0, EIncorrectEpochAdvance); + + // if rewards are calculated only for full epochs, rewards addition should + // happen prior to pool token calculation. Otherwise we can add then to the + // final rate instead of the + let rewards_amount = rewards.value(); + pool.rewards_pool.join(rewards); + pool.wal_balance = pool.wal_balance + rewards_amount; + pool.latest_epoch = current_epoch; + pool.node_info.rotate_public_key(); + + process_pending_stake(pool, wctx) +} + +/// Process the pending stake and withdrawal requests for the pool. Called in the +/// `advance_epoch` function in case the pool is in the committee and receives the +/// rewards. And may be called in user-facing functions to update the pool state, +/// if the pool is not in the committee. +/// +/// Additions: +/// - `WAL` is added to the `wal_balance` directly. +/// - Pool Token is added to the `pool_token_balance` via the exchange rate. +/// +/// Withdrawals: +/// - `WAL` withdrawal is processed via the exchange rate and pool token. +/// - Pool Token withdrawal is processed directly. +public(package) fun process_pending_stake(pool: &mut StakingPool, wctx: &WalrusContext) { + let current_epoch = wctx.epoch(); + + // do the withdrawals reduction for both + let token_withdraw = pool.pending_pool_token_withdraw.flush(wctx.epoch()); + let exchange_rate = pool_exchange_rate::new( + pool.wal_balance, + pool.pool_token_balance, + ); + + let pending_withdrawal = exchange_rate.get_wal_amount(token_withdraw); + pool.pool_token_balance = pool.pool_token_balance - token_withdraw; + + // check that the amount is not higher than the pool balance + assert!(pool.wal_balance >= pending_withdrawal, ECalculationError); + pool.wal_balance = pool.wal_balance - pending_withdrawal; + + // recalculate the additions + pool.wal_balance = pool.wal_balance + pool.pending_stake.flush(current_epoch); + pool.pool_token_balance = exchange_rate.get_token_amount(pool.wal_balance); + pool.exchange_rates.add(current_epoch, exchange_rate); +} + +// === Pool parameters === + +/// Sets the next commission rate for the pool. +public(package) fun set_next_commission(pool: &mut StakingPool, commission_rate: u64) { + pool.commission_rate = commission_rate; +} + +/// Sets the next storage price for the pool. +public(package) fun set_next_storage_price(pool: &mut StakingPool, storage_price: u64) { + pool.voting_params.storage_price = storage_price; +} + +/// Sets the next write price for the pool. +public(package) fun set_next_write_price(pool: &mut StakingPool, write_price: u64) { + pool.voting_params.write_price = write_price; +} + +/// Sets the next node capacity for the pool. +public(package) fun set_next_node_capacity(pool: &mut StakingPool, node_capacity: u64) { + pool.voting_params.node_capacity = node_capacity; +} + +/// Sets the public key to be used starting from the next epoch for which the node is selected. +public(package) fun set_next_public_key( + self: &mut StakingPool, + public_key: vector, + proof_of_possession: vector, + wctx: &WalrusContext, + ctx: &TxContext, +) { + // Verify proof of possession + assert!( + messages::new_proof_of_possession_msg( + wctx.epoch(), + ctx.sender(), + public_key, + ).verify_proof_of_possession(proof_of_possession), + EInvalidProofOfPossession, + ); + self.node_info.set_next_public_key(public_key); +} + +/// Sets the name of the storage node. +public(package) fun set_name(self: &mut StakingPool, name: String) { + self.node_info.set_name(name); +} + +/// Sets the network address or host of the storage node. +public(package) fun set_network_address(self: &mut StakingPool, network_address: String) { + self.node_info.set_network_address(network_address); +} + +/// Sets the public key used for TLS communication. +public(package) fun set_network_public_key(self: &mut StakingPool, network_public_key: vector) { + self.node_info.set_network_public_key(network_public_key); +} + +/// Destroy the pool if it is empty. +public(package) fun destroy_empty(pool: StakingPool) { + assert!(pool.is_empty(), EPoolNotEmpty); + + let StakingPool { + id, + pending_stake, + exchange_rates, + rewards_pool, + .., + } = pool; + + id.delete(); + exchange_rates.drop(); + rewards_pool.destroy_zero(); + + let (_epochs, pending_stakes) = pending_stake.unwrap().into_keys_values(); + pending_stakes.do!(|stake| assert!(stake == 0)); +} + +/// Set the state of the pool to `Active`. +public(package) fun set_is_active(pool: &mut StakingPool) { + assert!(pool.is_new()); + pool.state = PoolState::Active; +} + +/// Returns the exchange rate for the given current or future epoch. If there +/// isn't a value for the specified epoch, it will look for the most recent +/// value down to the pool activation epoch. +public(package) fun exchange_rate_at_epoch(pool: &StakingPool, mut epoch: u32): PoolExchangeRate { + let activation_epoch = pool.activation_epoch; + while (epoch >= activation_epoch) { + if (pool.exchange_rates.contains(epoch)) { + return pool.exchange_rates[epoch] + }; + epoch = epoch - 1; + }; + + pool_exchange_rate::empty() +} + +/// Returns the expected active stake for current or future epoch `E` for the pool. +/// It processes the pending stake and withdrawal requests from the current epoch +/// to `E`. +/// +/// Should be the main function to calculate the active stake for the pool at +/// the given epoch, due to the complexity of the pending stake and withdrawal +/// requests, and lack of immediate updates. +public(package) fun wal_balance_at_epoch(pool: &StakingPool, epoch: u32): u64 { + let mut expected = pool.wal_balance; + let exchange_rate = pool_exchange_rate::new(pool.wal_balance, pool.pool_token_balance); + let token_withdraw = pool.pending_pool_token_withdraw.value_at(epoch); + let pending_withdrawal = exchange_rate.get_wal_amount(token_withdraw); + + expected = expected + pool.pending_stake.value_at(epoch); + expected = expected - pending_withdrawal; + expected +} + +// === Accessors === + +/// Returns the commission rate for the pool. +public(package) fun commission_rate(pool: &StakingPool): u64 { pool.commission_rate } + +/// Returns the rewards amount for the pool. +public(package) fun rewards_amount(pool: &StakingPool): u64 { pool.rewards_pool.value() } + +/// Returns the rewards for the pool. +public(package) fun wal_balance(pool: &StakingPool): u64 { pool.wal_balance } + +/// Returns the storage price for the pool. +public(package) fun storage_price(pool: &StakingPool): u64 { pool.voting_params.storage_price } + +/// Returns the write price for the pool. +public(package) fun write_price(pool: &StakingPool): u64 { pool.voting_params.write_price } + +/// Returns the node capacity for the pool. +public(package) fun node_capacity(pool: &StakingPool): u64 { pool.voting_params.node_capacity } + +/// Returns the activation epoch for the pool. +public(package) fun activation_epoch(pool: &StakingPool): u32 { pool.activation_epoch } + +/// Returns the node info for the pool. +public(package) fun node_info(pool: &StakingPool): &StorageNodeInfo { &pool.node_info } + +/// Returns `true` if the pool is empty. +public(package) fun is_new(pool: &StakingPool): bool { pool.state == PoolState::New } + +/// Returns `true` if the pool is active. +public(package) fun is_active(pool: &StakingPool): bool { pool.state == PoolState::Active } + +/// Returns `true` if the pool is withdrawing. +public(package) fun is_withdrawing(pool: &StakingPool): bool { + match (pool.state) { + PoolState::Withdrawing(_) => true, + _ => false, + } +} + +/// Returns `true` if the pool is empty. +public(package) fun is_empty(pool: &StakingPool): bool { + let pending_stake = pool.pending_stake.unwrap(); + let non_empty = pending_stake.keys().count!(|epoch| pending_stake[epoch] != 0); + + pool.wal_balance == 0 && non_empty == 0 && pool.pool_token_balance == 0 +} diff --git a/contracts/walrus/sources/staking/storage_node.move b/contracts/walrus/sources/staking/storage_node.move new file mode 100644 index 00000000..2d8c6c69 --- /dev/null +++ b/contracts/walrus/sources/staking/storage_node.move @@ -0,0 +1,158 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[allow(unused_field, unused_function, unused_variable, unused_use)] +module walrus::storage_node; + +use std::string::String; +use sui::{bls12381::{G1, g1_from_bytes}, group_ops::Element}; +use walrus::event_blob::EventBlobAttestation; + +// Error codes +// Keep errors in `walrus-sui/types/move_errors.rs` up to date with changes here. +const EInvalidNetworkPublicKey: u64 = 0; + +/// Represents a storage node in the system. +public struct StorageNodeInfo has store, copy, drop { + name: String, + node_id: ID, + network_address: String, + public_key: Element, + next_epoch_public_key: Option>, + network_public_key: vector, +} + +/// A Capability which represents a storage node and authorizes the holder to +/// perform operations on the storage node. +public struct StorageNodeCap has key, store { + id: UID, + node_id: ID, + last_epoch_sync_done: u32, + last_event_blob_attestation: Option, +} + +/// A public constructor for the StorageNodeInfo. +public(package) fun new( + name: String, + node_id: ID, + network_address: String, + public_key: vector, + network_public_key: vector, +): StorageNodeInfo { + assert!(network_public_key.length() == 33, EInvalidNetworkPublicKey); + StorageNodeInfo { + node_id, + name, + network_address, + public_key: g1_from_bytes(&public_key), + next_epoch_public_key: option::none(), + network_public_key, + } +} + +/// Create a new storage node capability. +public(package) fun new_cap(node_id: ID, ctx: &mut TxContext): StorageNodeCap { + StorageNodeCap { + id: object::new(ctx), + node_id, + last_epoch_sync_done: 0, + last_event_blob_attestation: option::none(), + } +} + +// === Accessors === + +/// Return the public key of the storage node. +public(package) fun public_key(self: &StorageNodeInfo): &Element { + &self.public_key +} + +/// Return the public key of the storage node for the next epoch. +public(package) fun next_epoch_public_key(self: &StorageNodeInfo): &Element { + self.next_epoch_public_key.borrow_with_default(&self.public_key) +} + +/// Return the node ID of the storage node. +public fun id(cap: &StorageNodeInfo): ID { cap.node_id } + +/// Return the pool ID of the storage node. +public fun node_id(cap: &StorageNodeCap): ID { cap.node_id } + +/// Return the last epoch in which the storage node attested that it has +/// finished syncing. +public fun last_epoch_sync_done(cap: &StorageNodeCap): u32 { + cap.last_epoch_sync_done +} + +/// Return the latest event blob attestion. +public fun last_event_blob_attestation(cap: &mut StorageNodeCap): Option { + cap.last_event_blob_attestation +} + +// === Modifiers === + +/// Set the last epoch in which the storage node attested that it has finished syncing. +public(package) fun set_last_epoch_sync_done(self: &mut StorageNodeCap, epoch: u32) { + self.last_epoch_sync_done = epoch; +} + +/// Set the last epoch in which the storage node attested that it has finished syncing. +public(package) fun set_last_event_blob_attestation( + self: &mut StorageNodeCap, + attestation: EventBlobAttestation, +) { + self.last_event_blob_attestation = option::some(attestation); +} + +/// Sets the public key to be used starting from the next epoch for which the node is selected. +public(package) fun set_next_public_key(self: &mut StorageNodeInfo, public_key: vector) { + self.next_epoch_public_key.swap_or_fill(g1_from_bytes(&public_key)); +} + +/// Sets the name of the storage node. +public(package) fun set_name(self: &mut StorageNodeInfo, name: String) { + self.name = name; +} + +/// Sets the network address or host of the storage node. +public(package) fun set_network_address(self: &mut StorageNodeInfo, network_address: String) { + self.network_address = network_address; +} + +/// Sets the public key used for TLS communication. +public(package) fun set_network_public_key( + self: &mut StorageNodeInfo, + network_public_key: vector, +) { + self.network_public_key = network_public_key; +} + +/// Set the public key to the next epochs public key. +public(package) fun rotate_public_key(self: &mut StorageNodeInfo) { + if (self.next_epoch_public_key.is_some()) { + self.public_key = self.next_epoch_public_key.extract() + } +} + +// === Testing === + +#[test_only] +/// Create a storage node with dummy name & address +public fun new_for_testing(public_key: vector): StorageNodeInfo { + let ctx = &mut tx_context::dummy(); + let node_id = ctx.fresh_object_address().to_id(); + StorageNodeInfo { + node_id, + name: b"node".to_string(), + network_address: b"127.0.0.1".to_string(), + public_key: g1_from_bytes(&public_key), + next_epoch_public_key: option::none(), + network_public_key: x"820e2b273530a00de66c9727c40f48be985da684286983f398ef7695b8a44677ab", + } +} + +#[test_only] +public fun destroy_cap_for_testing(cap: StorageNodeCap) { + let StorageNodeCap { id, .. } = cap; + id.delete(); +} diff --git a/contracts/walrus/sources/staking/walrus_context.move b/contracts/walrus/sources/staking/walrus_context.move new file mode 100644 index 00000000..03935228 --- /dev/null +++ b/contracts/walrus/sources/staking/walrus_context.move @@ -0,0 +1,42 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Module: `walrus_context` +/// +/// Implements the `WalrusContext` struct which is used to store the current +/// state of the system. Improves testing and readability of signatures by +/// aggregating the parameters into a single struct. Context is used almost +/// everywhere in the system, so it is important to have a single source of +/// truth for the current state. +module walrus::walrus_context; + +use sui::vec_map::VecMap; + +/// Represents the current values in the Walrus system. Helps avoid passing +/// too many parameters to functions, and allows for easier testing. +public struct WalrusContext has drop { + /// Current Walrus epoch + epoch: u32, + /// Whether the committee has been selected for the next epoch. + committee_selected: bool, + /// The current committee in the system. + committee: VecMap>, +} + +/// Create a new `WalrusContext` object. +public(package) fun new( + epoch: u32, + committee_selected: bool, + committee: VecMap>, +): WalrusContext { + WalrusContext { epoch, committee_selected, committee } +} + +/// Read the current `epoch` from the context. +public(package) fun epoch(self: &WalrusContext): u32 { self.epoch } + +/// Read the current `committee_selected` from the context. +public(package) fun committee_selected(self: &WalrusContext): bool { self.committee_selected } + +/// Read the current `committee` from the context. +public(package) fun committee(self: &WalrusContext): &VecMap> { &self.committee } diff --git a/contracts/walrus/sources/system.move b/contracts/walrus/sources/system.move new file mode 100644 index 00000000..480d648b --- /dev/null +++ b/contracts/walrus/sources/system.move @@ -0,0 +1,223 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[allow(unused_variable, unused_function, unused_field, unused_mut_parameter)] +/// Module: system +module walrus::system; + +use sui::{balance::Balance, coin::Coin, dynamic_object_field}; +use wal::wal::WAL; +use walrus::{ + blob::Blob, + bls_aggregate::BlsCommittee, + epoch_parameters::EpochParams, + storage_node::StorageNodeCap, + storage_resource::Storage, + system_state_inner::{Self, SystemStateInnerV1} +}; + +/// Flag to indicate the version of the system. +const VERSION: u64 = 0; + +/// The one and only system object. +public struct System has key { + id: UID, + version: u64, +} + +/// Creates and shares an empty system object. +/// Must only be called by the initialization function. +public(package) fun create_empty(max_epochs_ahead: u32, ctx: &mut TxContext) { + let mut system = System { id: object::new(ctx), version: VERSION }; + let system_state_inner = system_state_inner::create_empty(max_epochs_ahead, ctx); + dynamic_object_field::add(&mut system.id, VERSION, system_state_inner); + transfer::share_object(system); +} + +/// Marks blob as invalid given an invalid blob certificate. +public fun invalidate_blob_id( + system: &System, + signature: vector, + members: vector, + message: vector, +): u256 { + system.inner().invalidate_blob_id(signature, members, message) +} + +/// Certifies a blob containing Walrus events. +public fun certify_event_blob( + system: &mut System, + cap: &mut StorageNodeCap, + blob_id: u256, + root_hash: u256, + size: u64, + encoding_type: u8, + ending_checkpoint_sequence_num: u64, + epoch: u32, + ctx: &mut TxContext, +) { + system + .inner_mut() + .certify_event_blob( + cap, + blob_id, + root_hash, + size, + encoding_type, + ending_checkpoint_sequence_num, + epoch, + ctx, + ) +} + +/// Allows buying a storage reservation for a given period of epochs. +public fun reserve_space( + self: &mut System, + storage_amount: u64, + epochs_ahead: u32, + payment: &mut Coin, + ctx: &mut TxContext, +): Storage { + self.inner_mut().reserve_space(storage_amount, epochs_ahead, payment, ctx) +} + +/// Registers a new blob in the system. +/// `size` is the size of the unencoded blob. The reserved space in `storage` must be at +/// least the size of the encoded blob. +public fun register_blob( + self: &mut System, + storage: Storage, + blob_id: u256, + root_hash: u256, + size: u64, + encoding_type: u8, + deletable: bool, + write_payment: &mut Coin, + ctx: &mut TxContext, +): Blob { + self + .inner_mut() + .register_blob( + storage, + blob_id, + root_hash, + size, + encoding_type, + deletable, + write_payment, + ctx, + ) +} + +/// Certify that a blob will be available in the storage system until the end epoch of the +/// storage associated with it. +public fun certify_blob( + self: &System, + blob: &mut Blob, + signature: vector, + signers: vector, + message: vector, +) { + self.inner().certify_blob(blob, signature, signers, message); +} + +/// Deletes a deletable blob and returns the contained storage resource. +public fun delete_blob(self: &System, blob: Blob): Storage { + self.inner().delete_blob(blob) +} + +/// Extend the period of validity of a blob with a new storage resource. +/// The new storage resource must be the same size as the storage resource +/// used in the blob, and have a longer period of validity. +public fun extend_blob_with_resource(self: &System, blob: &mut Blob, extension: Storage) { + self.inner().extend_blob_with_resource(blob, extension); +} + +/// Extend the period of validity of a blob by extending its contained storage resource. +public fun extend_blob( + self: &mut System, + blob: &mut Blob, + epochs_ahead: u32, + payment: &mut Coin, +) { + self.inner_mut().extend_blob(blob, epochs_ahead, payment); +} + +// === Public Accessors === + +/// Get epoch. Uses the committee to get the epoch. +public fun epoch(self: &System): u32 { + self.inner().epoch() +} + +/// Accessor for total capacity size. +public fun total_capacity_size(self: &System): u64 { + self.inner().total_capacity_size() +} + +/// Accessor for used capacity size. +public fun used_capacity_size(self: &System): u64 { + self.inner().used_capacity_size() +} + +/// Accessor for the number of shards. +public fun n_shards(self: &System): u16 { + self.inner().n_shards() +} + +// === Restricted to Package === + +/// Accessor for the current committee. +public(package) fun committee(self: &System): &BlsCommittee { + self.inner().committee() +} + +#[test_only] +public(package) fun committee_mut(self: &mut System): &mut BlsCommittee { + self.inner_mut().committee_mut() +} + +/// Update epoch to next epoch, and update the committee, price and capacity. +/// +/// Called by the epoch change function that connects `Staking` and `System`. Returns +/// the balance of the rewards from the previous epoch. +public(package) fun advance_epoch( + self: &mut System, + new_committee: BlsCommittee, + new_epoch_params: EpochParams, +): Balance { + self.inner_mut().advance_epoch(new_committee, new_epoch_params) +} + +// === Internals === + +/// Get a mutable reference to `SystemStateInner` from the `System`. +fun inner_mut(system: &mut System): &mut SystemStateInnerV1 { + assert!(system.version == VERSION); + dynamic_object_field::borrow_mut(&mut system.id, VERSION) +} + +/// Get an immutable reference to `SystemStateInner` from the `System`. +public(package) fun inner(system: &System): &SystemStateInnerV1 { + assert!(system.version == VERSION); + dynamic_object_field::borrow(&system.id, VERSION) +} + +// === Testing === + +#[test_only] +public(package) fun new_for_testing(): System { + let ctx = &mut tx_context::dummy(); + let mut system = System { id: object::new(ctx), version: VERSION }; + let system_state_inner = system_state_inner::new_for_testing(); + dynamic_object_field::add(&mut system.id, VERSION, system_state_inner); + system +} + +#[test_only] +public(package) fun new_for_testing_with_multiple_members(ctx: &mut TxContext): System { + let mut system = System { id: object::new(ctx), version: VERSION }; + let system_state_inner = system_state_inner::new_for_testing_with_multiple_members(ctx); + dynamic_object_field::add(&mut system.id, VERSION, system_state_inner); + system +} diff --git a/contracts/walrus/sources/system/blob.move b/contracts/walrus/sources/system/blob.move new file mode 100644 index 00000000..e5df5846 --- /dev/null +++ b/contracts/walrus/sources/system/blob.move @@ -0,0 +1,307 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module walrus::blob; + +use sui::{bcs, dynamic_field, hash}; +use std::string::String; +use walrus::{ + encoding, + events::{emit_blob_registered, emit_blob_certified, emit_blob_deleted}, + messages::CertifiedBlobMessage, + metadata::Metadata, + storage_resource::Storage, +}; + +// Error codes +// Keep errors in `walrus-sui/types/move_errors.rs` up to date with changes here. +const ENotCertified: u64 = 0; +const EBlobNotDeletable: u64 = 1; +const EResourceBounds: u64 = 2; +const EResourceSize: u64 = 3; +const EWrongEpoch: u64 = 4; +const EAlreadyCertified: u64 = 5; +const EInvalidBlobId: u64 = 6; + +// The fixed dynamic filed name for metadata +const METADATA_DF: vector = b"metadata"; + +// === Object definitions === + +/// The blob structure represents a blob that has been registered to with some storage, +/// and then may eventually be certified as being available in the system. +public struct Blob has key, store { + id: UID, + registered_epoch: u32, + blob_id: u256, + size: u64, + encoding_type: u8, + // Stores the epoch first certified. + certified_epoch: option::Option, + storage: Storage, + // Marks if this blob can be deleted. + deletable: bool, +} + +// === Accessors === + +public fun registered_epoch(self: &Blob): u32 { + self.registered_epoch +} + +public fun blob_id(self: &Blob): u256 { + self.blob_id +} + +public fun size(self: &Blob): u64 { + self.size +} + +public fun encoding_type(self: &Blob): u8 { + self.encoding_type +} + +public fun certified_epoch(self: &Blob): &Option { + &self.certified_epoch +} + +public fun storage(self: &Blob): &Storage { + &self.storage +} + +public fun encoded_size(self: &Blob, n_shards: u16): u64 { + encoding::encoded_blob_length( + self.size, + self.encoding_type, + n_shards, + ) +} + +public(package) fun storage_mut(self: &mut Blob): &mut Storage { + &mut self.storage +} + +public fun end_epoch(self: &Blob): u32 { + self.storage.end_epoch() +} + +/// Aborts if the blob is not certified or already expired. +public(package) fun assert_certified_not_expired(self: &Blob, current_epoch: u32) { + // Assert this is a certified blob + assert!(self.certified_epoch.is_some(), ENotCertified); + + // Check the blob is within its availability period + assert!(current_epoch < self.storage.end_epoch(), EResourceBounds); +} + +public struct BlobIdDerivation has drop { + encoding_type: u8, + size: u64, + root_hash: u256, +} + +/// Derives the blob_id for a blob given the root_hash, encoding_type and size. +public(package) fun derive_blob_id(root_hash: u256, encoding_type: u8, size: u64): u256 { + let blob_id_struct = BlobIdDerivation { + encoding_type, + size, + root_hash, + }; + + let serialized = bcs::to_bytes(&blob_id_struct); + let encoded = hash::blake2b256(&serialized); + let mut decoder = bcs::new(encoded); + let blob_id = decoder.peel_u256(); + blob_id +} + +/// Creates a new blob in `registered_epoch`. +/// `size` is the size of the unencoded blob. The reserved space in `storage` must be at +/// least the size of the encoded blob. +public(package) fun new( + storage: Storage, + blob_id: u256, + root_hash: u256, + size: u64, + encoding_type: u8, + deletable: bool, + registered_epoch: u32, + n_shards: u16, + ctx: &mut TxContext, +): Blob { + let id = object::new(ctx); + + // Check resource bounds. + assert!(registered_epoch >= storage.start_epoch(), EResourceBounds); + assert!(registered_epoch < storage.end_epoch(), EResourceBounds); + + // check that the encoded size is less than the storage size + let encoded_size = encoding::encoded_blob_length( + size, + encoding_type, + n_shards, + ); + assert!(encoded_size <= storage.storage_size(), EResourceSize); + + // Cryptographically verify that the Blob ID authenticates + // both the size and fe_type. + assert!(derive_blob_id(root_hash, encoding_type, size) == blob_id, EInvalidBlobId); + + // Emit register event + emit_blob_registered( + registered_epoch, + blob_id, + size, + encoding_type, + storage.end_epoch(), + deletable, + id.to_inner(), + ); + + Blob { + id, + registered_epoch, + blob_id, + size, + encoding_type, + certified_epoch: option::none(), + storage, + deletable, + } +} + +/// Certifies that a blob will be available in the storage system until the end epoch of the +/// storage associated with it, given a [`CertifiedBlobMessage`]. +public(package) fun certify_with_certified_msg( + blob: &mut Blob, + current_epoch: u32, + message: CertifiedBlobMessage, +) { + // Check that the blob is registered in the system + assert!(blob_id(blob) == message.certified_blob_id(), EInvalidBlobId); + + // Check that the blob is not already certified + assert!(!blob.certified_epoch.is_some(), EAlreadyCertified); + + // Check that the message is from the current epoch + assert!(message.certified_epoch() == current_epoch, EWrongEpoch); + + // Check that the storage in the blob is still valid + assert!(message.certified_epoch() < blob.storage.end_epoch(), EResourceBounds); + + // Mark the blob as certified + blob.certified_epoch.fill(message.certified_epoch()); + + blob.emit_certified(false); +} + +/// Deletes a deletable blob and returns the contained storage. +/// +/// Emits a `BlobDeleted` event for the given epoch. +/// Aborts if the Blob is not deletable or already expired. +public(package) fun delete(self: Blob, epoch: u32): Storage { + let Blob { + id, + storage, + deletable, + blob_id, + certified_epoch, + .., + } = self; + assert!(deletable, EBlobNotDeletable); + assert!(storage.end_epoch() > epoch, EResourceBounds); + let object_id = id.to_inner(); + id.delete(); + emit_blob_deleted(epoch, blob_id, storage.end_epoch(), object_id, certified_epoch.is_some()); + storage +} + +/// Allows calling `.share()` on a `Blob` to wrap it into a shared `SharedBlob` whose lifetime can +/// be extended by anyone. +public use fun walrus::shared_blob::new as Blob.share; + +/// Allow the owner of a blob object to destroy it. +public fun burn(blob: Blob) { + let Blob { + id, + storage, + .., + } = blob; + + id.delete(); + storage.destroy(); +} + +/// Extend the period of validity of a blob with a new storage resource. +/// The new storage resource must be the same size as the storage resource +/// used in the blob, and have a longer period of validity. +public(package) fun extend_with_resource(blob: &mut Blob, extension: Storage, current_epoch: u32) { + // We only extend certified blobs within their period of validity + // with storage that extends this period. First we check for these + // conditions. + + blob.assert_certified_not_expired(current_epoch); + + // Check that the extension is valid, and the end + // period of the extension is after the current period. + assert!(extension.end_epoch() > blob.storage.end_epoch(), EResourceBounds); + + // Note: if the amounts do not match there will be an abort here. + blob.storage.fuse_periods(extension); + + blob.emit_certified(true); +} + +/// Emits a `BlobCertified` event for the given blob. +public(package) fun emit_certified(self: &Blob, is_extension: bool) { + // Emit certified event + // + // Note: We use the original certified period also for extensions since + // for the purposes of reconfiguration this is the committee that has a + // quorum that hold the resource. + emit_blob_certified( + *self.certified_epoch.borrow(), + self.blob_id, + self.storage.end_epoch(), + self.deletable, + self.id.to_inner(), + is_extension, + ); +} + +// === Metadata === + +/// Adds the metadata dynamic field to the Blob. +/// +/// Aborts if the metadata is already present. +public fun add_metadata(self: &mut Blob, metadata: Metadata) { + dynamic_field::add(&mut self.id, METADATA_DF, metadata) +} + +/// Removes the metadata dynamic field from the Blob, returning the contained `Metadata`. +/// +/// Aborts if the metadata does not exist. +public fun take_metadata(self: &mut Blob): Metadata { + dynamic_field::remove(&mut self.id, METADATA_DF) +} + +/// Returns the metadata associated with the Blob. +/// +/// Aborts if the metadata does not exist. +fun metadata(self: &mut Blob): &mut Metadata { + dynamic_field::borrow_mut(&mut self.id, METADATA_DF) +} + +/// Inserts a key-value pair into the metadata. +/// +/// If the key is already present, the value is updated. Aborts if the metadata does not exist. +public fun insert_or_update_metadata_pair(self: &mut Blob, key: String, value: String) { + self.metadata().insert_or_update(key, value) +} + +/// Removes the metadata associated with the given key. +/// +/// Aborts if the metadata does not exist. +public fun remove_metadata_pair(self: &mut Blob, key: &String): (String, String) { + self.metadata().remove(key) +} diff --git a/contracts/walrus/sources/system/bls_aggregate.move b/contracts/walrus/sources/system/bls_aggregate.move new file mode 100644 index 00000000..b378a52e --- /dev/null +++ b/contracts/walrus/sources/system/bls_aggregate.move @@ -0,0 +1,199 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module walrus::bls_aggregate; + +use sui::bls12381::{Self, bls12381_min_pk_verify, G1}; +use sui::group_ops::{Self, Element}; +use sui::vec_map::{Self, VecMap}; +use walrus::messages::{Self, CertifiedMessage}; + +// Error codes +const ETotalMemberOrder: u64 = 0; +const ESigVerification: u64 = 1; +const ENotEnoughStake: u64 = 2; +const EIncorrectCommittee: u64 = 3; + +public struct BlsCommitteeMember has store, copy, drop { + public_key: Element, + weight: u16, + node_id: ID, +} + +/// This represents a BLS signing committee for a given epoch. +public struct BlsCommittee has store, copy, drop { + /// A vector of committee members + members: vector, + /// The total number of shards held by the committee + n_shards: u16, + /// The epoch in which the committee is active. + epoch: u32, +} + +/// Constructor for committee. +public(package) fun new_bls_committee( + epoch: u32, + members: vector, +): BlsCommittee { + // Compute the total number of shards + let mut n_shards = 0; + members.do_ref!(|member| { + let weight = member.weight; + assert!(weight > 0, EIncorrectCommittee); + n_shards = n_shards + weight; + }); + + BlsCommittee { members, n_shards, epoch } +} + +/// Constructor for committee member. +public(package) fun new_bls_committee_member( + public_key: Element, + weight: u16, + node_id: ID, +): BlsCommitteeMember { + BlsCommitteeMember { + public_key, + weight, + node_id, + } +} + +// === Accessors for BlsCommitteeMember === + +/// Get the node id of the committee member. +public(package) fun node_id(self: &BlsCommitteeMember): sui::object::ID { + self.node_id +} + +// === Accessors for BlsCommittee === + +/// Get the epoch of the committee. +public(package) fun epoch(self: &BlsCommittee): u32 { + self.epoch +} + +/// Returns the number of shards held by the committee. +public(package) fun n_shards(self: &BlsCommittee): u16 { + self.n_shards +} + +/// Returns the member at given index +public(package) fun get_idx(self: &BlsCommittee, idx: u64): &BlsCommitteeMember { + self.members.borrow(idx) +} + +/// Checks if the committee contains a given node. +public(package) fun contains(self: &BlsCommittee, node_id: &ID): bool { + self.find_index(node_id).is_some() +} + +/// Returns the member weight if it is part of the committee or 0 otherwise +public(package) fun get_member_weight(self: &BlsCommittee, node_id: &ID): u16 { + self.find_index(node_id).and!(|idx| { + let member = &self.members[idx]; + option::some(member.weight) + }).get_with_default(0) +} + +/// Finds the index of the member by node_id +public(package) fun find_index(self: &BlsCommittee, node_id: &ID): std::option::Option { + self.members.find_index!(|member| &member.node_id == node_id) +} + +/// Returns the members of the committee with their weights. +public(package) fun to_vec_map(self: &BlsCommittee): VecMap { + let mut result = vec_map::empty(); + self.members.do_ref!(|member| { + result.insert(member.node_id, member.weight) + }); + result +} + +/// Verifies that a message is signed by a quorum of the members of a committee. +/// +/// The signers are listed as indices into the `members` vector of the committee +/// in increasing +/// order and with no repetitions. The total weight of the signers (i.e. total +/// number of shards) +/// is returned, but if a quorum is not reached the function aborts with an +/// error. +public(package) fun verify_quorum_in_epoch( + self: &BlsCommittee, + signature: vector, + signers: vector, + message: vector, +): CertifiedMessage { + let stake_support = self.verify_certificate( + &signature, + &signers, + &message, + ); + + messages::new_certified_message(message, self.epoch, stake_support) +} + +/// Returns true if the weight is more than the aggregate weight of quorum members of a committee. +public(package) fun verify_quorum(self: &BlsCommittee, weight: u16): bool { + 3 * (weight as u64) >= 2 * (self.n_shards as u64) + 1 +} + +/// Verify an aggregate BLS signature is a certificate in the epoch, and return +/// the type of +/// certificate and the bytes certified. The `signers` vector is an increasing +/// list of indexes +/// into the `members` vector of the committee. If there is a certificate, the +/// function +/// returns the total stake. Otherwise, it aborts. +public(package) fun verify_certificate( + self: &BlsCommittee, + signature: &vector, + signers: &vector, + message: &vector, +): u16 { + // Use the signers flags to construct the key and the weights. + + // Lower bound for the next `member_index` to ensure they are monotonically + // increasing + let mut min_next_member_index = 0; + let mut aggregate_key = bls12381::g1_identity(); + let mut aggregate_weight = 0; + + signers.do_ref!(|member_index| { + let member_index = *member_index as u64; + assert!(member_index >= min_next_member_index, ETotalMemberOrder); + min_next_member_index = member_index + 1; + + // Bounds check happens here + let member = &self.members[member_index]; + let key = &member.public_key; + let weight = member.weight; + + aggregate_key = bls12381::g1_add(&aggregate_key, key); + aggregate_weight = aggregate_weight + weight; + }); + + // The expression below is the solution to the inequality: + // n_shards = 3 f + 1 + // stake >= 2f + 1 + assert!(verify_quorum(self, aggregate_weight), ENotEnoughStake); + + // Verify the signature + let pub_key_bytes = group_ops::bytes(&aggregate_key); + assert!( + bls12381_min_pk_verify( + signature, + pub_key_bytes, + message, + ), + ESigVerification, + ); + + (aggregate_weight as u16) +} + +#[test_only] +/// Increments the committee epoch by one. +public fun increment_epoch_for_testing(self: &mut BlsCommittee) { + self.epoch = self.epoch + 1; +} diff --git a/contracts/walrus/sources/system/encoding.move b/contracts/walrus/sources/system/encoding.move new file mode 100644 index 00000000..4242c2ba --- /dev/null +++ b/contracts/walrus/sources/system/encoding.move @@ -0,0 +1,20 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module walrus::encoding; + +use walrus::redstuff; + +// Supported Encoding Types +const RED_STUFF_ENCODING: u8 = 0; + +// Errors +const EInvalidEncoding: u64 = 0; + +/// Computes the encoded length of a blob given its unencoded length, encoding type +/// and number of shards `n_shards`. +public fun encoded_blob_length(unencoded_length: u64, encoding_type: u8, n_shards: u16): u64 { + // Currently only supports a single encoding type + assert!(encoding_type == RED_STUFF_ENCODING, EInvalidEncoding); + redstuff::encoded_blob_length(unencoded_length, n_shards) +} diff --git a/contracts/walrus/sources/system/epoch_parameters.move b/contracts/walrus/sources/system/epoch_parameters.move new file mode 100644 index 00000000..414b0d44 --- /dev/null +++ b/contracts/walrus/sources/system/epoch_parameters.move @@ -0,0 +1,55 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module walrus::epoch_parameters; + +/// The epoch parameters for the system. +public struct EpochParams has store, copy, drop { + /// The storage capacity of the system. + total_capacity_size: u64, + /// The price per unit size of storage. + storage_price_per_unit_size: u64, + /// The write price per unit size. + write_price_per_unit_size: u64, +} + +// === Constructor === + +public(package) fun new( + total_capacity_size: u64, + storage_price_per_unit_size: u64, + write_price_per_unit_size: u64, +): EpochParams { + EpochParams { + total_capacity_size, + storage_price_per_unit_size, + write_price_per_unit_size, + } +} + +// === Accessors === + +/// The storage capacity of the system. +public(package) fun capacity(self: &EpochParams): u64 { + self.total_capacity_size +} + +/// The price per unit size of storage. +public(package) fun storage_price(self: &EpochParams): u64 { + self.storage_price_per_unit_size +} + +/// The write price per unit size. +public(package) fun write_price(self: &EpochParams): u64 { + self.write_price_per_unit_size +} + +// === Test only === + +public fun epoch_params_for_testing(): EpochParams { + EpochParams { + total_capacity_size: 1_000_000_000, + storage_price_per_unit_size: 5, + write_price_per_unit_size: 1, + } +} diff --git a/contracts/walrus/sources/system/event_blob.move b/contracts/walrus/sources/system/event_blob.move new file mode 100644 index 00000000..e854166b --- /dev/null +++ b/contracts/walrus/sources/system/event_blob.move @@ -0,0 +1,159 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Module to certify event blobs. +module walrus::event_blob; + +use sui::vec_map::VecMap; + +// === Definitions related to event blob certification === + +/// Event blob index which was attested by a storage node. +public struct EventBlobAttestation has store, copy, drop { + checkpoint_sequence_num: u64, + epoch: u32, +} + +/// State of a certified event blob. +public struct EventBlob has copy, store, drop { + /// Blob id of the certified event blob. + blob_id: u256, + /// Ending sui checkpoint of the certified event blob. + ending_checkpoint_sequence_number: u64, +} + +/// State of event blob stream. +#[allow(unused_field)] +public struct EventBlobCertificationState has key, store { + id: UID, + /// Latest certified event blob. + latest_certified_blob: Option, + /// Current event blob being attested. + aggregate_weight_per_blob: VecMap, +} + +// === Accessors related to event blob attestation === + +public(package) fun new_attestation( + checkpoint_sequence_num: u64, + epoch: u32, +): EventBlobAttestation { + EventBlobAttestation { + checkpoint_sequence_num, + epoch, + } +} + +public(package) fun last_attested_event_blob_checkpoint_seq_num(self: &EventBlobAttestation): u64 { + self.checkpoint_sequence_num +} + +public(package) fun last_attested_event_blob_epoch(self: &EventBlobAttestation): u32 { self.epoch } + +// === Accessors for EventBlob === + +public(package) fun new_event_blob( + ending_checkpoint_sequence_number: u64, + blob_id: u256, +): EventBlob { + EventBlob { + blob_id, + ending_checkpoint_sequence_number, + } +} + +/// Returns the blob id of the event blob +public(package) fun blob_id(self: &EventBlob): u256 { + self.blob_id +} + +/// Returns the ending checkpoint sequence number of the event blob +public(package) fun ending_checkpoint_sequence_number(self: &EventBlob): u64 { + self.ending_checkpoint_sequence_number +} + +// === Accessors for EventBlobCertificationState === + +/// Creates a blob state with no signers and no last checkpoint sequence number +public(package) fun create_with_empty_state(ctx: &mut TxContext): EventBlobCertificationState { + let id = object::new(ctx); + EventBlobCertificationState { + id, + latest_certified_blob: option::none(), + aggregate_weight_per_blob: sui::vec_map::empty(), + } +} + +/// Returns the blob id of the latest certified event blob +public(package) fun get_latest_certified_blob_id(self: &EventBlobCertificationState): Option { + self.latest_certified_blob.map!(|state| state.blob_id()) +} + +/// Returns the checkpoint sequence number of the latest certified event +/// blob +public(package) fun get_latest_certified_checkpoint_sequence_number( + self: &EventBlobCertificationState, +): Option { + self.latest_certified_blob.map!(|state| state.ending_checkpoint_sequence_number()) +} + +/// Returns true if a blob is already certified or false otherwise +public(package) fun is_blob_already_certified( + self: &EventBlobCertificationState, + ending_checkpoint_sequence_num: u64, +): bool { + self + .get_latest_certified_checkpoint_sequence_number() + .map!( + | + latest_certified_sequence_num, + | latest_certified_sequence_num >= ending_checkpoint_sequence_num, + ) + .get_with_default(false) +} + +/// Updates the latest certified event blob +public(package) fun update_latest_certified_event_blob( + self: &mut EventBlobCertificationState, + checkpoint_sequence_number: u64, + blob_id: u256, +) { + self.get_latest_certified_checkpoint_sequence_number().do!(|latest_certified_sequence_num| { + assert!(checkpoint_sequence_number > latest_certified_sequence_num); + }); + self.latest_certified_blob = + option::some( + new_event_blob(checkpoint_sequence_number, blob_id), + ); +} + +/// Update the aggregate weight of an event blob +public(package) fun update_aggregate_weight( + self: &mut EventBlobCertificationState, + blob_id: u256, + weight: u16, +): u16 { + let agg_weight = self.aggregate_weight_per_blob.get_mut(&blob_id); + *agg_weight = *agg_weight + weight; + *agg_weight +} + +/// Start tracking which nodes are signing the event blob with given id for +/// event blob certification +public(package) fun start_tracking_blob(self: &mut EventBlobCertificationState, blob_id: u256) { + if (!self.aggregate_weight_per_blob.contains(&blob_id)) { + self.aggregate_weight_per_blob.insert(blob_id, 0); + }; +} + +/// Stop tracking nodes for the given blob id +public(package) fun stop_tracking_blob(self: &mut EventBlobCertificationState, blob_id: u256) { + if (self.aggregate_weight_per_blob.contains(&blob_id)) { + self.aggregate_weight_per_blob.remove(&blob_id); + }; +} + +/// Reset blob certification state upon epoch change +public(package) fun reset(self: &mut EventBlobCertificationState) { + self.aggregate_weight_per_blob = sui::vec_map::empty(); +} diff --git a/contracts/walrus/sources/system/events.move b/contracts/walrus/sources/system/events.move new file mode 100644 index 00000000..7acccdae --- /dev/null +++ b/contracts/walrus/sources/system/events.move @@ -0,0 +1,146 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Module to emit events. Used to allow filtering all events in the +/// rust client (as work-around for the lack of composable event filters). +module walrus::events; + +use sui::event; + +// === Event definitions === + +/// Signals that a blob with meta-data has been registered. +public struct BlobRegistered has copy, drop { + epoch: u32, + blob_id: u256, + size: u64, + encoding_type: u8, + end_epoch: u32, + deletable: bool, + // The object id of the related `Blob` object + object_id: ID, +} + +/// Signals that a blob is certified. +public struct BlobCertified has copy, drop { + epoch: u32, + blob_id: u256, + end_epoch: u32, + deletable: bool, + // The object id of the related `Blob` object + object_id: ID, + // Marks if this is an extension for explorers, etc. + is_extension: bool, +} + +/// Signals that a blob has been deleted. +public struct BlobDeleted has copy, drop { + epoch: u32, + blob_id: u256, + end_epoch: u32, + // The object ID of the related `Blob` object. + object_id: ID, + // If the blob object was previously certified. + was_certified: bool, +} + +/// Signals that a BlobID is invalid. +public struct InvalidBlobID has copy, drop { + epoch: u32, // The epoch in which the blob ID is first registered as invalid + blob_id: u256, +} + +/// Signals that epoch `epoch` has started and the epoch change is in progress. +public struct EpochChangeStart has copy, drop { + epoch: u32, +} + +/// Signals that a set of storage nodes holding at least 2f+1 shards have finished the epoch +/// change, i.e., received all of their assigned shards. +public struct EpochChangeDone has copy, drop { + epoch: u32, +} + +/// Signals that a node has received the specified shards for the new epoch. +public struct ShardsReceived has copy, drop { + epoch: u32, + shards: vector, +} + +/// Signals that the committee and the system parameters for `next_epoch` have been selected. +public struct EpochParametersSelected has copy, drop { + next_epoch: u32, +} + +/// Signals that the given shards can be recovered using the shard recovery endpoint. +public struct ShardRecoveryStart has copy, drop { + epoch: u32, + shards: vector, +} + +// === Functions to emit the events from other modules === + +public(package) fun emit_blob_registered( + epoch: u32, + blob_id: u256, + size: u64, + encoding_type: u8, + end_epoch: u32, + deletable: bool, + object_id: ID, +) { + event::emit(BlobRegistered { + epoch, + blob_id, + size, + encoding_type, + end_epoch, + deletable, + object_id, + }); +} + +public(package) fun emit_blob_certified( + epoch: u32, + blob_id: u256, + end_epoch: u32, + deletable: bool, + object_id: ID, + is_extension: bool, +) { + event::emit(BlobCertified { epoch, blob_id, end_epoch, deletable, object_id, is_extension }); +} + +public(package) fun emit_invalid_blob_id(epoch: u32, blob_id: u256) { + event::emit(InvalidBlobID { epoch, blob_id }); +} + +public(package) fun emit_blob_deleted( + epoch: u32, + blob_id: u256, + end_epoch: u32, + object_id: ID, + was_certified: bool +) { + event::emit(BlobDeleted { epoch, blob_id, end_epoch, object_id, was_certified }); +} + +public(package) fun emit_epoch_change_start(epoch: u32) { + event::emit(EpochChangeStart { epoch }) +} + +public(package) fun emit_epoch_change_done(epoch: u32) { + event::emit(EpochChangeDone { epoch }) +} + +public(package) fun emit_shards_received(epoch: u32, shards: vector) { + event::emit(ShardsReceived { epoch, shards }) +} + +public(package) fun emit_epoch_parameters_selected(next_epoch: u32) { + event::emit(EpochParametersSelected { next_epoch }) +} + +public(package) fun emit_shard_recovery_start(epoch: u32, shards: vector) { + event::emit(ShardRecoveryStart { epoch, shards }) +} diff --git a/contracts/walrus/sources/system/messages.move b/contracts/walrus/sources/system/messages.move new file mode 100644 index 00000000..bece4669 --- /dev/null +++ b/contracts/walrus/sources/system/messages.move @@ -0,0 +1,273 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module walrus::messages; + +use sui::{bcs, bls12381::bls12381_min_pk_verify}; + +const APP_ID: u8 = 3; +const INTENT_VERSION: u8 = 0; + +const BLS_KEY_LEN: u64 = 48; + +// Message Types +const PROOF_OF_POSSESSION_MSG_TYPE: u8 = 0; +const BLOB_CERT_MSG_TYPE: u8 = 1; +const INVALID_BLOB_ID_MSG_TYPE: u8 = 2; + +// Errors +const EIncorrectAppId: u64 = 0; +const EIncorrectEpoch: u64 = 1; +const EInvalidMsgType: u64 = 2; +const EIncorrectIntentVersion: u64 = 3; + +#[error] +const EInvalidKeyLength: vector = b"The length of the provided bls key is incorrect."; + +/// Message signed by a BLS key in the proof of possession. +public struct ProofOfPossessionMessage has drop { + intent_type: u8, + intent_version: u8, + intent_app: u8, + epoch: u32, + sui_address: address, + bls_key: vector, +} + +/// Creates a new ProofOfPossessionMessage given the expected epoch, sui address and BLS key. +public(package) fun new_proof_of_possession_msg( + epoch: u32, + sui_address: address, + bls_key: vector, +): ProofOfPossessionMessage { + assert!(bls_key.length() == BLS_KEY_LEN, EInvalidKeyLength); + ProofOfPossessionMessage { + intent_type: PROOF_OF_POSSESSION_MSG_TYPE, + intent_version: INTENT_VERSION, + intent_app: APP_ID, + epoch, + sui_address, + bls_key, + } +} + +/// BCS encodes a ProofOfPossessionMessage, considering the BLS key as a fixed-length byte +/// array with 48 bytes. +public(package) fun to_bcs(self: &ProofOfPossessionMessage): vector { + let mut bcs = vector[]; + bcs.append(bcs::to_bytes(&self.intent_type)); + bcs.append(bcs::to_bytes(&self.intent_version)); + bcs.append(bcs::to_bytes(&self.intent_app)); + bcs.append(bcs::to_bytes(&self.epoch)); + bcs.append(bcs::to_bytes(&self.sui_address)); + self.bls_key.do_ref!(|key_byte| bcs.append(bcs::to_bytes(key_byte))); + bcs +} + +/// Verify the provided proof of possession using the contained public key and the provided +/// signature. +public(package) fun verify_proof_of_possession( + self: &ProofOfPossessionMessage, + pop_signature: vector, +): bool { + let message_bytes = self.to_bcs(); + bls12381_min_pk_verify( + &pop_signature, + &self.bls_key, + &message_bytes, + ) +} + +/// A message certified by nodes holding `stake_support` shards. +public struct CertifiedMessage has drop { + intent_type: u8, + intent_version: u8, + cert_epoch: u32, + stake_support: u16, + message: vector, +} + +/// Message type for certifying a blob. +/// +/// Constructed from a `CertifiedMessage`, states that `blob_id` has been certified in `epoch` +/// by a quorum. +public struct CertifiedBlobMessage has drop { + epoch: u32, + blob_id: u256, +} + +/// Message type for Invalid Blob Certificates. +/// +/// Constructed from a `CertifiedMessage`, states that `blob_id` has been marked as invalid +/// in `epoch` by a quorum. +public struct CertifiedInvalidBlobId has drop { + epoch: u32, + blob_id: u256, +} + +/// Creates a `CertifiedMessage` with support `stake_support` by parsing `message_bytes` and +/// verifying the intent and the message epoch. +public(package) fun new_certified_message( + message_bytes: vector, + committee_epoch: u32, + stake_support: u16, +): CertifiedMessage { + // Here we BCS decode the header of the message to check intents, epochs, etc. + + let mut bcs_message = bcs::new(message_bytes); + let intent_type = bcs_message.peel_u8(); + let intent_version = bcs_message.peel_u8(); + assert!(intent_version == INTENT_VERSION, EIncorrectIntentVersion); + + let intent_app = bcs_message.peel_u8(); + assert!(intent_app == APP_ID, EIncorrectAppId); + + let cert_epoch = bcs_message.peel_u32(); + assert!(cert_epoch == committee_epoch, EIncorrectEpoch); + + let message = bcs_message.into_remainder_bytes(); + + CertifiedMessage { intent_type, intent_version, cert_epoch, stake_support, message } +} + +/// Constructs the certified blob message, note that constructing +/// implies a certified message, that is already checked. +public(package) fun certify_blob_message(message: CertifiedMessage): CertifiedBlobMessage { + // Assert type is correct + assert!(message.intent_type() == BLOB_CERT_MSG_TYPE, EInvalidMsgType); + + // The certified blob message contain a blob_id : u256 + let epoch = message.cert_epoch(); + let message_body = message.into_message(); + + let mut bcs_body = bcs::new(message_body); + let blob_id = bcs_body.peel_u256(); + + // On purpose we do not check that nothing is left in the message + // to allow in the future for extensibility. + + CertifiedBlobMessage { epoch, blob_id } +} + +/// Constructs the certified blob message, note this is only +/// used for event blobs +public(package) fun certified_event_blob_message(epoch: u32, blob_id: u256): CertifiedBlobMessage { + CertifiedBlobMessage { epoch, blob_id } +} + +/// Construct the certified invalid Blob ID message, note that constructing +/// implies a certified message, that is already checked. +public(package) fun invalid_blob_id_message(message: CertifiedMessage): CertifiedInvalidBlobId { + // Assert type is correct + assert!(message.intent_type() == INVALID_BLOB_ID_MSG_TYPE, EInvalidMsgType); + + // The InvalidBlobID message has no payload besides the blob_id. + // The certified blob message contain a blob_id : u256 + let epoch = message.cert_epoch(); + let message_body = message.into_message(); + + let mut bcs_body = bcs::new(message_body); + let blob_id = bcs_body.peel_u256(); + + // This output is provided as a service in case anything else needs to rely on + // certified invalid blob ID information in the future. But out base design only + // uses the event emitted here. + CertifiedInvalidBlobId { epoch, blob_id } +} + +// === Accessors for CertifiedMessage === + +public(package) fun intent_type(self: &CertifiedMessage): u8 { + self.intent_type +} + +public(package) fun intent_version(self: &CertifiedMessage): u8 { + self.intent_version +} + +public(package) fun cert_epoch(self: &CertifiedMessage): u32 { + self.cert_epoch +} + +public(package) fun stake_support(self: &CertifiedMessage): u16 { + self.stake_support +} + +public(package) fun message(self: &CertifiedMessage): &vector { + &self.message +} + +// Deconstruct into the vector of message bytes +public(package) fun into_message(self: CertifiedMessage): vector { + self.message +} + +// === Accessors for CertifiedBlobMessage === + +public(package) fun certified_epoch(self: &CertifiedBlobMessage): u32 { + self.epoch +} + +public(package) fun certified_blob_id(self: &CertifiedBlobMessage): u256 { + self.blob_id +} + +// === Accessors for CertifiedInvalidBlobId === + +public(package) fun certified_invalid_epoch(self: &CertifiedInvalidBlobId): u32 { + self.epoch +} + +public(package) fun invalid_blob_id(self: &CertifiedInvalidBlobId): u256 { + self.blob_id +} + +// === Test only functions === + +#[test_only] +public fun certified_message_for_testing( + intent_type: u8, + intent_version: u8, + cert_epoch: u32, + stake_support: u16, + message: vector, +): CertifiedMessage { + CertifiedMessage { intent_type, intent_version, cert_epoch, stake_support, message } +} + +#[test_only] +public fun certified_blob_message_for_testing(epoch: u32, blob_id: u256): CertifiedBlobMessage { + CertifiedBlobMessage { epoch, blob_id } +} + +#[test_only] +public fun certified_message_bytes(epoch: u32, blob_id: u256): vector { + let mut message = vector[]; + message.push_back(BLOB_CERT_MSG_TYPE); + message.push_back(INTENT_VERSION); + message.push_back(APP_ID); + message.append(bcs::to_bytes(&epoch)); + message.append(bcs::to_bytes(&blob_id)); + message +} + +#[test_only] +public fun invalid_message_bytes(epoch: u32, blob_id: u256): vector { + let mut message = vector[]; + message.push_back(INVALID_BLOB_ID_MSG_TYPE); + message.push_back(INTENT_VERSION); + message.push_back(APP_ID); + message.append(bcs::to_bytes(&epoch)); + message.append(bcs::to_bytes(&blob_id)); + message +} + +#[test] +fun test_message_creation() { + let epoch = 42; + let blob_id = 0xdeadbeefdeadbeefdeadbeefdeadbeef; + let msg = certified_message_bytes(epoch, blob_id); + let cert_msg = new_certified_message(msg, epoch, 1).certify_blob_message(); + assert!(cert_msg.blob_id == blob_id); + assert!(cert_msg.epoch == epoch); +} diff --git a/contracts/walrus/sources/system/metadata.move b/contracts/walrus/sources/system/metadata.move new file mode 100644 index 00000000..2ef61e70 --- /dev/null +++ b/contracts/walrus/sources/system/metadata.move @@ -0,0 +1,37 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Contains the metadata for Blobs on Walrus. +module walrus::metadata; + +use sui::vec_map::{Self, VecMap}; +use std::string::String; + + +/// The metadata struct for Blob objects. +public struct Metadata has store, drop { + metadata: VecMap +} + +/// Creates a new instance of Metadata. +public fun new(): Metadata { + Metadata { + metadata: vec_map::empty() + } +} + +/// Inserts a key-value pair into the metadata. +/// +/// If the key is already present, the value is updated. +public fun insert_or_update(self: &mut Metadata, key: String, value: String) { + if (self.metadata.contains(&key)) { + self.metadata.remove(&key); + }; + self.metadata.insert(key, value); +} + + +/// Removes the metadata associated with the given key. +public fun remove(self: &mut Metadata, key: &String): (String, String) { + self.metadata.remove(key) +} diff --git a/contracts/walrus/sources/system/redstuff.move b/contracts/walrus/sources/system/redstuff.move new file mode 100644 index 00000000..2c3e99d3 --- /dev/null +++ b/contracts/walrus/sources/system/redstuff.move @@ -0,0 +1,104 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module walrus::redstuff; + +// The length of a hash used for the Red Stuff metadata +const DIGEST_LEN: u64 = 32; + +// The length of a blob id in the stored metadata +const BLOB_ID_LEN: u64 = 32; + +/// Computes the encoded length of a blob for the Red Stuff encoding, given its +/// unencoded size and the number of shards. The output length includes the +/// size of the metadata hashes and the blob ID. +/// This computation is the same as done by the function of the same name in +/// `crates/walrus_core/encoding/config.rs` and should be kept in sync. +public(package) fun encoded_blob_length(unencoded_length: u64, n_shards: u16): u64 { + // prettier-ignore + let slivers_size = ( + source_symbols_primary(n_shards) as u64 + (source_symbols_secondary(n_shards) as u64), + ) * (symbol_size(unencoded_length, n_shards) as u64); + + (n_shards as u64) * (slivers_size + metadata_size(n_shards)) +} + +/// The number of primary source symbols per sliver given `n_shards`. +fun source_symbols_primary(n_shards: u16): u16 { + n_shards - max_byzantine(n_shards) - decoding_safety_limit(n_shards) +} + +/// The number of secondary source symbols per sliver given `n_shards`. +fun source_symbols_secondary(n_shards: u16): u16 { + n_shards - 2 * max_byzantine(n_shards) - decoding_safety_limit(n_shards) +} + +/// The total number of source symbols given `n_shards`. +fun n_source_symbols(n_shards: u16): u64 { + (source_symbols_primary(n_shards) as u64) * (source_symbols_secondary(n_shards) as u64) +} + +/// Computes the symbol size given the `unencoded_length` and number of shards +/// `n_shards`. If the resulting symbols would be larger than a `u16`, this +/// results in an Error. +fun symbol_size(mut unencoded_length: u64, n_shards: u16): u16 { + if (unencoded_length == 0) { + unencoded_length = 1; + }; + let n_symbols = n_source_symbols(n_shards); + ((unencoded_length - 1) / n_symbols + 1) as u16 +} + +/// The size of the metadata, i.e. sliver root hashes and blob_id. +fun metadata_size(n_shards: u16): u64 { + (n_shards as u64) * DIGEST_LEN * 2 + BLOB_ID_LEN +} + +/// Returns the decoding safety limit. See `crates/walrus-core/src/encoding/config.rs` +/// for a description. +fun decoding_safety_limit(n_shards: u16): u16 { + // These ranges are chosen to ensure that the safety limit is at most 20% of f, + // up to a safety limit of 5. + (max_byzantine(n_shards) / 5).min(5) +} + +/// Maximum number of byzantine shards, given `n_shards`. +fun max_byzantine(n_shards: u16): u16 { + (n_shards - 1) / 3 +} + +// Tests + +#[test_only] +fun assert_encoded_size(unencoded_length: u64, n_shards: u16, encoded_size: u64) { + assert!(encoded_blob_length(unencoded_length, n_shards) == encoded_size, 0); +} + +#[test] +/// These tests replicate the tests for `encoded_blob_length` in +/// `crates/walrus_core/encoding/config.rs` and should be kept in sync. +fun test_encoded_size() { + assert_encoded_size(1, 10, 10 * ((4 + 7) + 10 * 2 * 32 + 32)); + assert_encoded_size(1, 1000, 1000 * ((329 + 662) + 1000 * 2 * 32 + 32)); + assert_encoded_size((4 * 7) * 100, 10, 10 * ((4 + 7) * 100 + 10 * 2 * 32 + 32)); + assert_encoded_size( + (329 * 662) * 100, + 1000, + 1000 * ((329 + 662) * 100 + 1000 * 2 * 32 + 32), + ); +} + +#[test] +fun test_zero_size() { + // test should fail here + encoded_blob_length(0, 10); +} + +#[test, expected_failure] +fun test_symbol_too_large() { + let n_shards = 100; + // Create an unencoded length for which each symbol must be larger than the maximum size + let unencoded_length = (0xffff + 1) * n_source_symbols(n_shards); + // Test should fail here + let _ = symbol_size(unencoded_length, n_shards); +} diff --git a/contracts/walrus/sources/system/shared_blob.move b/contracts/walrus/sources/system/shared_blob.move new file mode 100644 index 00000000..e3e24120 --- /dev/null +++ b/contracts/walrus/sources/system/shared_blob.move @@ -0,0 +1,43 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module walrus::shared_blob; + +use sui::{balance::{Self, Balance}, coin::Coin}; +use wal::wal::WAL; +use walrus::{blob::Blob, system::System}; + +/// A wrapper around `Blob` that acts as a "tip jar" that can be funded by anyone and allows +/// keeping the wrapped `Blob` alive indefinitely. +public struct SharedBlob has key, store { + id: UID, + blob: Blob, + funds: Balance, +} + +/// Shares the provided `blob` as a `SharedBlob` with zero funds. +public fun new(blob: Blob, ctx: &mut TxContext) { + transfer::share_object(SharedBlob { + id: object::new(ctx), + blob, + funds: balance::zero(), + }) +} + +/// Adds the provided `Coin` to the stored funds. +public fun fund(self: &mut SharedBlob, added_funds: Coin) { + self.funds.join(added_funds.into_balance()); +} + +/// Extends the lifetime of the wrapped `Blob` by `epochs_ahead` epochs if the stored funds are +/// sufficient and the new lifetime does not exceed the maximum lifetime. +public fun extend( + self: &mut SharedBlob, + system: &mut System, + epochs_ahead: u32, + ctx: &mut TxContext, +) { + let mut coin = self.funds.withdraw_all().into_coin(ctx); + system.extend_blob(&mut self.blob, epochs_ahead, &mut coin); + self.funds.join(coin.into_balance()); +} diff --git a/contracts/walrus/sources/system/storage_accounting.move b/contracts/walrus/sources/system/storage_accounting.move new file mode 100644 index 00000000..85b17b89 --- /dev/null +++ b/contracts/walrus/sources/system/storage_accounting.move @@ -0,0 +1,137 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module walrus::storage_accounting; + +use sui::balance::{Self, Balance}; +use wal::wal::WAL; + +// Errors +// Keep errors in `walrus-sui/types/move_errors.rs` up to date with changes here. +const ETooFarInFuture: u64 = 0; + +/// Holds information about a future epoch, namely how much +/// storage needs to be reclaimed and the rewards to be distributed. +public struct FutureAccounting has store { + epoch: u32, + storage_to_reclaim: u64, + rewards_to_distribute: Balance, +} + +/// Constructor for FutureAccounting +public(package) fun new_future_accounting( + epoch: u32, + storage_to_reclaim: u64, + rewards_to_distribute: Balance, +): FutureAccounting { + FutureAccounting { epoch, storage_to_reclaim, rewards_to_distribute } +} + +/// Accessor for epoch, read-only +public(package) fun epoch(accounting: &FutureAccounting): u32 { + *&accounting.epoch +} + +/// Accessor for storage_to_reclaim, mutable. +public(package) fun storage_to_reclaim(accounting: &FutureAccounting): u64 { + accounting.storage_to_reclaim +} + +/// Increase storage to reclaim +public(package) fun increase_storage_to_reclaim(accounting: &mut FutureAccounting, amount: u64) { + accounting.storage_to_reclaim = accounting.storage_to_reclaim + amount; +} + +/// Decrease storage to reclaim +public(package) fun decrease_storage_to_reclaim(accounting: &mut FutureAccounting, amount: u64) { + accounting.storage_to_reclaim = accounting.storage_to_reclaim - amount; +} + +/// Accessor for rewards_to_distribute, mutable. +public(package) fun rewards_balance(accounting: &mut FutureAccounting): &mut Balance { + &mut accounting.rewards_to_distribute +} + +/// Destructor for FutureAccounting, when empty. +public(package) fun delete_empty_future_accounting(self: FutureAccounting) { + self.unwrap_balance().destroy_zero() +} + +public(package) fun unwrap_balance(self: FutureAccounting): Balance { + let FutureAccounting { + rewards_to_distribute, + .., + } = self; + rewards_to_distribute +} + +#[test_only] +public(package) fun burn_for_testing(self: FutureAccounting) { + let FutureAccounting { + rewards_to_distribute, + .., + } = self; + + rewards_to_distribute.destroy_for_testing(); +} + +/// A ring buffer holding future accounts for a continuous range of epochs. +public struct FutureAccountingRingBuffer has store { + current_index: u32, + length: u32, + ring_buffer: vector, +} + +/// Constructor for FutureAccountingRingBuffer +public(package) fun ring_new(length: u32): FutureAccountingRingBuffer { + let ring_buffer = vector::tabulate!( + length as u64, + |epoch| FutureAccounting { + epoch: epoch as u32, + storage_to_reclaim: 0, + rewards_to_distribute: balance::zero(), + }, + ); + + FutureAccountingRingBuffer { current_index: 0, length: length, ring_buffer: ring_buffer } +} + +/// Lookup an entry a number of epochs in the future. +public(package) fun ring_lookup_mut( + self: &mut FutureAccountingRingBuffer, + epochs_in_future: u32, +): &mut FutureAccounting { + // Check for out-of-bounds access. + assert!(epochs_in_future < self.length, ETooFarInFuture); + + let actual_index = (epochs_in_future + self.current_index) % self.length; + &mut self.ring_buffer[actual_index as u64] +} + +public(package) fun ring_pop_expand(self: &mut FutureAccountingRingBuffer): FutureAccounting { + // Get current epoch + let current_index = self.current_index; + let current_epoch = self.ring_buffer[current_index as u64].epoch; + + // Expand the ring buffer + self + .ring_buffer + .push_back(FutureAccounting { + epoch: current_epoch + self.length, + storage_to_reclaim: 0, + rewards_to_distribute: balance::zero(), + }); + + // Now swap remove the current element and increment the current_index + let accounting = self.ring_buffer.swap_remove(current_index as u64); + self.current_index = (current_index + 1) % self.length; + + accounting +} + +// === Accessors === + +/// The maximum number of epochs for which we can use `self`. +public(package) fun max_epochs_ahead(self: &FutureAccountingRingBuffer): u32 { + self.length +} diff --git a/contracts/walrus/sources/system/storage_resource.move b/contracts/walrus/sources/system/storage_resource.move new file mode 100644 index 00000000..7842895e --- /dev/null +++ b/contracts/walrus/sources/system/storage_resource.move @@ -0,0 +1,144 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module walrus::storage_resource; + +const EInvalidEpoch: u64 = 0; +const EIncompatibleEpochs: u64 = 1; +const EIncompatibleAmount: u64 = 2; + +/// Reservation for storage for a given period, which is inclusive start, exclusive end. +public struct Storage has key, store { + id: UID, + start_epoch: u32, + end_epoch: u32, + storage_size: u64, +} + +// === Accessors === + +public fun start_epoch(self: &Storage): u32 { + self.start_epoch +} + +public fun end_epoch(self: &Storage): u32 { + self.end_epoch +} + +public fun storage_size(self: &Storage): u64 { + self.storage_size +} + +/// Constructor for [Storage] objects. +/// Necessary to allow `walrus::system` to create storage objects. +/// Cannot be called outside of the current module and [walrus::system]. +public(package) fun create_storage( + start_epoch: u32, + end_epoch: u32, + storage_size: u64, + ctx: &mut TxContext, +): Storage { + Storage { id: object::new(ctx), start_epoch, end_epoch, storage_size } +} + +/// Extends the end epoch by `extendion_epochs` epochs. +public(package) fun extend_end_epoch(self: &mut Storage, extension_epochs: u32) { + self.end_epoch = self.end_epoch + extension_epochs; +} + +/// Split the storage object into two based on `split_epoch` +/// +/// `storage` is modified to cover the period from `start_epoch` to `split_epoch` +/// and a new storage object covering `split_epoch` to `end_epoch` is returned. +public fun split_by_epoch(storage: &mut Storage, split_epoch: u32, ctx: &mut TxContext): Storage { + assert!(split_epoch >= storage.start_epoch && split_epoch <= storage.end_epoch, EInvalidEpoch); + let end_epoch = storage.end_epoch; + storage.end_epoch = split_epoch; + Storage { + id: object::new(ctx), + start_epoch: split_epoch, + end_epoch, + storage_size: storage.storage_size, + } +} + +/// Split the storage object into two based on `split_size` +/// +/// `storage` is modified to cover `split_size` and a new object covering +/// `storage.storage_size - split_size` is created. +public fun split_by_size(storage: &mut Storage, split_size: u64, ctx: &mut TxContext): Storage { + let storage_size = storage.storage_size - split_size; + storage.storage_size = split_size; + Storage { + id: object::new(ctx), + start_epoch: storage.start_epoch, + end_epoch: storage.end_epoch, + storage_size, + } +} + +/// Fuse two storage objects that cover adjacent periods with the same storage size. +public fun fuse_periods(first: &mut Storage, second: Storage) { + let Storage { + id, + start_epoch: second_start, + end_epoch: second_end, + storage_size: second_size, + } = second; + id.delete(); + assert!(first.storage_size == second_size, EIncompatibleAmount); + if (first.end_epoch == second_start) { + first.end_epoch = second_end; + } else { + assert!(first.start_epoch == second_end, EIncompatibleEpochs); + first.start_epoch = second_start; + } +} + +/// Fuse two storage objects that cover the same period +public fun fuse_amount(first: &mut Storage, second: Storage) { + let Storage { + id, + start_epoch: second_start, + end_epoch: second_end, + storage_size: second_size, + } = second; + id.delete(); + assert!( + first.start_epoch == second_start && first.end_epoch == second_end, + EIncompatibleEpochs, + ); + first.storage_size = first.storage_size + second_size; +} + +/// Fuse two storage objects that either cover the same period +/// or adjacent periods with the same storage size. +public fun fuse(first: &mut Storage, second: Storage) { + if (first.start_epoch == second.start_epoch) { + // Fuse by storage_size + fuse_amount(first, second); + } else { + // Fuse by period + fuse_periods(first, second); + } +} + +#[test_only] +/// Constructor for [Storage] objects for tests +public fun create_for_test( + start_epoch: u32, + end_epoch: u32, + storage_size: u64, + ctx: &mut TxContext, +): Storage { + Storage { id: object::new(ctx), start_epoch, end_epoch, storage_size } +} + +/// Destructor for [Storage] objects +public fun destroy(storage: Storage) { + let Storage { + id, + .., + } = storage; + id.delete(); +} diff --git a/contracts/walrus/sources/system/system_state_inner.move b/contracts/walrus/sources/system/system_state_inner.move new file mode 100644 index 00000000..4707eb90 --- /dev/null +++ b/contracts/walrus/sources/system/system_state_inner.move @@ -0,0 +1,528 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[allow(unused_variable, unused_mut_parameter, unused_field)] +module walrus::system_state_inner; + +use sui::{balance::Balance, coin::Coin}; +use wal::wal::WAL; +use walrus::{ + blob::{Self, Blob}, + bls_aggregate::{Self, BlsCommittee}, + encoding::encoded_blob_length, + epoch_parameters::EpochParams, + event_blob::{Self, EventBlobCertificationState, new_attestation}, + events::emit_invalid_blob_id, + messages, + storage_accounting::{Self, FutureAccountingRingBuffer}, + storage_node::StorageNodeCap, + storage_resource::{Self, Storage} +}; + +/// An upper limit for the maximum number of epochs ahead for which a blob can be registered. +/// Needed to bound the size of the `future_accounting`. +const MAX_MAX_EPOCHS_AHEAD: u32 = 1000; + +// Keep in sync with the same constant in `crates/walrus-sui/utils.rs`. +const BYTES_PER_UNIT_SIZE: u64 = 1_024 * 1_024; // 1 MiB + +// Errors +// Keep errors in `walrus-sui/types/move_errors.rs` up to date with changes here. +const EInvalidMaxEpochsAhead: u64 = 0; +const EStorageExceeded: u64 = 1; +const EInvalidEpochsAhead: u64 = 2; +const EInvalidIdEpoch: u64 = 3; +const EIncorrectCommittee: u64 = 4; +const EInvalidAccountingEpoch: u64 = 5; +const EIncorrectAttestation: u64 = 6; +const ERepeatedAttestation: u64 = 7; +const ENotCommitteeMember: u64 = 8; + +/// The inner object that is not present in signatures and can be versioned. +#[allow(unused_field)] +public struct SystemStateInnerV1 has key, store { + id: UID, + /// The current committee, with the current epoch. + committee: BlsCommittee, + // Some accounting + total_capacity_size: u64, + used_capacity_size: u64, + /// The price per unit size of storage. + storage_price_per_unit_size: u64, + /// The write price per unit size. + write_price_per_unit_size: u64, + /// Accounting ring buffer for future epochs. + future_accounting: FutureAccountingRingBuffer, + /// Event blob certification state + event_blob_certification_state: EventBlobCertificationState, +} + +/// Creates an empty system state with a capacity of zero and an empty +/// committee. +public(package) fun create_empty(max_epochs_ahead: u32, ctx: &mut TxContext): SystemStateInnerV1 { + let committee = bls_aggregate::new_bls_committee(0, vector[]); + assert!(max_epochs_ahead <= MAX_MAX_EPOCHS_AHEAD, EInvalidMaxEpochsAhead); + let future_accounting = storage_accounting::ring_new(max_epochs_ahead); + let event_blob_certification_state = event_blob::create_with_empty_state( + ctx, + ); + let id = object::new(ctx); + SystemStateInnerV1 { + id, + committee, + total_capacity_size: 0, + used_capacity_size: 0, + storage_price_per_unit_size: 0, + write_price_per_unit_size: 0, + future_accounting, + event_blob_certification_state, + } +} + +/// Update epoch to next epoch, and update the committee, price and capacity. +/// +/// Called by the epoch change function that connects `Staking` and `System`. +/// Returns +/// the balance of the rewards from the previous epoch. +public(package) fun advance_epoch( + self: &mut SystemStateInnerV1, + new_committee: BlsCommittee, + new_epoch_params: EpochParams, +): Balance { + // Check new committee is valid, the existence of a committee for the next + // epoch + // is proof that the time has come to move epochs. + let old_epoch = self.epoch(); + let new_epoch = old_epoch + 1; + + assert!(new_committee.epoch() == new_epoch, EIncorrectCommittee); + self.committee = new_committee; + + // Update the system object. + self.total_capacity_size = new_epoch_params.capacity().max(self.used_capacity_size); + self.storage_price_per_unit_size = new_epoch_params.storage_price(); + self.write_price_per_unit_size = new_epoch_params.write_price(); + + let accounts_old_epoch = self.future_accounting.ring_pop_expand(); + + // Make sure that we have the correct epoch + assert!(accounts_old_epoch.epoch() == old_epoch, EInvalidAccountingEpoch); + + // Stop tracking all event blobs + self.event_blob_certification_state.reset(); + + // Update storage based on the accounts data. + self.used_capacity_size = self.used_capacity_size - accounts_old_epoch.storage_to_reclaim(); + accounts_old_epoch.unwrap_balance() +} + +/// Allow buying a storage reservation for a given period of epochs. +public(package) fun reserve_space( + self: &mut SystemStateInnerV1, + storage_amount: u64, + epochs_ahead: u32, + payment: &mut Coin, + ctx: &mut TxContext, +): Storage { + // Check the period is within the allowed range. + assert!(epochs_ahead > 0, EInvalidEpochsAhead); + assert!(epochs_ahead <= self.future_accounting.max_epochs_ahead(), EInvalidEpochsAhead); + + // Check capacity is available. + assert!(self.used_capacity_size + storage_amount <= self.total_capacity_size, EStorageExceeded); + + // Pay rewards for each future epoch into the future accounting. + self.process_storage_payments(storage_amount, 0, epochs_ahead, payment); + + self.reserve_space_without_payment(storage_amount, epochs_ahead, ctx) +} + +/// Allow buying a storage reservation for a given period of epochs without +/// payment. +/// Only to be used for event blobs. +fun reserve_space_without_payment( + self: &mut SystemStateInnerV1, + storage_amount: u64, + epochs_ahead: u32, + ctx: &mut TxContext, +): Storage { + // Check the period is within the allowed range. + assert!(epochs_ahead > 0, EInvalidEpochsAhead); + assert!(epochs_ahead <= self.future_accounting.max_epochs_ahead(), EInvalidEpochsAhead); + + // Update the storage accounting. + self.used_capacity_size = self.used_capacity_size + storage_amount; + + // Account the space to reclaim in the future. + let final_account = self.future_accounting.ring_lookup_mut(epochs_ahead - 1); + final_account.increase_storage_to_reclaim(storage_amount); + + let self_epoch = epoch(self); + + storage_resource::create_storage( + self_epoch, + self_epoch + epochs_ahead, + storage_amount, + ctx, + ) +} + +/// Processes invalid blob id message. Checks the certificate in the current +/// committee and ensures +/// that the epoch is correct before emitting an event. +public(package) fun invalidate_blob_id( + self: &SystemStateInnerV1, + signature: vector, + members: vector, + message: vector, +): u256 { + let certified_message = self + .committee + .verify_quorum_in_epoch( + signature, + members, + message, + ); + + let invalid_blob_message = certified_message.invalid_blob_id_message(); + let blob_id = invalid_blob_message.invalid_blob_id(); + // Assert the epoch is correct. + let epoch = invalid_blob_message.certified_invalid_epoch(); + assert!(epoch == self.epoch(), EInvalidIdEpoch); + + // Emit the event about a blob id being invalid here. + emit_invalid_blob_id( + epoch, + blob_id, + ); + blob_id +} + +/// Registers a new blob in the system. +/// `size` is the size of the unencoded blob. The reserved space in `storage` +/// must be at +/// least the size of the encoded blob. +public(package) fun register_blob( + self: &mut SystemStateInnerV1, + storage: Storage, + blob_id: u256, + root_hash: u256, + size: u64, + encoding_type: u8, + deletable: bool, + write_payment_coin: &mut Coin, + ctx: &mut TxContext, +): Blob { + let blob = blob::new( + storage, + blob_id, + root_hash, + size, + encoding_type, + deletable, + self.epoch(), + self.n_shards(), + ctx, + ); + let write_price = self.write_price(blob.encoded_size(self.n_shards())); + let payment = write_payment_coin.split(write_price, ctx).into_balance(); + let accounts = self.future_accounting.ring_lookup_mut(0).rewards_balance().join(payment); + blob +} + +/// Certify that a blob will be available in the storage system until the end +/// epoch of the +/// storage associated with it. +public(package) fun certify_blob( + self: &SystemStateInnerV1, + blob: &mut Blob, + signature: vector, + signers: vector, + message: vector, +) { + let certified_msg = self + .committee() + .verify_quorum_in_epoch( + signature, + signers, + message, + ); + let certified_blob_msg = certified_msg.certify_blob_message(); + blob.certify_with_certified_msg(self.epoch(), certified_blob_msg); +} + +/// Deletes a deletable blob and returns the contained storage resource. +public(package) fun delete_blob(self: &SystemStateInnerV1, blob: Blob): Storage { + blob.delete(self.epoch()) +} + +/// Extend the period of validity of a blob with a new storage resource. +/// The new storage resource must be the same size as the storage resource +/// used in the blob, and have a longer period of validity. +public(package) fun extend_blob_with_resource( + self: &SystemStateInnerV1, + blob: &mut Blob, + extension: Storage, +) { + blob.extend_with_resource(extension, self.epoch()); +} + +/// Extend the period of validity of a blob by extending its contained storage +/// resource. +public(package) fun extend_blob( + self: &mut SystemStateInnerV1, + blob: &mut Blob, + epochs_ahead: u32, + payment: &mut Coin, +) { + // Check that the blob is certified and not expired. + blob.assert_certified_not_expired(self.epoch()); + + let start_offset = blob.storage().end_epoch() - self.epoch(); + let end_offset = start_offset + epochs_ahead; + + // Check the period is within the allowed range. + assert!(epochs_ahead > 0, EInvalidEpochsAhead); + assert!(end_offset <= self.future_accounting.max_epochs_ahead(), EInvalidEpochsAhead); + + // Pay rewards for each future epoch into the future accounting. + let storage_size = blob.storage().storage_size(); + self.process_storage_payments( + storage_size, + start_offset, + end_offset, + payment, + ); + + // Account the space to reclaim in the future. + + // First account for the space not being freed in the original end epoch. + self + .future_accounting + .ring_lookup_mut(start_offset - 1) + .decrease_storage_to_reclaim(storage_size); + + // Then account for the space being freed in the new end epoch. + self + .future_accounting + .ring_lookup_mut(end_offset - 1) + .increase_storage_to_reclaim(storage_size); + + blob.storage_mut().extend_end_epoch(epochs_ahead); + + blob.emit_certified(true); +} + +fun process_storage_payments( + self: &mut SystemStateInnerV1, + storage_size: u64, + start_offset: u32, + end_offset: u32, + payment: &mut Coin, +) { + let storage_units = storage_units_from_size(storage_size); + let period_payment_due = self.storage_price_per_unit_size * storage_units; + let coin_balance = payment.balance_mut(); + + start_offset.range_do!(end_offset, |i| { + let accounts = self.future_accounting.ring_lookup_mut(i); + + // Distribute rewards + let rewards_balance = accounts.rewards_balance(); + // Note this will abort if the balance is not enough. + let epoch_payment = coin_balance.split(period_payment_due); + rewards_balance.join(epoch_payment); + }); +} + +public(package) fun certify_event_blob( + self: &mut SystemStateInnerV1, + cap: &mut StorageNodeCap, + blob_id: u256, + root_hash: u256, + size: u64, + encoding_type: u8, + ending_checkpoint_sequence_num: u64, + epoch: u32, + ctx: &mut TxContext, +) { + assert!(self.committee().contains(&cap.node_id()), ENotCommitteeMember); + assert!(epoch == self.epoch(), EInvalidIdEpoch); + + let cap_attestion = cap.last_event_blob_attestation(); + if (cap_attestion.is_some()) { + let attestation = cap_attestion.destroy_some(); + assert!( + attestation.last_attested_event_blob_epoch() < self.epoch() || + ending_checkpoint_sequence_num > + attestation.last_attested_event_blob_checkpoint_seq_num(), + ERepeatedAttestation, + ); + let latest_certified_checkpoint_seq_num = self + .event_blob_certification_state + .get_latest_certified_checkpoint_sequence_number(); + if (latest_certified_checkpoint_seq_num.is_some()) { + let certified_checkpoint_seq_num = latest_certified_checkpoint_seq_num.destroy_some(); + assert!( + attestation.last_attested_event_blob_epoch() < self.epoch() || + attestation.last_attested_event_blob_checkpoint_seq_num() + <= certified_checkpoint_seq_num, + EIncorrectAttestation, + ); + } else { + assert!( + attestation.last_attested_event_blob_epoch() < self.epoch(), + EIncorrectAttestation, + ); + } + }; + + let attestation = new_attestation(ending_checkpoint_sequence_num, epoch); + cap.set_last_event_blob_attestation(attestation); + + let blob_certified = self + .event_blob_certification_state + .is_blob_already_certified( + ending_checkpoint_sequence_num, + ); + if (blob_certified) { + return + }; + + self.event_blob_certification_state.start_tracking_blob(blob_id); + let weight = self.committee().get_member_weight(&cap.node_id()); + let agg_weight = self.event_blob_certification_state.update_aggregate_weight(blob_id, weight); + let certified = self.committee().verify_quorum(agg_weight); + if (!certified) { + return + }; + + let num_shards = self.n_shards(); + let epochs_ahead = self.future_accounting.max_epochs_ahead(); + let storage = self.reserve_space_without_payment( + encoded_blob_length( + size, + encoding_type, + num_shards, + ), + epochs_ahead, + ctx, + ); + let mut blob = blob::new( + storage, + blob_id, + root_hash, + size, + encoding_type, + false, + self.epoch(), + self.n_shards(), + ctx, + ); + let certified_blob_msg = messages::certified_event_blob_message( + self.epoch(), + blob_id, + ); + blob.certify_with_certified_msg(self.epoch(), certified_blob_msg); + self + .event_blob_certification_state + .update_latest_certified_event_blob( + ending_checkpoint_sequence_num, + blob_id, + ); + self.event_blob_certification_state.stop_tracking_blob(blob_id); + blob.burn(); +} + +// === Accessors === + +/// Get epoch. Uses the committee to get the epoch. +public(package) fun epoch(self: &SystemStateInnerV1): u32 { + self.committee.epoch() +} + +/// Accessor for total capacity size. +public(package) fun total_capacity_size(self: &SystemStateInnerV1): u64 { + self.total_capacity_size +} + +/// Accessor for used capacity size. +public(package) fun used_capacity_size(self: &SystemStateInnerV1): u64 { + self.used_capacity_size +} + +/// An accessor for the current committee. +public(package) fun committee(self: &SystemStateInnerV1): &BlsCommittee { + &self.committee +} + +#[test_only] +public(package) fun committee_mut(self: &mut SystemStateInnerV1): &mut BlsCommittee { + &mut self.committee +} + +public(package) fun n_shards(self: &SystemStateInnerV1): u16 { + self.committee.n_shards() +} + +public(package) fun write_price(self: &SystemStateInnerV1, write_size: u64): u64 { + let storage_units = storage_units_from_size(write_size); + self.write_price_per_unit_size * storage_units +} + +fun storage_units_from_size(size: u64): u64 { + (size + BYTES_PER_UNIT_SIZE - 1) / BYTES_PER_UNIT_SIZE +} + +// === Testing === + +#[test_only] +use walrus::{test_utils}; + +#[test_only] +public(package) fun new_for_testing(): SystemStateInnerV1 { + let committee = test_utils::new_bls_committee_for_testing(0); + let ctx = &mut tx_context::dummy(); + let id = object::new(ctx); + SystemStateInnerV1 { + id, + committee, + total_capacity_size: 1_000_000_000, + used_capacity_size: 0, + storage_price_per_unit_size: 5, + write_price_per_unit_size: 1, + future_accounting: storage_accounting::ring_new(104), + event_blob_certification_state: event_blob::create_with_empty_state( + ctx, + ), + } +} + +#[test_only] +public(package) fun new_for_testing_with_multiple_members(ctx: &mut TxContext): SystemStateInnerV1 { + let committee = test_utils::new_bls_committee_with_multiple_members_for_testing( + 0, + ctx, + ); + + let id = object::new(ctx); + SystemStateInnerV1 { + id, + committee, + total_capacity_size: 1_000_000_000, + used_capacity_size: 0, + storage_price_per_unit_size: 5, + write_price_per_unit_size: 1, + future_accounting: storage_accounting::ring_new(104), + event_blob_certification_state: event_blob::create_with_empty_state( + ctx, + ), + } +} + +#[test_only] +public(package) fun get_event_blob_certification_state( + system: &SystemStateInnerV1, +): &EventBlobCertificationState { + &system.event_blob_certification_state +} diff --git a/docs/dev-guide/sui-struct.md b/docs/dev-guide/sui-struct.md index 92a64997..060ce09c 100644 --- a/docs/dev-guide/sui-struct.md +++ b/docs/dev-guide/sui-struct.md @@ -9,7 +9,7 @@ on Sui. The Move code of the Walrus Testnet contracts is available at -. An example package using +. An example package using the Walrus contracts is available at . diff --git a/examples/move/walrus_dep/Move.toml b/examples/move/walrus_dep/Move.toml index ca68f03c..7eb61fe4 100644 --- a/examples/move/walrus_dep/Move.toml +++ b/examples/move/walrus_dep/Move.toml @@ -4,7 +4,7 @@ edition = "2024.beta" [dependencies] Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "testnet-v1.29.2" } -blob_store = { git = "https://github.com/MystenLabs/walrus-docs.git", rev = "main", subdir = "contracts/blob_store" } +blob_store = { git = "https://github.com/MystenLabs/walrus-docs.git", rev = "main", subdir = "contracts/sources/walrus" } [addresses] walrus_dep = "0x0" From dd9f665add3eb53f42a4214006660cd1cceaf5fa Mon Sep 17 00:00:00 2001 From: Karl Wuest <5716112+karlwuest@users.noreply.github.com> Date: Tue, 15 Oct 2024 01:54:20 +0200 Subject: [PATCH 23/50] feat: add overview of staking (#131) staking overview --- .github/workflows/lint.yaml | 2 +- docs/usage/stake.md | 28 +++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 9112acae..1411cebe 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -26,7 +26,7 @@ jobs: name: Check editorconfig steps: - uses: actions/checkout@v4 - - run: pip install editorconfig-checker=="2.7.3" + - run: pip install editorconfig-checker=="2.7.3" --break-system-packages - run: ec markdownlint: diff --git a/docs/usage/stake.md b/docs/usage/stake.md index 133025c1..f92d4628 100644 --- a/docs/usage/stake.md +++ b/docs/usage/stake.md @@ -1,4 +1,30 @@ -# Stake and Unstake +# Staking + +In Walrus, anyone can delegate stake to storage nodes and, by doing so, influence, which storage +nodes get selected for the committee in future epochs, and how many shards these nodes will hold. +Shards are assigned to storage nodes every epoch, roughly proportional to the amount of stake +that was delegated to them. By staking with a storage node, users also earn rewards, as they +will receive a share of the storage fees. + +Since moving shards from one storage node to another requires transferring a lot of data and +storage nodes potentially need to expand their storage capacity, the selection of the committee +for the next epoch is done ahead of time, in the middle of the previous epoch. This provides +sufficient time to storage node operators to provision additional resources, if needed. + +For stake to affect the shard distribution in epoch `e` and become "active", it must be staked +before the committee for this epoch has been selected, meaning that it has to be staked before +the midpoint of epoch `e - 1`. If it is staked after that point in time, it will only influence +the committee selection for epoch `e + 1` and thus only become active, and accrue rewards, in +that epoch. + +Unstaking has a similar delay: because unstaking funds only has an effect on the committee in +the next committee selection, the stake will remain active until that committee takes over. +This means that, to unstake at the start of epoch `e`, the user needs to "request withdrawal" +before the midpoint of epoch `e - 1`. Otherwise, i.e., if the user unstakes after this point, +the stake will remain active, and continue to accrue rewards, throughout epoch `e`, and the +balance and rewards will be available to withdraw at the start of epoch `e + 1`. + +## How to stake From 63242add2b23b171f2822c4939052fd224129dee Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Tue, 15 Oct 2024 10:21:13 +0200 Subject: [PATCH 24/50] latest Testnet object IDs and contracts --- contracts/walrus/README.md | 7 ++---- .../walrus/sources/staking/staked_wal.move | 2 -- .../walrus/sources/staking/staking_inner.move | 1 - .../walrus/sources/staking/staking_pool.move | 1 - .../walrus/sources/staking/storage_node.move | 1 - contracts/walrus/sources/system/blob.move | 1 - .../sources/system/storage_accounting.move | 1 - .../sources/system/system_state_inner.move | 1 - docs/dev-guide/sui-struct.md | 1 - docs/usage/setup.md | 14 +++++------ docs/usage/web-api.md | 24 +++++++++---------- .../blob_upload_download_webapi.html | 8 +++---- 12 files changed, 24 insertions(+), 38 deletions(-) diff --git a/contracts/walrus/README.md b/contracts/walrus/README.md index f80eaa07..94cfd67f 100644 --- a/contracts/walrus/README.md +++ b/contracts/walrus/README.md @@ -1,11 +1,8 @@ # Walrus Testnet Move contracts - - This is the Move source code for the Walrus Testnet instance. We provide this so developers can -experiment with building Walrus apps that require Move extensions. A slightly different version of -these contracts is deployed on Sui Testnet as package -`0x668fb342c7ea45a3a8d645efefbb41d6b732a5fd4ead552f58df7fabe443c12e`. +experiment with building Walrus apps that require Move extensions. These contracts are deployed on +Sui Testnet as package `0x9f992cc2430a1f442ca7a5ca7638169f5d5c00e0ebc3977a65e9ac6e497fe5ef`. **A word of caution:** Walrus Mainnet will use new Move packages with struct layouts and function signatures that may not be compatible with this package. Move code that builds against this package diff --git a/contracts/walrus/sources/staking/staked_wal.move b/contracts/walrus/sources/staking/staked_wal.move index 42b8bb09..cdb5dfcc 100644 --- a/contracts/walrus/sources/staking/staked_wal.move +++ b/contracts/walrus/sources/staking/staked_wal.move @@ -12,7 +12,6 @@ module walrus::staked_wal; use sui::balance::Balance; use wal::wal::WAL; -// Keep errors in `walrus-sui/types/move_errors.rs` up to date with changes here. const ENotWithdrawing: u64 = 0; const EMetadataMismatch: u64 = 1; const EInvalidAmount: u64 = 2; @@ -22,7 +21,6 @@ const ECantSplitWithdrawing: u64 = 5; /// The state of the staked WAL. It can be either `Staked` or `Withdrawing`. /// The `Withdrawing` state contains the epoch when the staked WAL can be -/// public enum StakedWalState has store, copy, drop { // Default state of the staked WAL - it is staked in the staking pool. Staked, diff --git a/contracts/walrus/sources/staking/staking_inner.move b/contracts/walrus/sources/staking/staking_inner.move index 26131df4..a331a111 100644 --- a/contracts/walrus/sources/staking/staking_inner.move +++ b/contracts/walrus/sources/staking/staking_inner.move @@ -31,7 +31,6 @@ const MIN_STAKE: u64 = 0; /// Temporary upper limit for the number of storage nodes. const TEMP_ACTIVE_SET_SIZE_LIMIT: u16 = 100; -// Keep errors in `walrus-sui/types/move_errors.rs` up to date with changes here. const EWrongEpochState: u64 = 0; const EInvalidSyncEpoch: u64 = 1; const EDuplicateSyncDone: u64 = 2; diff --git a/contracts/walrus/sources/staking/staking_pool.move b/contracts/walrus/sources/staking/staking_pool.move index fe0c3018..758020a4 100644 --- a/contracts/walrus/sources/staking/staking_pool.move +++ b/contracts/walrus/sources/staking/staking_pool.move @@ -16,7 +16,6 @@ use walrus::{ walrus_context::WalrusContext }; -// Keep errors in `walrus-sui/types/move_errors.rs` up to date with changes here. const EPoolAlreadyUpdated: u64 = 0; const ECalculationError: u64 = 1; const EIncorrectEpochAdvance: u64 = 2; diff --git a/contracts/walrus/sources/staking/storage_node.move b/contracts/walrus/sources/staking/storage_node.move index 2d8c6c69..104faa37 100644 --- a/contracts/walrus/sources/staking/storage_node.move +++ b/contracts/walrus/sources/staking/storage_node.move @@ -9,7 +9,6 @@ use sui::{bls12381::{G1, g1_from_bytes}, group_ops::Element}; use walrus::event_blob::EventBlobAttestation; // Error codes -// Keep errors in `walrus-sui/types/move_errors.rs` up to date with changes here. const EInvalidNetworkPublicKey: u64 = 0; /// Represents a storage node in the system. diff --git a/contracts/walrus/sources/system/blob.move b/contracts/walrus/sources/system/blob.move index e5df5846..041c4a27 100644 --- a/contracts/walrus/sources/system/blob.move +++ b/contracts/walrus/sources/system/blob.move @@ -14,7 +14,6 @@ use walrus::{ }; // Error codes -// Keep errors in `walrus-sui/types/move_errors.rs` up to date with changes here. const ENotCertified: u64 = 0; const EBlobNotDeletable: u64 = 1; const EResourceBounds: u64 = 2; diff --git a/contracts/walrus/sources/system/storage_accounting.move b/contracts/walrus/sources/system/storage_accounting.move index 85b17b89..44c00ca5 100644 --- a/contracts/walrus/sources/system/storage_accounting.move +++ b/contracts/walrus/sources/system/storage_accounting.move @@ -7,7 +7,6 @@ use sui::balance::{Self, Balance}; use wal::wal::WAL; // Errors -// Keep errors in `walrus-sui/types/move_errors.rs` up to date with changes here. const ETooFarInFuture: u64 = 0; /// Holds information about a future epoch, namely how much diff --git a/contracts/walrus/sources/system/system_state_inner.move b/contracts/walrus/sources/system/system_state_inner.move index 4707eb90..0e158883 100644 --- a/contracts/walrus/sources/system/system_state_inner.move +++ b/contracts/walrus/sources/system/system_state_inner.move @@ -27,7 +27,6 @@ const MAX_MAX_EPOCHS_AHEAD: u32 = 1000; const BYTES_PER_UNIT_SIZE: u64 = 1_024 * 1_024; // 1 MiB // Errors -// Keep errors in `walrus-sui/types/move_errors.rs` up to date with changes here. const EInvalidMaxEpochsAhead: u64 = 0; const EStorageExceeded: u64 = 1; const EInvalidEpochsAhead: u64 = 2; diff --git a/docs/dev-guide/sui-struct.md b/docs/dev-guide/sui-struct.md index 060ce09c..45221e9f 100644 --- a/docs/dev-guide/sui-struct.md +++ b/docs/dev-guide/sui-struct.md @@ -7,7 +7,6 @@ querying or executing transactions on Sui directly. However, Walrus uses Sui to and smart contract developers can read information about the Walrus system, as well as stored blobs, on Sui. - The Move code of the Walrus Testnet contracts is available at . An example package using the Walrus contracts is available at diff --git a/docs/usage/setup.md b/docs/usage/setup.md index eea3e475..7bce0ae3 100644 --- a/docs/usage/setup.md +++ b/docs/usage/setup.md @@ -144,12 +144,10 @@ These need to be configured in a file `~/.config/walrus/client_config.yaml`. The current Testnet deployment uses the following objects: - - ```yaml -system_object: 0xccb3f5c1f63adf8d1e40d9bda649cc6ed3a46d399ce2b205b187e028f4253e57 -staking_object: 0x6d1380cc205471c73fc048033d0c4f031fc1ac3628a27a1baf5e17729a396345 -exchange_object: 0x41d3fdd9c5007d551d005af097af45ad37c1ba5f15b7b50ad5c1072bd069dcb6 +system_object: 0x50b84b68eb9da4c6d904a929f43638481c09c03be6274b8569778fe085c1590d +staking_object: 0x37c0e4d7b36a2f64d51bba262a1791f844cfd88f31379f1b7c04244061d43914 +exchange_object: 0x0e60a946a527902c90bbc71240435728cd6dc26b9e8debc69f09b71671c3029b ``` ### Custom path (optional) {#config-custom-path} @@ -166,9 +164,9 @@ The configuration file currently supports the following parameters: ```yaml # These are the only mandatory fields. These objects are specific for a particular Walrus # deployment but then do not change over time. -system_object: 0xccb3f5c1f63adf8d1e40d9bda649cc6ed3a46d399ce2b205b187e028f4253e57 -staking_object: 0x6d1380cc205471c73fc048033d0c4f031fc1ac3628a27a1baf5e17729a396345 -exchange_object: 0x41d3fdd9c5007d551d005af097af45ad37c1ba5f15b7b50ad5c1072bd069dcb6 +system_object: 0x50b84b68eb9da4c6d904a929f43638481c09c03be6274b8569778fe085c1590d +staking_object: 0x37c0e4d7b36a2f64d51bba262a1791f844cfd88f31379f1b7c04244061d43914 +exchange_object: 0x0e60a946a527902c90bbc71240435728cd6dc26b9e8debc69f09b71671c3029b # You can define a custom path to your Sui wallet configuration here. If this is unset or `null`, # the wallet is configured from `./sui_config.yaml` (relative to your current working directory), or diff --git a/docs/usage/web-api.md b/docs/usage/web-api.md index 0cd0d711..d9fd2498 100644 --- a/docs/usage/web-api.md +++ b/docs/usage/web-api.md @@ -34,34 +34,34 @@ only authorized parties may access it, or other measures to manage gas costs. ## Using a public aggregator or publisher {#public-services} For some use cases (e.g., a public website), or to just try out the HTTP API, a publicly accessible -aggregator and/or publisher is required. For your convenience, we provide these at the following -hosts: +aggregator and/or publisher is required. Several entities run such aggregators and publishers; the +instances run by Mysten Labs are accessible at the following hosts: - -- Aggregator: `http://ewr-ptn-agg-00.walrus-private-testnet.walrus.space:9000` -- Publisher: `http://lax-ptn-pub-00.walrus-private-testnet.walrus.space:9000` +- Aggregator: `https://aggregator.walrus-testnet.walrus.space` +- Publisher: `https://publisher.walrus-testnet.walrus.space` + + Our publisher is currently limiting requests to 10 MiB. If you want to upload larger files, you need to [run your own publisher](#local-daemon) or use the [CLI](./client-cli.md). -Note that the publisher consumes (Testnet) Sui on the service side, and a Mainnet deployment would -likely not be able to provide uncontrolled public access to publishing without requiring some -authentication and compensation for the Sui used. +Note that the publisher consumes (Testnet) SUI and WAL on the service side, and a Mainnet deployment +would likely not be able to provide uncontrolled public access to publishing without requiring some +authentication and compensation for the funds used. ## HTTP API Usage For the following examples, we assume you set the `AGGREGATOR` and `PUBLISHER` environment variables to your desired aggregator and publisher, respectively. For example: - ```sh -AGGREGATOR=http://ewr-ptn-agg-00.walrus-private-testnet.walrus.space:9000 -PUBLISHER=http://lax-ptn-pub-00.walrus-private-testnet.walrus.space:9000 +AGGREGATOR=https://aggregator.walrus-testnet.walrus.space +PUBLISHER=https://publisher.walrus-testnet.walrus.space ``` ```admonish tip title="API specification" Walrus aggregators and publishers expose their API specifications at the path `/v1/api`. You can -view this in the browser` e.g., at +view this in the browser` e.g., at ``` ### Store diff --git a/examples/javascript/blob_upload_download_webapi.html b/examples/javascript/blob_upload_download_webapi.html index 7554740a..30910ca1 100644 --- a/examples/javascript/blob_upload_download_webapi.html +++ b/examples/javascript/blob_upload_download_webapi.html @@ -214,8 +214,8 @@

Blob Upload

+ placeholder="https://publisher.walrus-testnet.walrus.space" + value="https://publisher.walrus-testnet.walrus.space" required />
@@ -224,8 +224,8 @@

Blob Upload

+ placeholder="https://aggregator.walrus-testnet.walrus.space" + value="https://aggregator.walrus-testnet.walrus.space" required />
From 611908c3e3500fd33ebd76e07902080cffb5e5bb Mon Sep 17 00:00:00 2001 From: Pavlos Chrysochoidis <10210143+pchrysochoidis@users.noreply.github.com> Date: Tue, 15 Oct 2024 14:03:34 +0100 Subject: [PATCH 25/50] docs: stake dApp instructions (#133) --- docs/usage/stake.md | 46 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/docs/usage/stake.md b/docs/usage/stake.md index f92d4628..c9c354fb 100644 --- a/docs/usage/stake.md +++ b/docs/usage/stake.md @@ -26,8 +26,52 @@ balance and rewards will be available to withdraw at the start of epoch `e + 1`. ## How to stake +### Walrus Staking dApp + +The Walrus Staking dApp allows users to stake (or unstake) to any of the storage nodes of the system + +### How to use the dApp + +- Visit [https://stake.walrus.site](https://stake.walrus.site) +- Connect your wallet + - Click the `Connect Wallet` button at the top right corner + - Select the wallet (if the wallet was connected before this and the next step wont be required) + - Approve the connection + - (Make sure the selected wallet network is Testnet) + +### Exchange Testnet SUI to WAL + +To be able to stake you will need to have WAL in your wallet. +You can exchange your Testnet SUI to WAL using the dApp + +- Click the `Get WAL` button +- Select the amount of SUI +- And click `Exchange` +- (Follow the instructions in your wallet to approve the transaction) + +### Stake + +- Find the Storage node that you want to stake to + - Below the system stats there is the list of the `Current Committee` of storage nodes + - You can select one of the nodes in that list + - or if the storage node is not in the current committee + - find all the Storage nodes at the bottom of the page +- Once you selected the Storage node click the stake button +- Select the amount of WAL +- Click Stake +- (Follow the instructions in your wallet to approve the transaction) + +### Unstake + +- Find the `Staked Wal` you want to unstake + - Below the `Current Committee` list you will find all your `Staked Wal` + - Also you can expand a Storage Node and find all your stakes with that node +- Depending on the state of the `Staked Wal` you will be able to Unstake or Withdraw your funds +- Click the `Unstake` or `Withdraw` button +- Click continue to confirm your action +- (Follow the instructions in your wallet to approve the transaction) + -- Stake / Unstake dApp link and docs - How to monitor nodes for stake / apr etc - Move contracts to stake / unstake From 10c0c98f7b27d126f2bb5631f83f5df474fa6e48 Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Wed, 16 Oct 2024 17:02:20 +0200 Subject: [PATCH 26/50] add list of aggregators and publishers --- docs/usage/web-api.md | 76 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 65 insertions(+), 11 deletions(-) diff --git a/docs/usage/web-api.md b/docs/usage/web-api.md index d9fd2498..e064cf90 100644 --- a/docs/usage/web-api.md +++ b/docs/usage/web-api.md @@ -34,20 +34,74 @@ only authorized parties may access it, or other measures to manage gas costs. ## Using a public aggregator or publisher {#public-services} For some use cases (e.g., a public website), or to just try out the HTTP API, a publicly accessible -aggregator and/or publisher is required. Several entities run such aggregators and publishers; the -instances run by Mysten Labs are accessible at the following hosts: +aggregator and/or publisher is required. Several entities run such aggregators and publishers, see +the lists of public [aggregators](#public-aggregators) and [publishers](#public-publishers) below. -- Aggregator: `https://aggregator.walrus-testnet.walrus.space` -- Publisher: `https://publisher.walrus-testnet.walrus.space` - - - -Our publisher is currently limiting requests to 10 MiB. If you want to upload larger files, you need +Public publishers limit requests to 10 MiB by default. If you want to upload larger files, you need to [run your own publisher](#local-daemon) or use the [CLI](./client-cli.md). -Note that the publisher consumes (Testnet) SUI and WAL on the service side, and a Mainnet deployment -would likely not be able to provide uncontrolled public access to publishing without requiring some -authentication and compensation for the funds used. +Also, note that the publisher consumes (Testnet) SUI and WAL on the service side, and a Mainnet +deployment would likely not be able to provide uncontrolled public access to publishing without +requiring some authentication and compensation for the funds used. + +### Public aggregators + +The following is a list of know public aggregators; they are checked periodically, but each of them +may still be temporarily unavailable: + +- `https://aggregator.walrus-testnet.walrus.space` +- `https://wal-aggregator-testnet.staketab.org` +- `https://walrus-testnet-aggregator.bartestnet.com` +- `https://walrus-testnet.blockscope.net` +- `https://walrus-testnet-aggregator.nodes.guru` +- `https://walrus-cache-testnet.overclock.run` +- `https://sui-walrus-testnet.bwarelabs.com/aggregator` +- `https://walrus-testnet-aggregator.stakin-nodes.com` +- `https://testnet-aggregator-walrus.kiliglab.io` +- `https://walrus-cache-testnet.latitude-sui.com` +- `https://walrus-tn.juicystake.io:9443` +- `https://walrus-agg-testnet.chainode.tech:9002` +- `https://walrus-testnet-aggregator.starduststaking.com:444` +- `http://walrus-testnet-aggregator.everstake.one:9000` +- `http://walrus.testnet.pops.one:9000` +- `http://scarlet-brussels-376c2.walrus.bdnodes.net:9000` +- `http://aggregator.testnet.sui.rpcpool.com:9000` +- `http://walrus.krates.ai:9000` +- `http://walrus-testnet.stakingdefenseleague.com:9000` +- `http://walrus.sui.thepassivetrust.com:9000` + + + +### Public publishers + +- `https://publisher.walrus-testnet.walrus.space` +- `https://wal-publisher-testnet.staketab.org` +- `https://walrus-testnet-publisher.bartestnet.com` +- `https://walrus-testnet.blockscope.net:444` +- `https://walrus-testnet-publisher.nodes.guru` +- `https://walrus-publish-testnet.chainode.tech:9003` +- `https://sui-walrus-testnet.bwarelabs.com/publisher` +- `https://walrus-testnet-publisher.stakin-nodes.com` +- `https://testnet-publisher-walrus.kiliglab.io` +- `http://walrus-publisher-testnet.overclock.run:9001` +- `http://walrus-testnet-publisher.everstake.one:9001` +- `http://walrus.testnet.pops.one:9001` +- `http://ivory-dakar-e5812.walrus.bdnodes.net:9001` +- `http://publisher.testnet.sui.rpcpool.com:9001` +- `http://walrus.krates.ai:9001` +- `http://walrus-publisher-testnet.latitude-sui.com:9001` +- `http://walrus-tn.juicystake.io:9090` +- `http://walrus-testnet.stakingdefenseleague.com:9001` +- `http://walrus.sui.thepassivetrust.com:9001` + + ## HTTP API Usage From 4a4fbb082dd035c72c74e2105912c928a93dc1c2 Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Wed, 16 Oct 2024 18:33:02 +0200 Subject: [PATCH 27/50] update public aggregator/publisher list --- docs/usage/web-api.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/usage/web-api.md b/docs/usage/web-api.md index e064cf90..b45b4a4e 100644 --- a/docs/usage/web-api.md +++ b/docs/usage/web-api.md @@ -61,7 +61,7 @@ may still be temporarily unavailable: - `https://walrus-cache-testnet.latitude-sui.com` - `https://walrus-tn.juicystake.io:9443` - `https://walrus-agg-testnet.chainode.tech:9002` -- `https://walrus-testnet-aggregator.starduststaking.com:444` +- `https://walrus-testnet-aggregator.starduststaking.com:11444` - `http://walrus-testnet-aggregator.everstake.one:9000` - `http://walrus.testnet.pops.one:9000` - `http://scarlet-brussels-376c2.walrus.bdnodes.net:9000` @@ -80,12 +80,13 @@ Reported but currently not available: - `https://publisher.walrus-testnet.walrus.space` - `https://wal-publisher-testnet.staketab.org` - `https://walrus-testnet-publisher.bartestnet.com` -- `https://walrus-testnet.blockscope.net:444` - `https://walrus-testnet-publisher.nodes.guru` -- `https://walrus-publish-testnet.chainode.tech:9003` - `https://sui-walrus-testnet.bwarelabs.com/publisher` - `https://walrus-testnet-publisher.stakin-nodes.com` - `https://testnet-publisher-walrus.kiliglab.io` +- `https://walrus-testnet.blockscope.net:444` +- `https://walrus-publish-testnet.chainode.tech:9003` +- `https://walrus-testnet-publisher.starduststaking.com:11445` - `http://walrus-publisher-testnet.overclock.run:9001` - `http://walrus-testnet-publisher.everstake.one:9001` - `http://walrus.testnet.pops.one:9001` @@ -100,7 +101,6 @@ Reported but currently not available: ## HTTP API Usage From 0c58ba3ef3a3f93abc5bc8c303ba55d0deeca4b7 Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Wed, 16 Oct 2024 19:02:32 +0200 Subject: [PATCH 28/50] docs: update ToS and link to privacy policy --- docs/SUMMARY.md | 9 +- docs/devnet_tos.md | 50 +-- docs/legal/devnet_tos.md | 49 +++ docs/legal/privacy.md | 783 ++++++++++++++++++++++++++++++++++ docs/legal/testnet_tos.md | 73 ++++ docs/testnet_tos.md | 52 +-- docs/walrus-sites/legal.md | 3 - docs/walrus-sites/privacy.md | 784 +---------------------------------- docs/walrus-sites/tos.md | 6 +- 9 files changed, 915 insertions(+), 894 deletions(-) create mode 100644 docs/legal/devnet_tos.md create mode 100644 docs/legal/privacy.md create mode 100644 docs/legal/testnet_tos.md delete mode 100644 docs/walrus-sites/legal.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 1f2d4787..8cb31bc0 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -59,12 +59,11 @@ - [Redirecting objects to Walrus Sites](./walrus-sites/redirects.md) - [The Walrus Sites Portal](./walrus-sites/portal.md) - [Known restrictions](./walrus-sites/restrictions.md) -- [Legal terms](./walrus-sites/legal.md) - - [Privacy policy](./walrus-sites/privacy.md) - - [Terms of service](./walrus-sites/tos.md) +- [Terms of service](./walrus-sites/tos.md) --- [Glossary](./glossary.md) -[Devnet terms of service](./devnet_tos.md) -[Testnet terms of service](./testnet_tos.md) +[Devnet terms of service](./legal/devnet_tos.md) +[Testnet terms of service](./legal/testnet_tos.md) +[Privacy policy](./legal/privacy.md) diff --git a/docs/devnet_tos.md b/docs/devnet_tos.md index e5ceb996..737a4311 100644 --- a/docs/devnet_tos.md +++ b/docs/devnet_tos.md @@ -1,49 +1 @@ - -# DEVNET TERMS OF SERVICE - WALRUS - -Last updated: June 13, 2024 - -By using Mysten Labs Devnet software, technologies, tools, and other services (collectively -“Devnet”), you agree to the general Terms of Service and these additional Devnet Terms of Service -(together, the “Terms”). If you do not agree, do not participate in Devnet. If you are using Devnet -on behalf of an organization, you represent and warrant that you are an authorized representative of -that organization and have the authority to bind that business or entity to the Terms. - -## Eligibility Criteria - -You may use Devnet only if you: - -- Are 18 years or older and capable of forming a binding contract with us. -- Are not otherwise barred from participating in Devnet under applicable law. - -We may, at our discretion, introduce new or change existing eligibility criteria or conditions we -deem appropriate. Devnet may operate in certain phases, and your participation in any one phase of -Devnet does not guarantee that you will be selected for any other phases of Devnet. - -## Duration - -Devnet will commence on the date we prescribe and continue until terminated at our discretion. We -may change, discontinue, or wipe, temporarily or permanently, all or any part of Devnet, at any -time and without notice at our discretion, including, without limitation, the modification of the -presence, amounts, or any other conditions applicable to data you have stored within Devnet, without -any liability to you or other Devnet users. - -## No Warranty - -Mysten Labs provides the Devnet platform solely as a developer preview. Devnet is provided "as is" -and "with all faults." We make no warranties, express or implied, regarding the reliability, -accuracy, performance, or fitness for a particular purpose of the service provided. You accept all -risks associated with the use of Devnet and agree that Mysten Labs, its affiliates, and its -employees shall not be liable for any damages, whether direct, indirect, incidental, special, -consequential, or punitive, arising out of the use or inability to use the service, including but -not limited to lost profits, loss of business, or data loss. - -No employee or representative of Mysten Labs is authorized to make any warranties or representations -beyond those stated in this agreement. Any statements made by employees or representatives of Mysten -Labs regarding the service shall not be construed as warranties or representations, and customers -agree to indemnify and hold harmless Mysten Labs from any such statements. - -Any deficiencies or errors in the Devnet platform shall not constitute a breach of this agreement, -and customers agree to waive any right to seek a refund or compensation based on such deficiencies -or errors. This "as is, no warranty" provision shall survive the termination or expiration of any -other agreements between you and Mysten Labs. +# Devnet terms of service diff --git a/docs/legal/devnet_tos.md b/docs/legal/devnet_tos.md new file mode 100644 index 00000000..e5ceb996 --- /dev/null +++ b/docs/legal/devnet_tos.md @@ -0,0 +1,49 @@ + +# DEVNET TERMS OF SERVICE - WALRUS + +Last updated: June 13, 2024 + +By using Mysten Labs Devnet software, technologies, tools, and other services (collectively +“Devnet”), you agree to the general Terms of Service and these additional Devnet Terms of Service +(together, the “Terms”). If you do not agree, do not participate in Devnet. If you are using Devnet +on behalf of an organization, you represent and warrant that you are an authorized representative of +that organization and have the authority to bind that business or entity to the Terms. + +## Eligibility Criteria + +You may use Devnet only if you: + +- Are 18 years or older and capable of forming a binding contract with us. +- Are not otherwise barred from participating in Devnet under applicable law. + +We may, at our discretion, introduce new or change existing eligibility criteria or conditions we +deem appropriate. Devnet may operate in certain phases, and your participation in any one phase of +Devnet does not guarantee that you will be selected for any other phases of Devnet. + +## Duration + +Devnet will commence on the date we prescribe and continue until terminated at our discretion. We +may change, discontinue, or wipe, temporarily or permanently, all or any part of Devnet, at any +time and without notice at our discretion, including, without limitation, the modification of the +presence, amounts, or any other conditions applicable to data you have stored within Devnet, without +any liability to you or other Devnet users. + +## No Warranty + +Mysten Labs provides the Devnet platform solely as a developer preview. Devnet is provided "as is" +and "with all faults." We make no warranties, express or implied, regarding the reliability, +accuracy, performance, or fitness for a particular purpose of the service provided. You accept all +risks associated with the use of Devnet and agree that Mysten Labs, its affiliates, and its +employees shall not be liable for any damages, whether direct, indirect, incidental, special, +consequential, or punitive, arising out of the use or inability to use the service, including but +not limited to lost profits, loss of business, or data loss. + +No employee or representative of Mysten Labs is authorized to make any warranties or representations +beyond those stated in this agreement. Any statements made by employees or representatives of Mysten +Labs regarding the service shall not be construed as warranties or representations, and customers +agree to indemnify and hold harmless Mysten Labs from any such statements. + +Any deficiencies or errors in the Devnet platform shall not constitute a breach of this agreement, +and customers agree to waive any right to seek a refund or compensation based on such deficiencies +or errors. This "as is, no warranty" provision shall survive the termination or expiration of any +other agreements between you and Mysten Labs. diff --git a/docs/legal/privacy.md b/docs/legal/privacy.md new file mode 100644 index 00000000..140a0392 --- /dev/null +++ b/docs/legal/privacy.md @@ -0,0 +1,783 @@ + + +# PRIVACY POLICY + +**Last Updated:** June 18, 2024 + +This Privacy Policy is designed to help you understand how Mysten Labs, Inc., its subsidiaries and +affiliates (collectively called "**Mysten Labs**", "**we**," "**us**," and "**our**") collects, +uses, and shares your personal information and to help you understand and exercise your privacy +rights in accordance with applicable law. This Policy applies when you use our websites, contact our +team members, engage with us on social media or otherwise interact with us. + +1. [SCOPE](#scope) +1. [CHANGES TO OUR PRIVACY POLICY](#changes-to-our-privacy-policy) +1. [PERSONAL INFORMATION WE + COLLECT](#personal-information-we-collect) +1. [HOW WE USE YOUR INFORMATION](#how-we-use-your-information) +1. [HOW WE DISCLOSE YOUR + INFORMATION](#how-we-disclose-your-information) +1. [YOUR PRIVACY CHOICES AND + RIGHTS](#your-privacy-choices-and-rights) +1. [SECURITY OF YOUR + INFORMATION](#security-of-your-information) +1. [INTERNATIONAL DATA + TRANSFERS](#international-data-transfers) +1. [RETENTION OF PERSONAL + INFORMATION](#retention-of-personal-information) +1. [SUPPLEMENTAL NOTICE FOR CALIFORNIA + RESIDENTS](#supplemental-notice-for-california-residents) +1. [SUPPLEMENTAL NOTICE FOR NEVADA + RESIDENTS](#supplemental-notice-for-nevada-residents) +1. [CHILDREN'S INFORMATION](#childrens-information) +1. [THIRD-PARTY'S + WEBSITES/APPLICATIONS](#third-party-websitesapplications) +1. [SUPERVISORY AUTHORITY](#supervisory-authority) +1. [CONTACT US](#contact-us) + +## SCOPE + +This Privacy Policy applies to personal information processed by Mysten +Labs, including on our websites (the "**Site**"), and other online and +offline offerings. The Site, our services and our other online and +offline offerings are collectively called the "**Services**." For +clarity, the Services do not include the Walrus Protocol or any other +decentralized aspect of the Walrus or Sui Blockchain that is not +controlled by Mysten Labs due to the decentralized nature of these +blockchains. + +## CHANGES TO OUR PRIVACY POLICY + +We may revise this Privacy Policy from time to time in our sole +discretion. If there are any material changes to this Privacy Policy, we +will notify you as required by applicable law. You understand and agree +that you will be deemed to have accepted the updated Privacy Policy if +you continue to use our Services after the new Privacy Policy takes +effect. + +## PERSONAL INFORMATION WE COLLECT + +The categories of personal information we collect depend on how you +interact with us, our Services and the requirements of applicable law. +We collect information that you provide to us, information we obtain +automatically when you use our Services, and information from other +sources such as third-party services and organizations, as described +below. + +A. **Information You Provide to Us Directly** + +We may collect the following personal information that you provide to +us. + +- **Account Creation**. We may collect information if you create an + account with us, such as your name, username, email address, or + password. +- **Wallet and Transaction Information.** In order to engage in + transactions on the Services, you may need to provide us or our + third-party payment processors with access to or information about + your digital wallet. We will never ask you for or collect your + private keys. +- **Other Transactions**. We may collect personal information and + details associated with your activities on our Services, including + to deliver you your rewards associated with your use of the + Services. +- **Your Communications with Us**. We may collect personal + information, such as email address when you request information + about our Services, register for our newsletter or marketing + promotions, request customer or technical support, apply for a job + or otherwise communicate with us. +- **Interactive Features**. We and others who use our Services may + collect personal information that you submit or make available + through our interactive features (e.g., via the Mysten Labs + community, commenting functionalities, forums, blogs, and social + media pages). Any personal information you provide on the public + sections of these features will be considered "public," unless + otherwise required by applicable law, and is not subject to the + privacy protections referenced herein. +- **Surveys**. We may contact you to participate in surveys. If you + decide to participate, you may be asked to provide certain + information which may include personal information. +- **Sweepstakes, Giveaways or Contests**. We may collect personal + information you provide for any sweepstakes, giveaways or contests + that we offer. In some jurisdictions, we are required to publicly + share information of sweepstakes and contest winners. +- **Events.** We may collect personal information from individuals + when we attend or host conferences, trade shows, and other events. +- **Business Development and Strategic Partnerships.** We may collect + personal information from individuals and third parties to assess + and pursue potential business opportunities. +- **Job Applications.** We may post job openings and opportunities on + our Services. If you reply to one of these postings by submitting + your application, CV and/or cover letter to us, we will collect and + use this information to assess your qualifications. + +B. **Information Collected Automatically** + +We may collect personal information automatically when you use our +Services: +- **Automatic Data Collection**. We may collect certain information + automatically when you use our Services, such as your Internet + protocol (IP) address, user settings, MAC address, cookie + identifiers, mobile carrier, mobile advertising and other unique + identifiers, browser or device information, location information + (including approximate location derived from IP address), Internet + service provider, and metadata about the content you provide. We may + also automatically collect information regarding your use of our + Services, such as pages that you visit before, during and after + using our Services, information about the links you click, the types + of content you interact with, the frequency and duration of your + activities, and other information about how you use our Services. +- **Cookies, Pixel Tags/Web Beacons, and Other Technologies**. We, as + well as third parties that provide content, advertising, or other + functionality on our Services, may use cookies, pixel tags, local + storage, and other technologies ("**Technologies**") to + automatically collect information through your use of our Services. +- **Cookies**. Cookies are small text files placed in device browsers + that store preferences and facilitate and enhance your experience. +- **Pixel Tags/Web Beacons**. A pixel tag (also known as a web beacon) + is a piece of code embedded in our Services that collects + information about engagement on our Services. The use of a pixel tag + allows us to record, for example, that a user has visited a + particular web page or clicked on a particular advertisement. We may + also include web beacons in e-mails to understand whether messages + have been opened, acted on, or forwarded. + +> Our uses of these Technologies fall into the following general +> categories: + +- **Operationally Necessary**. This includes Technologies that allow + you access to our Services, applications, and tools that are + required to identify irregular website behavior, prevent fraudulent + activity, improve security, or allow you to make use of our + functionality; +- **Performance-Related**. We may use Technologies to assess the + performance of our Services, including as part of our analytic + practices to help us understand how individuals use our Services + (*see Analytics below*); +- **Functionality-Related**. We may use Technologies that allow us to + offer you enhanced functionality when accessing or using our + Services. This may include identifying you when you sign into our + Services or keeping track of your specified preferences, interests, + or past items viewed; +- **Advertising- or Targeting-Related**. We may use first party or + third-party Technologies to deliver content, including ads relevant + to your interests, on our Services or on third-party websites. + +> *See "[Your Privacy Choices and +> Rights](#your-privacy-choices-and-rights)" below to understand your +> choices regarding these Technologies.* + +- **Analytics**. We may use our Technologies and other third-party + tools to process analytics information on our Services. These + technologies allow us to process usage data to better understand how + our website and web-related Services are used, and to continually + improve and personalize our Services. Some of our analytics partners + include: +- **Google Analytics**. For more information about how Google uses + your data (including for its own purposes, e.g., for profiling or + linking it to other data), please visit [Google Analytics' Privacy + Policy](http://www.google.com/policies/privacy/partners/). + To learn more about how to opt-out of Google Analytics' use of your + information, please click + [here](http://tools.google.com/dlpage/gaoptout). +- **LinkedIn Analytics**. For more information, please visit + [LinkedIn Analytics' Privacy + Policy](https://www.linkedin.com/legal/privacy-policy). + To learn more about how to opt-out of LinkedIn's use of your + information, please click + [here](https://www.linkedin.com/help/linkedin/answer/62931?trk=microsites-frontend_legal_privacy-policy&lang=en). +- **Facebook Connect**. For more information, please visit Facebook's + [Data + Policy](https://www.facebook.com/policy.php?ref=pf). + You can object to the collection of your data by Facebook pixel, or + to the use of your data for the purpose of displaying Facebook ads + by contacting the following address while logged into your Facebook + account: https://www.facebook.com/settings?tab=ads. +- **Mixpanel**. For more information about Mixpanel, please visit + [Mixpanel's Privacy + Policy](https://mixpanel.com/legal/privacy-policy/). +- **Social Media Platforms**. Our Services may contain social media + buttons, such as Discord, Twitter, Instagram, TikTok, Youtube, and + Telegram, which might include widgets such as the "share this" + button or other interactive mini programs. These features may + collect your IP address and which page you are visiting on our + Services and may set a cookie to enable the feature to function + properly. Your interactions with these platforms are governed by the + privacy policy of the company providing it. + +C. **Information Collected from Other Sources** + +- **Third-Party Sources.** We may obtain information about you from + other sources, including through third-party services and + organizations. For example, if you access our Services through a + third-party application, such as an app store, a third-party login + service, or a social networking site, we may collect information + about you from that third-party application that you have made + available via your privacy settings. +- **Referrals, Sharing and Other Features**. Our Services may offer + various tools and functionalities that allow you to provide + information about your friends through our referral service; third + parties may also use these services to upload information about you. + Our referral services may also allow you to forward or share certain + content with a friend or colleague, such as an email inviting your + friend to use our Services. Please only share with us contact + information of people with whom you have a relationship (e.g., + relative, friend, neighbor, or co-worker). + +## HOW WE USE YOUR INFORMATION + +We use your information for a variety of business purposes, including to +provide our Services, for administrative purposes, and to market our +products and Services, as described below. + +A. **Provide Our Services** + +We use your information to fulfill our contract with you and provide you +with our Services and perform our contract with you, such as: + +- Managing your information and accounts; +- Providing access to certain areas, functionalities, and features of + our Services; +- Answering requests for customer or technical support; +- Communicating with you about your account, activities on our + Services, and policy changes; +- Processing information about your wallet to facilitate transfers via + the Services; +- Processing applications if you apply for a job, we post on our + Services; and +- Allowing you to register for events. + +B. **Administrative Purposes** + +We use your information for our legitimate interest, such as: + +- Pursuing our legitimate interests such as direct marketing, research + and development (including marketing research), network and + information security, and fraud prevention; +- Detecting security incidents, protecting against malicious, + deceptive, fraudulent or illegal activity, and prosecuting those + responsible for that activity; +- Measuring interest and engagement in our Services; +- Improving, upgrading or enhancing our Services; +- Developing new products and Services; +- Ensuring internal quality control and safety; +- Authenticating and verifying individual identities; +- Debugging to identify and repair errors with our Services; +- Auditing relating to interactions, transfers and other compliance + activities; +- Sharing information with third parties as needed to provide the + Services; +- Enforcing our agreements and policies; and +- Other uses as required to comply with our legal obligations. + +C. **Marketing and Advertising our Products and Services** + +We may use personal information to tailor and provide you with content +and advertisements. We may provide you with these materials as permitted +by applicable law. Some of the ways we may market to you include email +campaigns, custom audiences advertising, and "interest-based" or +"personalized advertising," including through cross-device tracking. + +If you have any questions about our marketing practices or if you would +like to opt out of the use of your personal information for marketing +purposes, you may contact us at any time as set forth in "[Contact +Us](#contact-us)" below. + +D. **With Your Consent** + +We may use personal information for other purposes that are clearly +disclosed to you at the time you provide personal information or with +your consent. + +E. **Other Purposes** + +We also use your information for other purposes as requested by you or +as permitted by applicable law. + +- **Automated Decision Making**. We may engage in automated decision + making, including profiling. Mysten Labs's processing of your + personal information will not result in a decision based solely on + automated processing that significantly affects you unless such a + decision is necessary as part of a contract we have with you, we + have your consent, or we are permitted by law to engage in such + automated decision making. If you have questions about our automated + decision making, you may contact us as set forth in "[Contact + Us](#contact-us)" below. +- **De-identified and Aggregated Information.** We may use personal + information and other information about you to create de-identified + and/or aggregated information, such as de-identified demographic + information, de-identified location information, information about + the device from which you access our Services, or other analyses we + create. + +## HOW WE DISCLOSE YOUR INFORMATION + +We disclose your information to third parties for a variety of business +purposes, including to provide our Services, to protect us or others, or +in the event of a major business transaction such as a merger, sale, or +asset transfer, as described below. + +A. **Disclosures to Provide our Services** + +The categories of third parties with whom we may share your information +are described below. + +- **Notice Regarding Use of Blockchain.** Transactions on the Services + will be conducted via Blockchain. Information about your transfers + will be provided to a Blockchain and may be accessible to third + parties due to the public nature of the Blockchain. Because entries + to a Blockchain are, by their nature, public, and because it may be + possible for someone to identify you through your pseudonymous, + public wallet address using external sources of information, any + transaction you enter onto the Blockchain could possibly be used to + identify you, or information about you. +- **Other Users of the Services and Parties You Transact With.** Some + of your personal information may be visible to other users of the + Services (e.g., information featured on generally accessible parts + of your profile; usernames of other Mysten Labs Services users). In + addition, to complete transfers via the Services, we will need to + share some of your personal information with the party that you are + transacting with. +- **Third Party Websites and Applications.** You may choose to share + personal information or interact with third-party websites and/or + third-party applications, including, but not limited to, third-party + electronic wallet extensions. Once your personal information has + been shared with a third-party website or a third-party application, + it will also be subject to such third party's privacy policy. We + encourage you to closely read each third-party website or + third-party application privacy policy before sharing your personal + information or otherwise interacting with them. Please note that we + do not control, and we are not responsible for the third-party + website's or the third-party application's processing of your + personal information. +- **Service Providers**. We may share your personal information with + our third-party service providers who use that information to help + us provide our Services. This includes service providers that + provide us with IT support, hosting, customer service, and related + services. +- **Business Partners**. We may share your personal information with + business partners to provide you with a product or service you have + requested. We may also share your personal information to business + partners with whom we jointly offer products or services. +- **Affiliates**. We may share your personal information with members + of our corporate family. +- **Other Users/Website Visitors**. As described above in "[Personal + Information We + Collect](#personal-information-we-collect)," our + Services allow you to share your profile and/or User Content with + other users or publicly, including to those who do not use our + Services. +- **Advertising Partners**. We may share your personal information + with third-party advertising partners. These third-party advertising + partners may set Technologies and other tracking tools on our + Services to collect information regarding your activities and your + device (e.g., your IP address, cookie identifiers, page(s) visited, + location, time of day). These advertising partners may use this + information (and similar information collected from other services) + for purposes of delivering personalized advertisements to you when + you visit digital properties within their networks. This practice is + commonly referred to as "interest-based advertising" or + "personalized advertising." +- **APIs/SDKs**. We may use third-party application program interfaces + ("**APIs**") and software development kits ("**SDKs**") as part of + the functionality of our Services. For more information about our + use of APIs and SDKs, please contact us as set forth in "[Contact + Us](#contact-us)" below. + +B. **Disclosures to Protect Us or Others** + +We may access, preserve, and disclose any information we store +associated with you to external parties if we, in good faith, believe +doing so is required or appropriate to: comply with law enforcement or +national security requests and legal process, such as a court order or +subpoena; protect your, our, or others' rights, property, or safety; +enforce our policies or contracts; collect amounts owed to us; or assist +with an investigation or prosecution of suspected or actual illegal +activity. + +C. **Disclosure in the Event of Merger, Sale, or Other Asset + Transfers** + +If we are involved in a merger, acquisition, financing due diligence, +reorganization, bankruptcy, receivership, purchase or sale of assets, or +transition of service to another provider, your information may be sold +or transferred as part of such a transaction, as permitted by law and/or +contract. + +## YOUR PRIVACY CHOICES AND RIGHTS + +**Your Privacy Choices**. The privacy choices you may have about your +personal information are determined by applicable law and are described +below. + +- **Email Communications**. If you receive an unwanted email from us, + you can use the unsubscribe link found at the bottom of the email to + opt out of receiving future emails. Note that you will continue to + receive transfer-related emails regarding Services you have + requested. We may also send you certain non-promotional + communications regarding us and our Services, and you will not be + able to opt out of those communications (e.g., communications + regarding our Services or updates to our Terms of Service or this + Privacy Policy). +- **Text Messages**. You may opt out of receiving text messages from + us by following the instructions in the text message you have + received from us or by otherwise contacting us. +- **Mobile Devices**. We may send you push notifications through our + mobile application. You may opt out from receiving these push + notifications by changing the settings on your mobile device. With + your consent, we may also collect precise location-based information + via our mobile application. You may opt out of this collection by + changing the settings on your mobile device. +- **"Do Not Track**.**"** Do Not Track ("**DNT**") is a privacy + preference that users can set in certain web browsers. Please note + that we do not respond to or honor DNT signals or similar mechanisms + transmitted by web browsers. +- **Cookies and Interest-Based Advertising**. You may stop or restrict + the placement of Technologies on your device or remove them by + adjusting your preferences as your browser or device permits. + However, if you adjust your preferences, our Services may not work + properly. Please note that cookie-based opt-outs are not effective + on mobile applications. However, you may opt-out of personalized + advertisements on some mobile applications by following the + instructions for + [Android](https://support.google.com/googleplay/android-developer/answer/6048248?hl=en), + [iOS](https://support.apple.com/en-us/HT202074) and + [others](https://www.networkadvertising.org/mobile-choice/). + +> The online advertising industry also provides websites from which you +> may opt out of receiving targeted ads from data partners and other +> advertising partners that participate in self-regulatory programs. You +> can access these and learn more about targeted advertising and +> consumer choice and privacy by visiting the [Network Advertising +> Initiative](http://www.networkadvertising.org/managing/opt_out.asp), +> [the Digital Advertising +> Alliance](http://www.aboutads.info/choices/), [the +> European Digital Advertising +> Alliance](https://www.youronlinechoices.eu/), and [the +> Digital Advertising Alliance of +> Canada](https://youradchoices.ca/choices/). +> +> Please note you must separately opt out in each browser and on each +> device. + +**Your Privacy Rights**. In accordance with applicable law, you may have +the right to: + +- **Access Personal Information about you, including: (i) confirming whether we are processing your + personal information; (ii) obtaining access to or a copy of your personal information; or (iii) + receiving an electronic copy of personal information that you have provided to us, or asking us to + send that information to another company (aka the right of data portability);** + +- **Request Correction** of your personal information where it is + inaccurate or incomplete. In some cases, we may provide self-service + tools that enable you to update your personal information; + +## Request Deletion of your personal information; + +- **Request Restriction of or Object** to our processing of your + personal information, including where the processing of your + personal information is based on our legitimate interest or for + direct marketing purposes; and + +**Withdraw Your Consent to our processing of your personal information. Please note that your +withdrawal will only take effect for future processing and will not affect the lawfulness of +processing before the withdrawal.** + +If you would like to exercise any of these rights, please contact us as +set forth in "[Contact Us](#contact-us)" below. We will +process such requests in accordance with applicable laws. + +## SECURITY OF YOUR INFORMATION + +We take steps designed to ensure that your information is treated +securely and in accordance with this Privacy Policy. Unfortunately, no +system is 100% secure, and we cannot ensure or warrant the security of +any information you provide to us. To the fullest extent permitted by +applicable law, we do not accept liability for unauthorized disclosure. + +By using our Services or providing personal information to us, you agree +that we may communicate with you electronically regarding security, +privacy, and administrative issues relating to your use of our Services. +If we learn of a security system's breach, we may attempt to notify you +electronically by posting a notice on our Services, by mail or by +sending an email to you. + +## INTERNATIONAL DATA TRANSFERS + +All information processed by us may be transferred, processed, and +stored anywhere in the world, including, but not limited to, the United +States or other countries, which may have data protection laws that are +different from the laws where you live. We endeavor to safeguard your +information consistent with the requirements of applicable laws. + +If we transfer personal information which originates in the European +Economic Area, Switzerland, and/or the United Kingdom to a country that +has not been found to provide an adequate level of protection under +applicable data protection laws, one of the safeguards we may use to +support such transfer is the EU Standard Contractual Clauses. + +## RETENTION OF PERSONAL INFORMATION + +We store the personal information we collect as described in this +Privacy Policy for as long as you use our Services or as necessary to +fulfill the purpose(s) for which it was collected, provide our Services, +resolve disputes, establish legal defenses, conduct audits, pursue +legitimate business purposes, enforce our agreements, and comply with +applicable laws. + +To determine the appropriate retention period for personal information, +we may consider applicable legal requirements, the amount, nature, and +sensitivity of the personal information, certain risk factors, the +purposes for which we process your personal information, and whether we +can achieve those purposes through other means. + +## SUPPLEMENTAL NOTICE FOR CALIFORNIA RESIDENTS + +This Supplemental Notice for California Residents only applies to our +processing of personal information that is subject to the California +Consumer Privacy Act of 2018 ("**CCPA**"). Mysten Labs does not believe +it is subject to the CCPA. That said, Mysten Labs provides this +supplemental notice for purposes of transparency. The CCPA provides +California residents with the right to know what categories of personal +information Mysten Labs has collected about them and whether Mysten Labs +disclosed that personal information for a business purpose (e.g., to a +service provider) in the preceding twelve months. California residents +can find this information below: + + + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Category of Personal Information Collected by Mysten +LabsCategories of Third Parties Personal Information is +Disclosed to for a Business Purpose

Identifiers

+

A real name, postal address, unique personal identifier, online +identifier, Internet Protocol address, email address, account name, or +other similar identifiers.

    +
  • Service providers

  • +
  • Third-party websites or applications

  • +
  • Blockchain networks

  • +
  • Other users or third parties you interact with

  • +
  • Advertising partners

  • +
  • Data analytics providers

  • +
  • Other users/public (alias only)

  • +

Personal information categories listed in Cal. Civ. Code +§ 1798.80(e)

+

A name, signature, Social Security number, address, telephone number, +passport number, driver’s license or state identification card number, +insurance policy number, education, employment, employment history, bank +account number, credit card number, debit card number, or any other +financial information. Personal Information does not include publicly +available information that is lawfully made available to the general +public from federal, state, or local government records. Note: Some +personal information included in this category may overlap with other +categories.

    +
  • Service providers

  • +
  • Third-party websites or applications (e.g., wallet
    +providers; third-party identity verification services)

  • +
  • Blockchain networks

  • +
  • Data analytics providers

  • +
  • Other users or third parties you interact with

  • +

Protected classification characteristics under California +or federal law

+

Age (40 years or older), race, color, ancestry, national origin, +citizenship, religion or creed, marital status, medical condition, +physical or mental disability, sex (including gender, gender identity, +gender expression, pregnancy or childbirth and related medical +conditions), sexual orientation, veteran or military status, genetic +information (including familial genetic information).

    +
  • Service providers (recruitment context).

  • +

Commercial information

+

Records of personal property, products or services purchased, +obtained, or considered, or other purchasing or consuming histories or +tendencies.

    +
  • Service providers

  • +
  • Blockchain networks

  • +
  • Data analytics providers

  • +
  • Other users or third parties you interact with

  • +

Internet or other electronic network +activity

+

Browsing history, search history, information on a consumer’s +interaction with an internet website, application, or +advertisement.

    +
  • Service providers

  • +
  • Blockchain networks

  • +
  • Data analytics providers

  • +
  • Other users or third parties you interact with

  • +
  • Advertising partners

  • +

Professional or employment-related +information

+

Current or past job history or performance evaluations.

    +
  • Service providers

  • +

Inferences drawn from other personal information to +create a profile about a consumer

+

Profile reflecting a consumer’s preferences, characteristics, +psychological trends, predispositions, behavior, attitudes, +intelligence, abilities, and aptitudes.

    +
  • Service providers

  • +
  • Data analytics providers

  • +
  • Advertising partners

  • +
+ +The categories of sources from which we collect personal information and +our business and commercial purposes for using personal information are +set forth in "[Personal Information we +Collect](#personal-information-we-collect)" and "[How We +Use of Your Information](#how-we-use-your-information)" above, +respectively. + +**"Sales" of Personal Information under the CCPA.** For purposes of the +CCPA, Mysten Labs does not "sell" personal information, nor do we have +actual knowledge of any "sale" of personal information of minors under +16 years of age. + +**Additional Privacy Rights for California Residents** + +**Non-Discrimination.** California residents have the right not to +receive discriminatory treatment by us for the exercise of their rights +conferred by the CCPA. + +**Authorized Agent.** Only you, or someone legally authorized to act on +your behalf, may make a verifiable consumer request related to your +personal information. To designate an authorized agent, please contact +us as set forth in "[Contact Us](#contact-us)" below and +provide written authorization signed by you and your designated agent. + +**Verification**. To protect your privacy, we will take the following +steps to verify your identity before fulfilling your request. When you +make a request, we will ask you to provide sufficient information that +allows us to reasonably verify you are the person about whom we +collected personal information or an authorized representative, which +may include confirming the email address associated with any personal +information we have about you. If you are a California resident and +would like to exercise any of your rights under the CCPA, please contact +us as set forth in "[Contact Us](#contact-us)" below. We +will process such requests in accordance with applicable laws. + +**Refer-a-Friend and Similar In +"[How We Use Your Personal +Information](#how-we-use-your-information) we may offer referral +programs or other incentivized data collection programs. For example, we +may offer incentives to you such as discounts or promotional items or +credit in connection with these programs, wherein you provide your +personal information in exchange for a reward, or provide personal +information regarding your friends or colleagues (such as their email +address) and receive rewards when they sign up to use our Services. (The +referred party may also receive rewards for signing up via your +referral.) These programs are entirely voluntary and allow us to grow +our business and provide additional benefits to you. The value of your +data to us depends on how you ultimately use our Services, whereas the +value of the referred party's data to us depends on whether the referred +party ultimately becomes a user and uses our Services. Said value will +be reflected in the incentive offered in connection with each program. + +**Accessibility**. This Privacy Policy uses industry-standard +technologies and was developed in line with the World Wide Web +Consortium's Web Content Accessibility Guidelines, version 2.1. If you +wish to print this policy, please do so from your web browser or by +saving the page as a PDF. + +**California Shine the Light**. The California "Shine the Light" law +permits users who are California residents to request and obtain from us +once a year, free of charge, a list of the third parties to whom we have +disclosed their personal information (if any) for their direct marketing +purposes in the prior calendar year, as well as the type of personal +information disclosed to those parties. + +**Right for minors to remove posted content.** Where required by law, +California residents under the age of 18 may request to have their +posted content or information removed from the publicly viewable +portions of the Services by contacting us directly as set forth in +"[Contact Us](#contact-us)" below. + +## SUPPLEMENTAL NOTICE FOR NEVADA RESIDENTS + +If you are a resident of Nevada, you have the right to opt-out of the +sale of certain personal information to third parties who intend to +license or sell that personal information. You can exercise this right +by contacting us as set forth in "[Contact +Us](#contact-us)" below with the subject line "Nevada Do +Not Sell Request" and providing us with your name and the email address +associated with your account. Please note that we do not currently sell +your personal information as sales are defined in Nevada Revised +Statutes Chapter 603A. + +## CHILDREN'S INFORMATION + +The Services are not directed to children under 13 (or other age as +required by local law), and we do not knowingly collect personal +information from children. If you learn that your child has provided us +with personal information without your consent, you may contact us as +set forth in "[Contact Us](#contact-us)" below. If we +learn that we have collected a child's personal information in violation +of applicable law, we will promptly take steps to delete such +information. + +## THIRD-PARTY WEBSITES/APPLICATIONS + +The Services may contain links to other websites/applications (such as +GitHub) and other websites/applications may reference or link to our +Services. These third-party services are not controlled by us. We +encourage our users to read the privacy policies of each website and +application with which they interact. We do not endorse, screen or +approve, and are not responsible for, the privacy practices or content +of such other websites or applications. Providing personal information +to third-party websites or applications is at your own risk. + +## SUPERVISORY AUTHORITY + +If you are located in the European Economic Area, Switzerland, the +United Kingdom, or Brazil, you have the right to lodge a complaint with +a supervisory authority if you believe our processing of your personal +information violates applicable law. + +## CONTACT US + +If you have any questions about our privacy practices or this Privacy +Policy, or to exercise your rights as detailed in this Privacy Policy, +please contact us at: + +Mysten Labs, Inc. \ +Attn: Privacy Group \ +379 University Ave, #200 \ +Palo Alto, CA 94301 \ +[privacy@mystenlabs.com](mailto:privacy@mystenlabs.com) \ ++1 (408) 384-8237 diff --git a/docs/legal/testnet_tos.md b/docs/legal/testnet_tos.md new file mode 100644 index 00000000..6b00ea9e --- /dev/null +++ b/docs/legal/testnet_tos.md @@ -0,0 +1,73 @@ + +# TESTNET TERMS OF SERVICE - WALRUS + +Last updated: October 17, 2024 + +By using Mysten Labs Testnet software, technologies, tools, and other services (collectively +**“Testnet”**), you agree to the general Terms of Service and these additional Testnet Terms of +Service (together, the **“Terms”**). If you do not agree, do not participate in Testnet. If you are +using Testnet on behalf of an organization, you represent and warrant that you are an authorized +representative of that organization and have the authority to bind that business or entity to the +Terms. + +## Privacy Policy + +Please review our [Privacy Policy](../legal/privacy.md), which also governs your use of Testnet, for +information on how we collect, use and share your information. By using Testnet you agree to be +bound by our Privacy Policy. + +## Eligibility Criteria + +You may use Testnet only if you: + +- Are 18 years or older and capable of forming a binding contract with us. +- Are not otherwise barred from participating in Testnet under applicable law. + +We may, at our discretion, introduce new or change existing eligibility criteria or conditions we +deem appropriate. Testnet may operate in certain phases, and your participation in any one phase of +Testnet does not guarantee that you will be able to participate in any other phases of Testnet. + +## Duration + +Testnet will commence on the date we prescribe and continue until terminated at our discretion. We +may change, discontinue, or wipe, temporarily or permanently, all or any part of Testnet, at any +time and without notice at our discretion, including, without limitation, the modification of the +presence, amounts, or any other conditions applicable to the Testnet tokens, without any liability +to you or other Testnet users. We do not guarantee that Testnet tokens will continue to be offered +for any particular length of time and you may not rely upon the continued availability of any +Testnet tokens. In the event of the expiration of Testnet, you acknowledge and agree that your +access to and use of your Testnet tokens will be removed, and all accrued Testnet tokens will be +deleted from the Testnet system. Testnet tokens will not be converted into any future rewards +offered by Mysten Labs. + +## Testnet Tokens + +During Testnet you may accumulate Testnet tokens, such as through the Testnet faucet, which are not, +and will never convert to or accrue to become Mainnet tokens or any other tokens or virtual assets. +Testnet tokens are virtual items with no monetary value. Testnet tokens do not constitute any +currency or property of any type and are not redeemable, refundable, or eligible for any fiat or +virtual currency or anything else of monetary value, under any circumstances. Testnet tokens are not +transferable between users outside of the Testnet, and you may not attempt to sell, trade, or +transfer any Testnet tokens outside of the Testnet, or obtain any manner of credit using any Testnet +tokens. Any attempt to sell, trade, or transfer any Testnet tokens outside of the Testnet will be +null and void. + +## No Warranty + +Mysten Labs provides the Testnet platform solely as a developer test environment. Testnet is +provided "as is" and "with all faults." We make no warranties, express or implied, regarding the +reliability, accuracy, performance, or fitness for a particular purpose of the service provided. You +accept all risks associated with the use of Testnet and agree that Mysten Labs, its affiliates, and +its employees shall not be liable for any damages, whether direct, indirect, incidental, special, +consequential, or punitive, arising out of the use or inability to use the service, including but +not limited to lost profits, loss of business, or data loss. + +No employee or representative of Mysten Labs is authorized to make any warranties or representations +beyond those stated in this agreement. Any statements made by employees or representatives of Mysten +Labs regarding the service shall not be construed as warranties or representations, and customers +agree to indemnify and hold harmless Mysten Labs from any such statements. + +Any deficiencies or errors in the Testnet platform shall not constitute a breach of this agreement, +and customers agree to waive any right to seek a refund or compensation based on such deficiencies +or errors. This "as is, no warranty" provision shall survive the termination or expiration of any +other agreements between you and Mysten Labs. diff --git a/docs/testnet_tos.md b/docs/testnet_tos.md index bac13308..289f5b02 100644 --- a/docs/testnet_tos.md +++ b/docs/testnet_tos.md @@ -1,51 +1 @@ - -# TESTNET TERMS OF SERVICE - WALRUS - - -Last updated: October 10, 2024 - -By using Mysten Labs Testnet software, technologies, tools, and other services (collectively -“Testnet”), you agree to the general Terms of Service and these additional Testnet Terms of Service -(together, the “Terms”). If you do not agree, do not participate in Testnet. If you are using -Testnet on behalf of an organization, you represent and warrant that you are an authorized -representative of that organization and have the authority to bind that business or entity to the -Terms. - -## Eligibility Criteria - -You may use Testnet only if you: - -- Are 18 years or older and capable of forming a binding contract with us. -- Are not otherwise barred from participating in Testnet under applicable law. - -We may, at our discretion, introduce new or change existing eligibility criteria or conditions we -deem appropriate. Testnet may operate in certain phases, and your participation in any one phase of -Testnet does not guarantee that you will be selected for any other phases of Testnet. - -## Duration - -Testnet will commence on the date we prescribe and continue until terminated at our discretion. We -may change, discontinue, or wipe, temporarily or permanently, all or any part of Testnet, at any -time and without notice at our discretion, including, without limitation, the modification of the -presence, amounts, or any other conditions applicable to data you have stored within Testnet, -without any liability to you or other Testnet users. - -## No Warranty - -Mysten Labs provides the Testnet platform solely as a developer preview. Testnet is provided "as is" -and "with all faults." We make no warranties, express or implied, regarding the reliability, -accuracy, performance, or fitness for a particular purpose of the service provided. You accept all -risks associated with the use of Testnet and agree that Mysten Labs, its affiliates, and its -employees shall not be liable for any damages, whether direct, indirect, incidental, special, -consequential, or punitive, arising out of the use or inability to use the service, including but -not limited to lost profits, loss of business, or data loss. - -No employee or representative of Mysten Labs is authorized to make any warranties or representations -beyond those stated in this agreement. Any statements made by employees or representatives of Mysten -Labs regarding the service shall not be construed as warranties or representations, and customers -agree to indemnify and hold harmless Mysten Labs from any such statements. - -Any deficiencies or errors in the Testnet platform shall not constitute a breach of this agreement, -and customers agree to waive any right to seek a refund or compensation based on such deficiencies -or errors. This "as is, no warranty" provision shall survive the termination or expiration of any -other agreements between you and Mysten Labs. +# Testnet terms of service diff --git a/docs/walrus-sites/legal.md b/docs/walrus-sites/legal.md deleted file mode 100644 index 45ed7251..00000000 --- a/docs/walrus-sites/legal.md +++ /dev/null @@ -1,3 +0,0 @@ -# Legal terms - -The following sections contain the legal terms for Walrus Sites. diff --git a/docs/walrus-sites/privacy.md b/docs/walrus-sites/privacy.md index 8e25ae4b..211250a3 100644 --- a/docs/walrus-sites/privacy.md +++ b/docs/walrus-sites/privacy.md @@ -1,783 +1 @@ - - -# Walrus Sites - PRIVACY POLICY - -**Last Updated:** June 18, 2024 - -This Privacy Policy is designed to help you understand how Mysten Labs, Inc., its subsidiaries and -affiliates (collectively called "**Mysten Labs**", "**we**," "**us**," and "**our**") collects, -uses, and shares your personal information and to help you understand and exercise your privacy -rights in accordance with applicable law. This Policy applies when you use our websites, contact our -team members, engage with us on social media or otherwise interact with us. - -1. [SCOPE](#scope) -1. [CHANGES TO OUR PRIVACY POLICY](#changes-to-our-privacy-policy) -1. [PERSONAL INFORMATION WE - COLLECT](#personal-information-we-collect) -1. [HOW WE USE YOUR INFORMATION](#how-we-use-your-information) -1. [HOW WE DISCLOSE YOUR - INFORMATION](#how-we-disclose-your-information) -1. [YOUR PRIVACY CHOICES AND - RIGHTS](#your-privacy-choices-and-rights) -1. [SECURITY OF YOUR - INFORMATION](#security-of-your-information) -1. [INTERNATIONAL DATA - TRANSFERS](#international-data-transfers) -1. [RETENTION OF PERSONAL - INFORMATION](#retention-of-personal-information) -1. [SUPPLEMENTAL NOTICE FOR CALIFORNIA - RESIDENTS](#supplemental-notice-for-california-residents) -1. [SUPPLEMENTAL NOTICE FOR NEVADA - RESIDENTS](#supplemental-notice-for-nevada-residents) -1. [CHILDREN'S INFORMATION](#childrens-information) -1. [THIRD-PARTY'S - WEBSITES/APPLICATIONS](#third-party-websitesapplications) -1. [SUPERVISORY AUTHORITY](#supervisory-authority) -1. [CONTACT US](#contact-us) - -## SCOPE - -This Privacy Policy applies to personal information processed by Mysten -Labs, including on our websites (the "**Site**"), and other online and -offline offerings. The Site, our services and our other online and -offline offerings are collectively called the "**Services**." For -clarity, the Services do not include the Walrus Protocol or any other -decentralized aspect of the Walrus or Sui Blockchain that is not -controlled by Mysten Labs due to the decentralized nature of these -blockchains. - -## CHANGES TO OUR PRIVACY POLICY - -We may revise this Privacy Policy from time to time in our sole -discretion. If there are any material changes to this Privacy Policy, we -will notify you as required by applicable law. You understand and agree -that you will be deemed to have accepted the updated Privacy Policy if -you continue to use our Services after the new Privacy Policy takes -effect. - -## PERSONAL INFORMATION WE COLLECT - -The categories of personal information we collect depend on how you -interact with us, our Services and the requirements of applicable law. -We collect information that you provide to us, information we obtain -automatically when you use our Services, and information from other -sources such as third-party services and organizations, as described -below. - -A. **Information You Provide to Us Directly** - -We may collect the following personal information that you provide to -us. - -- **Account Creation**. We may collect information if you create an - account with us, such as your name, username, email address, or - password. -- **Wallet and Transaction Information.** In order to engage in - transactions on the Services, you may need to provide us or our - third-party payment processors with access to or information about - your digital wallet. We will never ask you for or collect your - private keys. -- **Other Transactions**. We may collect personal information and - details associated with your activities on our Services, including - to deliver you your rewards associated with your use of the - Services. -- **Your Communications with Us**. We may collect personal - information, such as email address when you request information - about our Services, register for our newsletter or marketing - promotions, request customer or technical support, apply for a job - or otherwise communicate with us. -- **Interactive Features**. We and others who use our Services may - collect personal information that you submit or make available - through our interactive features (e.g., via the Mysten Labs - community, commenting functionalities, forums, blogs, and social - media pages). Any personal information you provide on the public - sections of these features will be considered "public," unless - otherwise required by applicable law, and is not subject to the - privacy protections referenced herein. -- **Surveys**. We may contact you to participate in surveys. If you - decide to participate, you may be asked to provide certain - information which may include personal information. -- **Sweepstakes, Giveaways or Contests**. We may collect personal - information you provide for any sweepstakes, giveaways or contests - that we offer. In some jurisdictions, we are required to publicly - share information of sweepstakes and contest winners. -- **Events.** We may collect personal information from individuals - when we attend or host conferences, trade shows, and other events. -- **Business Development and Strategic Partnerships.** We may collect - personal information from individuals and third parties to assess - and pursue potential business opportunities. -- **Job Applications.** We may post job openings and opportunities on - our Services. If you reply to one of these postings by submitting - your application, CV and/or cover letter to us, we will collect and - use this information to assess your qualifications. - -B. **Information Collected Automatically** - -We may collect personal information automatically when you use our -Services: -- **Automatic Data Collection**. We may collect certain information - automatically when you use our Services, such as your Internet - protocol (IP) address, user settings, MAC address, cookie - identifiers, mobile carrier, mobile advertising and other unique - identifiers, browser or device information, location information - (including approximate location derived from IP address), Internet - service provider, and metadata about the content you provide. We may - also automatically collect information regarding your use of our - Services, such as pages that you visit before, during and after - using our Services, information about the links you click, the types - of content you interact with, the frequency and duration of your - activities, and other information about how you use our Services. -- **Cookies, Pixel Tags/Web Beacons, and Other Technologies**. We, as - well as third parties that provide content, advertising, or other - functionality on our Services, may use cookies, pixel tags, local - storage, and other technologies ("**Technologies**") to - automatically collect information through your use of our Services. -- **Cookies**. Cookies are small text files placed in device browsers - that store preferences and facilitate and enhance your experience. -- **Pixel Tags/Web Beacons**. A pixel tag (also known as a web beacon) - is a piece of code embedded in our Services that collects - information about engagement on our Services. The use of a pixel tag - allows us to record, for example, that a user has visited a - particular web page or clicked on a particular advertisement. We may - also include web beacons in e-mails to understand whether messages - have been opened, acted on, or forwarded. - -> Our uses of these Technologies fall into the following general -> categories: - -- **Operationally Necessary**. This includes Technologies that allow - you access to our Services, applications, and tools that are - required to identify irregular website behavior, prevent fraudulent - activity, improve security, or allow you to make use of our - functionality; -- **Performance-Related**. We may use Technologies to assess the - performance of our Services, including as part of our analytic - practices to help us understand how individuals use our Services - (*see Analytics below*); -- **Functionality-Related**. We may use Technologies that allow us to - offer you enhanced functionality when accessing or using our - Services. This may include identifying you when you sign into our - Services or keeping track of your specified preferences, interests, - or past items viewed; -- **Advertising- or Targeting-Related**. We may use first party or - third-party Technologies to deliver content, including ads relevant - to your interests, on our Services or on third-party websites. - -> *See "[Your Privacy Choices and -> Rights](#your-privacy-choices-and-rights)" below to understand your -> choices regarding these Technologies.* - -- **Analytics**. We may use our Technologies and other third-party - tools to process analytics information on our Services. These - technologies allow us to process usage data to better understand how - our website and web-related Services are used, and to continually - improve and personalize our Services. Some of our analytics partners - include: -- **Google Analytics**. For more information about how Google uses - your data (including for its own purposes, e.g., for profiling or - linking it to other data), please visit [Google Analytics' Privacy - Policy](http://www.google.com/policies/privacy/partners/). - To learn more about how to opt-out of Google Analytics' use of your - information, please click - [here](http://tools.google.com/dlpage/gaoptout). -- **LinkedIn Analytics**. For more information, please visit - [LinkedIn Analytics' Privacy - Policy](https://www.linkedin.com/legal/privacy-policy). - To learn more about how to opt-out of LinkedIn's use of your - information, please click - [here](https://www.linkedin.com/help/linkedin/answer/62931?trk=microsites-frontend_legal_privacy-policy&lang=en). -- **Facebook Connect**. For more information, please visit Facebook's - [Data - Policy](https://www.facebook.com/policy.php?ref=pf). - You can object to the collection of your data by Facebook pixel, or - to the use of your data for the purpose of displaying Facebook ads - by contacting the following address while logged into your Facebook - account: https://www.facebook.com/settings?tab=ads. -- **Mixpanel**. For more information about Mixpanel, please visit - [Mixpanel's Privacy - Policy](https://mixpanel.com/legal/privacy-policy/). -- **Social Media Platforms**. Our Services may contain social media - buttons, such as Discord, Twitter, Instagram, TikTok, Youtube, and - Telegram, which might include widgets such as the "share this" - button or other interactive mini programs. These features may - collect your IP address and which page you are visiting on our - Services and may set a cookie to enable the feature to function - properly. Your interactions with these platforms are governed by the - privacy policy of the company providing it. - -C. **Information Collected from Other Sources** - -- **Third-Party Sources.** We may obtain information about you from - other sources, including through third-party services and - organizations. For example, if you access our Services through a - third-party application, such as an app store, a third-party login - service, or a social networking site, we may collect information - about you from that third-party application that you have made - available via your privacy settings. -- **Referrals, Sharing and Other Features**. Our Services may offer - various tools and functionalities that allow you to provide - information about your friends through our referral service; third - parties may also use these services to upload information about you. - Our referral services may also allow you to forward or share certain - content with a friend or colleague, such as an email inviting your - friend to use our Services. Please only share with us contact - information of people with whom you have a relationship (e.g., - relative, friend, neighbor, or co-worker). - -## HOW WE USE YOUR INFORMATION - -We use your information for a variety of business purposes, including to -provide our Services, for administrative purposes, and to market our -products and Services, as described below. - -A. **Provide Our Services** - -We use your information to fulfill our contract with you and provide you -with our Services and perform our contract with you, such as: - -- Managing your information and accounts; -- Providing access to certain areas, functionalities, and features of - our Services; -- Answering requests for customer or technical support; -- Communicating with you about your account, activities on our - Services, and policy changes; -- Processing information about your wallet to facilitate transfers via - the Services; -- Processing applications if you apply for a job, we post on our - Services; and -- Allowing you to register for events. - -B. **Administrative Purposes** - -We use your information for our legitimate interest, such as: - -- Pursuing our legitimate interests such as direct marketing, research - and development (including marketing research), network and - information security, and fraud prevention; -- Detecting security incidents, protecting against malicious, - deceptive, fraudulent or illegal activity, and prosecuting those - responsible for that activity; -- Measuring interest and engagement in our Services; -- Improving, upgrading or enhancing our Services; -- Developing new products and Services; -- Ensuring internal quality control and safety; -- Authenticating and verifying individual identities; -- Debugging to identify and repair errors with our Services; -- Auditing relating to interactions, transfers and other compliance - activities; -- Sharing information with third parties as needed to provide the - Services; -- Enforcing our agreements and policies; and -- Other uses as required to comply with our legal obligations. - -C. **Marketing and Advertising our Products and Services** - -We may use personal information to tailor and provide you with content -and advertisements. We may provide you with these materials as permitted -by applicable law. Some of the ways we may market to you include email -campaigns, custom audiences advertising, and "interest-based" or -"personalized advertising," including through cross-device tracking. - -If you have any questions about our marketing practices or if you would -like to opt out of the use of your personal information for marketing -purposes, you may contact us at any time as set forth in "[Contact -Us](#contact-us)" below. - -D. **With Your Consent** - -We may use personal information for other purposes that are clearly -disclosed to you at the time you provide personal information or with -your consent. - -E. **Other Purposes** - -We also use your information for other purposes as requested by you or -as permitted by applicable law. - -- **Automated Decision Making**. We may engage in automated decision - making, including profiling. Mysten Labs's processing of your - personal information will not result in a decision based solely on - automated processing that significantly affects you unless such a - decision is necessary as part of a contract we have with you, we - have your consent, or we are permitted by law to engage in such - automated decision making. If you have questions about our automated - decision making, you may contact us as set forth in "[Contact - Us](#contact-us)" below. -- **De-identified and Aggregated Information.** We may use personal - information and other information about you to create de-identified - and/or aggregated information, such as de-identified demographic - information, de-identified location information, information about - the device from which you access our Services, or other analyses we - create. - -## HOW WE DISCLOSE YOUR INFORMATION - -We disclose your information to third parties for a variety of business -purposes, including to provide our Services, to protect us or others, or -in the event of a major business transaction such as a merger, sale, or -asset transfer, as described below. - -A. **Disclosures to Provide our Services** - -The categories of third parties with whom we may share your information -are described below. - -- **Notice Regarding Use of Blockchain.** Transactions on the Services - will be conducted via Blockchain. Information about your transfers - will be provided to a Blockchain and may be accessible to third - parties due to the public nature of the Blockchain. Because entries - to a Blockchain are, by their nature, public, and because it may be - possible for someone to identify you through your pseudonymous, - public wallet address using external sources of information, any - transaction you enter onto the Blockchain could possibly be used to - identify you, or information about you. -- **Other Users of the Services and Parties You Transact With.** Some - of your personal information may be visible to other users of the - Services (e.g., information featured on generally accessible parts - of your profile; usernames of other Mysten Labs Services users). In - addition, to complete transfers via the Services, we will need to - share some of your personal information with the party that you are - transacting with. -- **Third Party Websites and Applications.** You may choose to share - personal information or interact with third-party websites and/or - third-party applications, including, but not limited to, third-party - electronic wallet extensions. Once your personal information has - been shared with a third-party website or a third-party application, - it will also be subject to such third party's privacy policy. We - encourage you to closely read each third-party website or - third-party application privacy policy before sharing your personal - information or otherwise interacting with them. Please note that we - do not control, and we are not responsible for the third-party - website's or the third-party application's processing of your - personal information. -- **Service Providers**. We may share your personal information with - our third-party service providers who use that information to help - us provide our Services. This includes service providers that - provide us with IT support, hosting, customer service, and related - services. -- **Business Partners**. We may share your personal information with - business partners to provide you with a product or service you have - requested. We may also share your personal information to business - partners with whom we jointly offer products or services. -- **Affiliates**. We may share your personal information with members - of our corporate family. -- **Other Users/Website Visitors**. As described above in "[Personal - Information We - Collect](#personal-information-we-collect)," our - Services allow you to share your profile and/or User Content with - other users or publicly, including to those who do not use our - Services. -- **Advertising Partners**. We may share your personal information - with third-party advertising partners. These third-party advertising - partners may set Technologies and other tracking tools on our - Services to collect information regarding your activities and your - device (e.g., your IP address, cookie identifiers, page(s) visited, - location, time of day). These advertising partners may use this - information (and similar information collected from other services) - for purposes of delivering personalized advertisements to you when - you visit digital properties within their networks. This practice is - commonly referred to as "interest-based advertising" or - "personalized advertising." -- **APIs/SDKs**. We may use third-party application program interfaces - ("**APIs**") and software development kits ("**SDKs**") as part of - the functionality of our Services. For more information about our - use of APIs and SDKs, please contact us as set forth in "[Contact - Us](#contact-us)" below. - -B. **Disclosures to Protect Us or Others** - -We may access, preserve, and disclose any information we store -associated with you to external parties if we, in good faith, believe -doing so is required or appropriate to: comply with law enforcement or -national security requests and legal process, such as a court order or -subpoena; protect your, our, or others' rights, property, or safety; -enforce our policies or contracts; collect amounts owed to us; or assist -with an investigation or prosecution of suspected or actual illegal -activity. - -C. **Disclosure in the Event of Merger, Sale, or Other Asset - Transfers** - -If we are involved in a merger, acquisition, financing due diligence, -reorganization, bankruptcy, receivership, purchase or sale of assets, or -transition of service to another provider, your information may be sold -or transferred as part of such a transaction, as permitted by law and/or -contract. - -## YOUR PRIVACY CHOICES AND RIGHTS - -**Your Privacy Choices**. The privacy choices you may have about your -personal information are determined by applicable law and are described -below. - -- **Email Communications**. If you receive an unwanted email from us, - you can use the unsubscribe link found at the bottom of the email to - opt out of receiving future emails. Note that you will continue to - receive transfer-related emails regarding Services you have - requested. We may also send you certain non-promotional - communications regarding us and our Services, and you will not be - able to opt out of those communications (e.g., communications - regarding our Services or updates to our Terms of Service or this - Privacy Policy). -- **Text Messages**. You may opt out of receiving text messages from - us by following the instructions in the text message you have - received from us or by otherwise contacting us. -- **Mobile Devices**. We may send you push notifications through our - mobile application. You may opt out from receiving these push - notifications by changing the settings on your mobile device. With - your consent, we may also collect precise location-based information - via our mobile application. You may opt out of this collection by - changing the settings on your mobile device. -- **"Do Not Track**.**"** Do Not Track ("**DNT**") is a privacy - preference that users can set in certain web browsers. Please note - that we do not respond to or honor DNT signals or similar mechanisms - transmitted by web browsers. -- **Cookies and Interest-Based Advertising**. You may stop or restrict - the placement of Technologies on your device or remove them by - adjusting your preferences as your browser or device permits. - However, if you adjust your preferences, our Services may not work - properly. Please note that cookie-based opt-outs are not effective - on mobile applications. However, you may opt-out of personalized - advertisements on some mobile applications by following the - instructions for - [Android](https://support.google.com/googleplay/android-developer/answer/6048248?hl=en), - [iOS](https://support.apple.com/en-us/HT202074) and - [others](https://www.networkadvertising.org/mobile-choice/). - -> The online advertising industry also provides websites from which you -> may opt out of receiving targeted ads from data partners and other -> advertising partners that participate in self-regulatory programs. You -> can access these and learn more about targeted advertising and -> consumer choice and privacy by visiting the [Network Advertising -> Initiative](http://www.networkadvertising.org/managing/opt_out.asp), -> [the Digital Advertising -> Alliance](http://www.aboutads.info/choices/), [the -> European Digital Advertising -> Alliance](https://www.youronlinechoices.eu/), and [the -> Digital Advertising Alliance of -> Canada](https://youradchoices.ca/choices/). -> -> Please note you must separately opt out in each browser and on each -> device. - -**Your Privacy Rights**. In accordance with applicable law, you may have -the right to: - -- **Access Personal Information about you, including: (i) confirming whether we are processing your - personal information; (ii) obtaining access to or a copy of your personal information; or (iii) - receiving an electronic copy of personal information that you have provided to us, or asking us to - send that information to another company (aka the right of data portability);** - -- **Request Correction** of your personal information where it is - inaccurate or incomplete. In some cases, we may provide self-service - tools that enable you to update your personal information; - -## Request Deletion of your personal information; - -- **Request Restriction of or Object** to our processing of your - personal information, including where the processing of your - personal information is based on our legitimate interest or for - direct marketing purposes; and - -**Withdraw Your Consent to our processing of your personal information. Please note that your -withdrawal will only take effect for future processing and will not affect the lawfulness of -processing before the withdrawal.** - -If you would like to exercise any of these rights, please contact us as -set forth in "[Contact Us](#contact-us)" below. We will -process such requests in accordance with applicable laws. - -## SECURITY OF YOUR INFORMATION - -We take steps designed to ensure that your information is treated -securely and in accordance with this Privacy Policy. Unfortunately, no -system is 100% secure, and we cannot ensure or warrant the security of -any information you provide to us. To the fullest extent permitted by -applicable law, we do not accept liability for unauthorized disclosure. - -By using our Services or providing personal information to us, you agree -that we may communicate with you electronically regarding security, -privacy, and administrative issues relating to your use of our Services. -If we learn of a security system's breach, we may attempt to notify you -electronically by posting a notice on our Services, by mail or by -sending an email to you. - -## INTERNATIONAL DATA TRANSFERS - -All information processed by us may be transferred, processed, and -stored anywhere in the world, including, but not limited to, the United -States or other countries, which may have data protection laws that are -different from the laws where you live. We endeavor to safeguard your -information consistent with the requirements of applicable laws. - -If we transfer personal information which originates in the European -Economic Area, Switzerland, and/or the United Kingdom to a country that -has not been found to provide an adequate level of protection under -applicable data protection laws, one of the safeguards we may use to -support such transfer is the EU Standard Contractual Clauses. - -## RETENTION OF PERSONAL INFORMATION - -We store the personal information we collect as described in this -Privacy Policy for as long as you use our Services or as necessary to -fulfill the purpose(s) for which it was collected, provide our Services, -resolve disputes, establish legal defenses, conduct audits, pursue -legitimate business purposes, enforce our agreements, and comply with -applicable laws. - -To determine the appropriate retention period for personal information, -we may consider applicable legal requirements, the amount, nature, and -sensitivity of the personal information, certain risk factors, the -purposes for which we process your personal information, and whether we -can achieve those purposes through other means. - -## SUPPLEMENTAL NOTICE FOR CALIFORNIA RESIDENTS - -This Supplemental Notice for California Residents only applies to our -processing of personal information that is subject to the California -Consumer Privacy Act of 2018 ("**CCPA**"). Mysten Labs does not believe -it is subject to the CCPA. That said, Mysten Labs provides this -supplemental notice for purposes of transparency. The CCPA provides -California residents with the right to know what categories of personal -information Mysten Labs has collected about them and whether Mysten Labs -disclosed that personal information for a business purpose (e.g., to a -service provider) in the preceding twelve months. California residents -can find this information below: - - - ---- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Category of Personal Information Collected by Mysten -LabsCategories of Third Parties Personal Information is -Disclosed to for a Business Purpose

Identifiers

-

A real name, postal address, unique personal identifier, online -identifier, Internet Protocol address, email address, account name, or -other similar identifiers.

    -
  • Service providers

  • -
  • Third-party websites or applications

  • -
  • Blockchain networks

  • -
  • Other users or third parties you interact with

  • -
  • Advertising partners

  • -
  • Data analytics providers

  • -
  • Other users/public (alias only)

  • -

Personal information categories listed in Cal. Civ. Code -§ 1798.80(e)

-

A name, signature, Social Security number, address, telephone number, -passport number, driver’s license or state identification card number, -insurance policy number, education, employment, employment history, bank -account number, credit card number, debit card number, or any other -financial information. Personal Information does not include publicly -available information that is lawfully made available to the general -public from federal, state, or local government records. Note: Some -personal information included in this category may overlap with other -categories.

    -
  • Service providers

  • -
  • Third-party websites or applications (e.g., wallet
    -providers; third-party identity verification services)

  • -
  • Blockchain networks

  • -
  • Data analytics providers

  • -
  • Other users or third parties you interact with

  • -

Protected classification characteristics under California -or federal law

-

Age (40 years or older), race, color, ancestry, national origin, -citizenship, religion or creed, marital status, medical condition, -physical or mental disability, sex (including gender, gender identity, -gender expression, pregnancy or childbirth and related medical -conditions), sexual orientation, veteran or military status, genetic -information (including familial genetic information).

    -
  • Service providers (recruitment context).

  • -

Commercial information

-

Records of personal property, products or services purchased, -obtained, or considered, or other purchasing or consuming histories or -tendencies.

    -
  • Service providers

  • -
  • Blockchain networks

  • -
  • Data analytics providers

  • -
  • Other users or third parties you interact with

  • -

Internet or other electronic network -activity

-

Browsing history, search history, information on a consumer’s -interaction with an internet website, application, or -advertisement.

    -
  • Service providers

  • -
  • Blockchain networks

  • -
  • Data analytics providers

  • -
  • Other users or third parties you interact with

  • -
  • Advertising partners

  • -

Professional or employment-related -information

-

Current or past job history or performance evaluations.

    -
  • Service providers

  • -

Inferences drawn from other personal information to -create a profile about a consumer

-

Profile reflecting a consumer’s preferences, characteristics, -psychological trends, predispositions, behavior, attitudes, -intelligence, abilities, and aptitudes.

    -
  • Service providers

  • -
  • Data analytics providers

  • -
  • Advertising partners

  • -
- -The categories of sources from which we collect personal information and -our business and commercial purposes for using personal information are -set forth in "[Personal Information we -Collect](#personal-information-we-collect)" and "[How We -Use of Your Information](#how-we-use-your-information)" above, -respectively. - -**"Sales" of Personal Information under the CCPA.** For purposes of the -CCPA, Mysten Labs does not "sell" personal information, nor do we have -actual knowledge of any "sale" of personal information of minors under -16 years of age. - -**Additional Privacy Rights for California Residents** - -**Non-Discrimination.** California residents have the right not to -receive discriminatory treatment by us for the exercise of their rights -conferred by the CCPA. - -**Authorized Agent.** Only you, or someone legally authorized to act on -your behalf, may make a verifiable consumer request related to your -personal information. To designate an authorized agent, please contact -us as set forth in "[Contact Us](#contact-us)" below and -provide written authorization signed by you and your designated agent. - -**Verification**. To protect your privacy, we will take the following -steps to verify your identity before fulfilling your request. When you -make a request, we will ask you to provide sufficient information that -allows us to reasonably verify you are the person about whom we -collected personal information or an authorized representative, which -may include confirming the email address associated with any personal -information we have about you. If you are a California resident and -would like to exercise any of your rights under the CCPA, please contact -us as set forth in "[Contact Us](#contact-us)" below. We -will process such requests in accordance with applicable laws. - -**Refer-a-Friend and Similar In -"[How We Use Your Personal -Information](#how-we-use-your-information) we may offer referral -programs or other incentivized data collection programs. For example, we -may offer incentives to you such as discounts or promotional items or -credit in connection with these programs, wherein you provide your -personal information in exchange for a reward, or provide personal -information regarding your friends or colleagues (such as their email -address) and receive rewards when they sign up to use our Services. (The -referred party may also receive rewards for signing up via your -referral.) These programs are entirely voluntary and allow us to grow -our business and provide additional benefits to you. The value of your -data to us depends on how you ultimately use our Services, whereas the -value of the referred party's data to us depends on whether the referred -party ultimately becomes a user and uses our Services. Said value will -be reflected in the incentive offered in connection with each program. - -**Accessibility**. This Privacy Policy uses industry-standard -technologies and was developed in line with the World Wide Web -Consortium's Web Content Accessibility Guidelines, version 2.1. If you -wish to print this policy, please do so from your web browser or by -saving the page as a PDF. - -**California Shine the Light**. The California "Shine the Light" law -permits users who are California residents to request and obtain from us -once a year, free of charge, a list of the third parties to whom we have -disclosed their personal information (if any) for their direct marketing -purposes in the prior calendar year, as well as the type of personal -information disclosed to those parties. - -**Right for minors to remove posted content.** Where required by law, -California residents under the age of 18 may request to have their -posted content or information removed from the publicly viewable -portions of the Services by contacting us directly as set forth in -"[Contact Us](#contact-us)" below. - -## SUPPLEMENTAL NOTICE FOR NEVADA RESIDENTS - -If you are a resident of Nevada, you have the right to opt-out of the -sale of certain personal information to third parties who intend to -license or sell that personal information. You can exercise this right -by contacting us as set forth in "[Contact -Us](#contact-us)" below with the subject line "Nevada Do -Not Sell Request" and providing us with your name and the email address -associated with your account. Please note that we do not currently sell -your personal information as sales are defined in Nevada Revised -Statutes Chapter 603A. - -## CHILDREN'S INFORMATION - -The Services are not directed to children under 13 (or other age as -required by local law), and we do not knowingly collect personal -information from children. If you learn that your child has provided us -with personal information without your consent, you may contact us as -set forth in "[Contact Us](#contact-us)" below. If we -learn that we have collected a child's personal information in violation -of applicable law, we will promptly take steps to delete such -information. - -## THIRD-PARTY WEBSITES/APPLICATIONS - -The Services may contain links to other websites/applications (such as -GitHub) and other websites/applications may reference or link to our -Services. These third-party services are not controlled by us. We -encourage our users to read the privacy policies of each website and -application with which they interact. We do not endorse, screen or -approve, and are not responsible for, the privacy practices or content -of such other websites or applications. Providing personal information -to third-party websites or applications is at your own risk. - -## SUPERVISORY AUTHORITY - -If you are located in the European Economic Area, Switzerland, the -United Kingdom, or Brazil, you have the right to lodge a complaint with -a supervisory authority if you believe our processing of your personal -information violates applicable law. - -## CONTACT US - -If you have any questions about our privacy practices or this Privacy -Policy, or to exercise your rights as detailed in this Privacy Policy, -please contact us at: - -Mysten Labs, Inc. \ -Attn: Privacy Group \ -379 University Ave, #200 \ -Palo Alto, CA 94301 \ -[privacy@mystenlabs.com](mailto:privacy@mystenlabs.com) \ -+1 (408) 384-8237 +# Privacy policy diff --git a/docs/walrus-sites/tos.md b/docs/walrus-sites/tos.md index 9a3b8a47..e40a7670 100644 --- a/docs/walrus-sites/tos.md +++ b/docs/walrus-sites/tos.md @@ -16,9 +16,9 @@ RESOLUTION” BELOW FOR DETAILS REGARDING ARBITRATION. ## I. Privacy Policy -Please review our Privacy Policy, which also governs your use of the Services, for information on -how we collect, use and share your information. By using the Services you agree to be bound by our -Privacy Policy. +Please review our [Privacy Policy](../legal/privacy.md), which also governs your use of the +Services, for information on how we collect, use and share your information. By using the Services +you agree to be bound by our Privacy Policy. ## II. Eligibility From 6758c522a7fb9706d067b0fee09849718e5f264c Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 17 Oct 2024 08:20:30 +0200 Subject: [PATCH 29/50] fix port of one publisher --- docs/usage/web-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/web-api.md b/docs/usage/web-api.md index b45b4a4e..5a045dfd 100644 --- a/docs/usage/web-api.md +++ b/docs/usage/web-api.md @@ -84,7 +84,7 @@ Reported but currently not available: - `https://sui-walrus-testnet.bwarelabs.com/publisher` - `https://walrus-testnet-publisher.stakin-nodes.com` - `https://testnet-publisher-walrus.kiliglab.io` -- `https://walrus-testnet.blockscope.net:444` +- `https://walrus-testnet.blockscope.net:11444` - `https://walrus-publish-testnet.chainode.tech:9003` - `https://walrus-testnet-publisher.starduststaking.com:11445` - `http://walrus-publisher-testnet.overclock.run:9001` From 70b2a120b6f59a3e0ffabcba990474ca88b0dd52 Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 17 Oct 2024 08:27:34 +0200 Subject: [PATCH 30/50] fix editorconfig-checker action --- .github/workflows/lint.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 1411cebe..eb6b71c8 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -26,7 +26,7 @@ jobs: name: Check editorconfig steps: - uses: actions/checkout@v4 - - run: pip install editorconfig-checker=="2.7.3" --break-system-packages + - run: pip install editorconfig-checker=="3.0.3" - run: ec markdownlint: From 1447260d812e0e3b0e8b963a5451808c400781c6 Mon Sep 17 00:00:00 2001 From: giac-mysten <124184614+giac-mysten@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:49:30 +0800 Subject: [PATCH 31/50] docs: add Walrus Sites testnet docs Signed-off-by: giac-mysten Co-authored-by: Alexandros Tzimas Co-authored-by: Markus Legner --- .markdownlint-cli2.yaml | 1 - docs/SUMMARY.md | 11 +- docs/assets/walrus-sites-hash-mismatch.png | Bin 0 -> 140910 bytes docs/blog/04_testnet_update.md | 11 ++ docs/walrus-sites/advanced.md | 5 + docs/walrus-sites/authentication.md | 44 +++++++ .../{tutorial-config.md => builder-config.md} | 6 +- docs/walrus-sites/commands.md | 57 ++++++++ docs/walrus-sites/intro.md | 24 +++- docs/walrus-sites/linking.md | 18 +-- docs/walrus-sites/overview.md | 49 ++++--- docs/walrus-sites/portal.md | 41 +++--- docs/walrus-sites/redirects.md | 2 +- docs/walrus-sites/restrictions.md | 28 ++-- docs/walrus-sites/routing.md | 123 ++++++++++++++++++ docs/walrus-sites/tutorial-install.md | 2 +- docs/walrus-sites/tutorial-migration.md | 25 ++++ docs/walrus-sites/tutorial-publish.md | 17 +-- docs/walrus-sites/tutorial-suins.md | 11 +- 19 files changed, 384 insertions(+), 91 deletions(-) create mode 100644 docs/assets/walrus-sites-hash-mismatch.png create mode 100644 docs/walrus-sites/advanced.md create mode 100644 docs/walrus-sites/authentication.md rename docs/walrus-sites/{tutorial-config.md => builder-config.md} (93%) create mode 100644 docs/walrus-sites/commands.md create mode 100644 docs/walrus-sites/routing.md create mode 100644 docs/walrus-sites/tutorial-migration.md diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml index 8fc380fc..5dda1a20 100644 --- a/.markdownlint-cli2.yaml +++ b/.markdownlint-cli2.yaml @@ -36,7 +36,6 @@ config: - Mainnet - Merkle - Mysten Labs - - Portal - Python - Rust - Sui diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 8cb31bc0..6d5ef601 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -53,11 +53,16 @@ - [Installing the site builder](./walrus-sites/tutorial-install.md) - [Publishing a Walrus Site](./walrus-sites/tutorial-publish.md) - [Bonus: Set a SuiNS name](./walrus-sites/tutorial-suins.md) - - [Advanced configuration](./walrus-sites/tutorial-config.md) -- [Technical overview](./walrus-sites/overview.md) + - [Migrating your site from Devnet](./walrus-sites/tutorial-migration.md) +- [Advanced functionality](./walrus-sites/advanced.md) + - [Site builder commands](./walrus-sites/commands.md) + - [Advanced site-builder configuration](./walrus-sites/builder-config.md) + - [Specifying headers and routing](./walrus-sites/routing.md) - [Linking from and to Walrus Sites](./walrus-sites/linking.md) - [Redirecting objects to Walrus Sites](./walrus-sites/redirects.md) - - [The Walrus Sites Portal](./walrus-sites/portal.md) +- [Technical overview](./walrus-sites/overview.md) + - [The Walrus Sites portal](./walrus-sites/portal.md) + - [Site data authentication](./walrus-sites/authentication.md) - [Known restrictions](./walrus-sites/restrictions.md) - [Terms of service](./walrus-sites/tos.md) diff --git a/docs/assets/walrus-sites-hash-mismatch.png b/docs/assets/walrus-sites-hash-mismatch.png new file mode 100644 index 0000000000000000000000000000000000000000..34c7a62467dba0713690374d9ab79ef335ae34b6 GIT binary patch literal 140910 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

FCJh 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

t;iE-EH>eJm5C#)V39EF#CY+y?n5mc)INH#Nl`b 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 literal 0 HcmV?d00001 diff --git a/docs/blog/04_testnet_update.md b/docs/blog/04_testnet_update.md index 77559227..66d653eb 100644 --- a/docs/blog/04_testnet_update.md +++ b/docs/blog/04_testnet_update.md @@ -89,3 +89,14 @@ to follow Sui event streams. As part of the Testnet release of Walrus the documentation and Move Smart contracts have been updated, and can be found at the [Walrus-docs repository](https://github.com/MystenLabs/walrus-docs) and as a [Walrus Docs Site](https://docs.walrus.site/). + +## New Walrus Sites features + +With the move to Walrus Testnet, Walrus Sites have also been updated! The new features in this +update greatly increase the flexibility, speed, and security of Walrus Sites. Developers can now +specify client-side routing rules, and add custom HTTP headers to the portals' responses for their +site, expanding the possibilities for what Walrus Sites can do. + +[Migrate now](../walrus-sites/tutorial-migration.md) to take advantage of these new features! +The old Walrus Sites, based on Walrus Devnet, will still be available for a short time. However, +Devnet will be wiped soon, so it is recommended to migrate as soon as possible. diff --git a/docs/walrus-sites/advanced.md b/docs/walrus-sites/advanced.md new file mode 100644 index 00000000..916c410e --- /dev/null +++ b/docs/walrus-sites/advanced.md @@ -0,0 +1,5 @@ +# Advanced functionality + +Keep reading to learn about the advanced features of Walrus Sites, including configuring the site +builder, specifying headers and routing for site resources, and *redirecting Sui objects to Walrus +Sites* to create per-object websites! diff --git a/docs/walrus-sites/authentication.md b/docs/walrus-sites/authentication.md new file mode 100644 index 00000000..7b3516b6 --- /dev/null +++ b/docs/walrus-sites/authentication.md @@ -0,0 +1,44 @@ +# Site data authentication + +Walrus Sites offer a simple mechanism to authenticate the data that is served from the Walrus +storage on the client side. Thus, Walrus Sites can guarantee (with various degrees of confidence, +depending on the setup) that the site data is authentic and has not been tampered with by a +malicious aggregator. + +## Authentication mechanism + +The Walrus Sites resource object on Sui stores, alongside the resource information, a SHA-256 hash +of the resource's content. + +When the client requests a resource, the portal will check that the hash of the data received from +the Walrus storage (and the Walrus aggregator in particular) matches the hash stored on Sui. + +If the hashes do not match, the portal will return the following warning page: + +![Hash mismatch warning page](../assets/walrus-sites-hash-mismatch.png) + +## Authentication Guarantees + +Depending on the type of deployment, this technique gives increasing levels of confidence that the +site data is authentic. We list them here in increasing order of assurance. + +### Remote server-side portal deployment + +In this case, the user must fully trust the portal provider to authenticate the data. With a trusted +portal, the authentication mechanism guarantees that the aggregator or cache (from which the blob +has been fetched) did not tamper with the contents of the blob. + +### Remote service-worker portal deployment + +Here, the portal provider is only trusted to provide the correct service worker code to the user. +The user's browser will then perform the fetching and authentication. The guarantees are therefore +the same as with the remote server-site portal, with the addition that the user can inspect the code +returned by the portal provider and verify its integrity (e.g., by comparing it the hash of the +service worker code to one that is known to be correct). + +### Local portal deployment + +Finally, a user can clone the Walrus Sites repository and deploy a portal locally, browsing Walrus +Sites through `localhost`. In this case, the user has full control over the portal code and can +verify its operation. Therefore, they can fully authenticate that the data served by Walrus and +Walrus Sites is what the original developer intended. diff --git a/docs/walrus-sites/tutorial-config.md b/docs/walrus-sites/builder-config.md similarity index 93% rename from docs/walrus-sites/tutorial-config.md rename to docs/walrus-sites/builder-config.md index f653d846..c090ddd0 100644 --- a/docs/walrus-sites/tutorial-config.md +++ b/docs/walrus-sites/builder-config.md @@ -17,16 +17,16 @@ the `--config` flag. For your first run, it should be sufficient to call the `si If, for any reason, you didn't add `walrus` to `$PATH`, make sure to configure a pointer to the binary, see below. -## Advanced configuration +## Additional options If you want to have more control over the behavior of the site builder, you can customize the following variables in the config file: - `package`: the object ID of the Walrus Sites package on Sui. This must always be specified in the config, and is already appropriately configured in `assets/example-config.yaml`. -- `portal`: the name of the Portal through which the site will be viewed; this only affects the +- `portal`: the name of the portal through which the site will be viewed; this only affects the output of the CLI, and nothing else (default: `walrus.site`). - All Walrus Sites are accessible through any Portal independent of this setting. + All Walrus Sites are accessible through any portal independent of this setting. - `general`: these are general options that can be configured both through the CLI and the config: - `rpc_url`: The URL of the Sui RPC node to use. If not set, the `site-builder` will infer it from the wallet. diff --git a/docs/walrus-sites/commands.md b/docs/walrus-sites/commands.md new file mode 100644 index 00000000..eb72267f --- /dev/null +++ b/docs/walrus-sites/commands.md @@ -0,0 +1,57 @@ +# Site builder commands + +We now describe in more detail the commands available through the site builder. + +```admonish tip +In general, the `--help` flag is your friend, you can add it to get further details for the whole +CLI (`./target/release/site-builder --help`) or individual commands (e.g., +`./target/release/site-builder update --help`). +``` + +## `publish` + +The `publish` command, as described in the [previous section](./tutorial-publish.md), publishes a +new site on Sui. The command takes a directory as input and creates a new Walrus Site from the +resources contained within. + +The `--epochs` flag allows you to specify for how long the site data will be stored on Walrus. By +default, this is set to `1` epoch. + +```admonish danger title="Epoch duration on Walrus Testnet" +On Walrus Testnet, one epoch will last **1 day**. Therefore, consider storing your site for a large +number of epochs if you want to make it available for the following months! The maximum duration is +set to 200 epochs. +``` + +If you are just uploading raw files without an `index.html`, you may want to use the +`--list-directory` flag, which will automatically create an index page to browse the files. See for +example . + +The `publish` command will also respect the instructions in the `ws-resources.json` configuration +file. To know more, see the section on [specifying headers and routing](./routing.md). + +## `update` + +This command is the equivalent of `publish`, but for updating an existing site. It takes the same +arguments, with the addition of the Sui object ID of the site to update. + +```admonish note +The wallet you are using to call `update` must be the *owner* of the Walrus Site object to be able +to update it. +``` + +## `convert` + +The `convert` command converts an object ID in hex format to the equivalent Base36 format. This +command is useful if you have the Sui object ID of a Walrus Site, and want to know the subdomain +where you can browse it. + +## `site-map` + +The `sitemap` command shows the resources that compose the Walrus Site at the given object ID. + +## `list-directory` + +With `list-directory`, you can obtain the `index.html` file that would be generated by running +`publish` or `update` with the `--list-directory` flag. This is useful to see how the index page +would look like before publishing it—and possibly editing to make it look nicer! diff --git a/docs/walrus-sites/intro.md b/docs/walrus-sites/intro.md index 051a6c3f..51a511a7 100644 --- a/docs/walrus-sites/intro.md +++ b/docs/walrus-sites/intro.md @@ -6,6 +6,20 @@ can build and deploy a Walrus Site and make it accessible to the world! Funnily, is itself available as a Walrus Site at (if you aren't there already). +```admonish tip title="Make sure you update!" +With the move to Walrus Testnet, Walrus Sites are also being updated! They now use Walrus Testnet as +the backing store, and they have been improved with new awesome features. [Migrate your +site](./tutorial-migration.md) today to take advantage of these new features! +``` + +```admonish danger title="Walrus Sites Devnet being discontinued" +Since the Walrus Devnet will be shut down soon, all the Walrus Sites stored on it will be wiped. To +minimize the downtime, the Devnet Walrus Sites will be available after the Testnet upgrade for two +weeks (until **2024-10-31**), to ensure that everyone has enough time to update. + +Make sure to reupload your sites to the Walrus Tevnet before the shutdown to avoid downtime! +``` + At a high level, here are some of the most exciting features: - Publishing a site does not require managing servers or complex configurations; just provide the @@ -30,10 +44,10 @@ NFT collection on Sui that has a frontend dApp to mint the NFTs hosted on Walrus which *each NFT* has a *specific, personalized Walrus Site*. You can check out the mint page at . This site is served to your -browser through the Walrus Site *Portal* . While the Portal's operation is -explained in a [later section](./portal.md), consider for now that there can be many Portals (hosted +browser through the Walrus Site *portal* . While the portal's operation is +explained in a [later section](./portal.md), consider for now that there can be many portals (hosted by whoever wants to have their own, and even on `localhost`). Further, the only function of the -Portal is to provide the browser with some code (specifically, a service worker) that allows it to +portal is to provide the browser with some code (specifically, a service worker) that allows it to fetch the Walrus Site from Sui and Walrus. If you have a Sui wallet with some Testnet SUI, you can try and "mint a new Flatlander" from the @@ -51,8 +65,8 @@ which is In summary: -- Walrus Sites are served through a Portal; in this case, `https://walrus.site`. There can be many - Portals, and anyone can host one. +- Walrus Sites are served through a portal; in this case, `https://walrus.site`. There can be many + portals, and anyone can host one. - The subdomain on the URL points to a specific object on Sui that allows the browser to fetch and render the site resources. This pointer can be - a SuiNS name, such as `flatland` in `https://flatland.walrus.site`, or diff --git a/docs/walrus-sites/linking.md b/docs/walrus-sites/linking.md index 67c2b2e1..58dc947d 100644 --- a/docs/walrus-sites/linking.md +++ b/docs/walrus-sites/linking.md @@ -17,20 +17,20 @@ as usual. Here is the part that is a bit different. Assume there is some image that you can browse at `https://gallery.walrus.site/walrus_arctic.webp`, and you want to link it from your own Walrus Site. -Recall that, however, `https://walrus.site` is just one of the possibly many Portals. I.e., the same -resource is browsable from a local Portal (`http://gallery.localhost:8080/walrus_arctic.webp`), or -from any other Portal (e.g., `https://gallery.myotherportal.com/walrus_arctic.webp`). Therefore, how -can you link the resource in a *Portal independent way*? This is important for interoperability, -availability, and respecting the user's choice of Portal. +Recall that, however, `https://walrus.site` is just one of the possibly many portals. I.e., the same +resource is browsable from a local portal (`http://gallery.localhost:8080/walrus_arctic.webp`), or +from any other portal (e.g., `https://gallery.myotherportal.com/walrus_arctic.webp`). Therefore, how +can you link the resource in a *portal-independent way*? This is important for interoperability, +availability, and respecting the user's choice of portal. ### The solution: Walrus Sites links -We solve this problem by having the Portals interpret special links that are normally invalid on -the web and redirect to the corresponding Walrus Sites resource in the Portal itself. +We solve this problem by having the portals interpret special links that are normally invalid on +the web and redirect to the corresponding Walrus Sites resource in the portal itself. Consider the example above, where the resource `/walrus_arctic.webp` is browsed from the Walrus Site with SuiNS name `gallery`, which points to the object ID `abcd123…` (in Base36 encoding). Then, -the Portal-independent link is: `https://gallery.suiobj/walrus_arctic.webp`. To fix the object ID +the portal-independent link is: `https://gallery.suiobj/walrus_arctic.webp`. To fix the object ID instead of the SuiNS name, you can use `https://abcd123….suiobj/walrus_arctic.webp`. Another possibility is to directly point to the Walrus *blob ID* of the resource, and have the @@ -38,5 +38,5 @@ browser "sniff" the content type. This works for images, for example, but not fo stylesheets. For example to point to the blob ID (e.g., containing an image) `qwer5678…`, use the URL `https://blobid.walrus/qwer5678…`. -With such a link, the Portal will extract the blob ID and redirect the request to the aggregator it +With such a link, the portal will extract the blob ID and redirect the request to the aggregator it is using to fetch blobs. diff --git a/docs/walrus-sites/overview.md b/docs/walrus-sites/overview.md index b1aecf07..375af310 100644 --- a/docs/walrus-sites/overview.md +++ b/docs/walrus-sites/overview.md @@ -25,21 +25,18 @@ fields](https://docs.sui.io/concepts/dynamic-fields/) of type `Resource`: ``` move struct Resource has store, drop { path: String, - content_type: String, - content_encoding: String, // The walrus blob id containing the bytes for this resource blob_id: u256, + ⋮ } ``` -Each resource contains +Importantly, each resource contains: - the `path` of the resource, for example `/index.html` (all the paths are always represented as starting from root `/`); -- the `content_type` of the resource, for example `text/html`; -- the `content_encoding` of the resource (at the moment the only available value is `plaintext`); - and -- the `blob_id`, which is the Walrus blob ID where the resource can be found. +- the `blob_id`, which is the Walrus blob ID where the resource can be found; and +- additional fields, that will be explained later in the documentation. These `Resource` dynamic fields are keyed with a struct of type `ResourcePath` @@ -70,20 +67,20 @@ approaches are possible: - Using a custom application on the client that has both a web browser and knowledge of how Walrus Sites work, and can locally perform this resolution. - A hybrid approach based on service workers, where a service worker that is able to perform the - resolution is installed in the browser from a Portal. + resolution is installed in the browser from a portal. All of these approaches are viable (the first has been used for similar applications in IPFS -gateways, for example), and have trade-offs. As an initial step, we have chosen to use the -service-worker based approach, as it is light weight and ensures that the Portal does not have to -process all the traffic from clients. This approach is described in the following. +gateways, for example), and have trade-offs. + +Currently, we provide the server-side and the service-worker based approaches. ### Browsing and domain isolation -We must ensure that, when browsing multiple sites through a Portal, for example the one hosted at +We must ensure that, when browsing multiple sites through a portal, for example the one hosted at , these sites are isolated. Isolation is necessary for security, and to ensure that the wallet connection in the browser works as expected. -To do so, we give each Walrus Site a specific *subdomain* of the Portal's domain. For example, the +To do so, we give each Walrus Site a specific *subdomain* of the portal's domain. For example, the Flatland mint dApp is hosted at , where the subdomain `flatland` is uniquely associated to the object ID of the Walrus Site through SuiNS. @@ -96,7 +93,21 @@ Base36 was chosen for two reasons, forced by the subdomain standards: 1. A subdomain can have at most 63 characters, while a Hex-encoded Sui object ID requires 64. 1. A subdomain is case *insensitive*, ruling out other popular encodings, e.g., Base64 or Base58. -## The end-to-end resolution of a Walrus Site +## Walrus Site portals + +### Portal types + +As mentioned before, we provide two types of portals to browse Walrus Sites: + +- The server-side portal, which is a server that resolves a Walrus Site, returning it to the + browser. The server-side portal is hosted at . +- The service-worker portal, which is a service worker that is installed in the browser and resolves + a Walrus Site. The service-worker portal is hosted at . + +We now describe the resolution process of a Walrus Site in the browser using the service-worker +portal as an example. + +### The end-to-end resolution of a Walrus Site We now show in greater detail how a Walrus Site is rendered in a client's browser with the service worker approach. The steps below all reference the following figure: @@ -107,18 +118,18 @@ worker approach. The steps below all reference the following figure: [`site-builder`](#the-site-builder), or making use of a publisher. Assume the developer uses the SuiNS name `dapp.sui` to point to the object ID of the created Walrus Site. - **Browsing starts** (step 1): A client browses `dapp.walrus.site/index.html` in their browser. -- **Service worker installation** (steps 2-6): The browser connects to the Portal hosted at - `walrus.site`, which responds with a page that installs the service worker for - `dapp.walrus.site`. The page is refreshed to activate the service worker. +- **Service worker installation** (steps 2-6): The browser connects to the portal hosted at + `walrus.site`, which responds with a page that installs the service worker for `dapp.walrus.site`. + The page is refreshed to activate the service worker. - **Site resolution** (steps 7-10): The service worker, which is now installed, interprets its *origin* `dapp.walrus.site`, and makes a SuiNS resolution for `dapp.sui`, obtaining the related object ID. Using the object ID, it then fetches the dynamic fields of the object (also checking [redirects](./portal.md)). From the dynamic fields, it selects the one for `/index.html`, and - extracts its Walrus blob ID and content type. + extracts its Walrus blob ID and headers (see the [advanced section on headers](./routing.md). - **Blob fetch** (steps 11-14): Given the blob ID, the service worker queries a Walrus aggregator for the blob. - **Returning the response** (steps 15-16): Now that the service worker has the bytes for - `/index.html`, and its `content_type`, it can craft a response that is then rendered by the + `/index.html`, and its headers, it can craft a response that is then rendered by the browser. These steps are executed for all resources the browser may query thereafter (for example, if diff --git a/docs/walrus-sites/portal.md b/docs/walrus-sites/portal.md index c24a66bd..50c3fcaa 100644 --- a/docs/walrus-sites/portal.md +++ b/docs/walrus-sites/portal.md @@ -1,39 +1,50 @@ -# The Walrus Sites Portal +# The Walrus Sites portal -We use the term "Portal" to indicate any technology that is used to access an browse Walrus Sites. +We use the term "portal" to indicate any technology that is used to access an browse Walrus Sites. As mentioned in the [overview](./overview.md#the-site-rendering-path), we foresee three kinds of -Portals: +portals: -1. server-side Portals; +1. server-side portals; 1. custom local apps; and -1. service-worker based Portals in the browser. +1. service-worker based portals in the browser. -Currently, only the service-worker based Portal is available. +Currently, the server-side and service-workers portals are available at and +, respectively. -## Running the Portal locally +## Running the portal locally -You can run a service-worker Portal locally if you want to browse Walrus Sites without accessing -external Portals or for development purposes. +You can run a service-worker portal locally if you want to browse Walrus Sites without accessing +external portals or for development purposes. This requires having the [`pnpm`](https://pnpm.io/) tool installed. To start, clone the `walrus-sites` repo and enter the `portal` directory. Here, run ``` sh +cd portal pnpm install +# Build the portal you want to use, or both +pnpm build:worker +pnpm build:server ``` -to install the dependencies, and +to install the dependencies, and then either one of the following commands: ``` sh -pnpm serve +# Serve the server-side portal +pnpm serve:dev:server + +# Serve the service-worker portal +pnpm serve:dev:worker ``` -to serve the Portal. Typically, you will find it served at `localhost:8080` (but check the output of -the serve command). +to serve one of the portals. Typically, you will find it served at `localhost:8080` (but check the +output of the serve command). + +For the production versions, use the `prod` commands: `serve:prod:server` and `serve:prod:worker`. -## Configuring the Portal +## Configuring the portal -The most important configuration parameters for the Portal are in `constants.ts`: +The most important configuration parameters for the portal are in `portal/common/lib/constants.ts`: - `NETWORK`: The Sui network to be used for fetching the Walrus Sites objects. Currently, we use Sui `testnet`. diff --git a/docs/walrus-sites/redirects.md b/docs/walrus-sites/redirects.md index ac38a0a0..871d4789 100644 --- a/docs/walrus-sites/redirects.md +++ b/docs/walrus-sites/redirects.md @@ -14,7 +14,7 @@ show there, each minted NFT has its own Walrus Site, which can be personalized b The solution is simple: We add a "redirect" in the NFT's [`Display`](https://docs.sui.io/standards/display#sui-utility-objects) property. Each time an NFT's -object ID is browsed through a Portal, the Portal will check the `Display` of the NFT and, if it +object ID is browsed through a portal, the portal will check the `Display` of the NFT and, if it encounters the `walrus site address` key, it will go fetch the Walrus Site that is at the corresponding object ID. diff --git a/docs/walrus-sites/restrictions.md b/docs/walrus-sites/restrictions.md index b7895312..bcb1c4a3 100644 --- a/docs/walrus-sites/restrictions.md +++ b/docs/walrus-sites/restrictions.md @@ -16,10 +16,10 @@ wallet. ## There is a maximum redirect depth The number of consecutive redirects a Walrus Site can perform is capped by the -Portal (see [Portal configuration](./portal.md)). This measure ensures that loading a Walrus Site +portal (see [Portal configuration](./portal.md)). This measure ensures that loading a Walrus Site does not result in an infinite loading loop. -Different Portals can set this limit as they desire. The limit for the Portal hosted at +Different portals can set this limit as they desire. The limit for the portal hosted at has a maximum redirect depth of 3. ## Service workers are not available @@ -35,22 +35,24 @@ worker from within a Walrus Site will result in a dysfunctional site and a poor user. ```admonish note -This limitation only applies to Portal based on service workers. A web Portal will not +This limitation only applies to portal based on service workers. A web portal will not have this limitation. ``` -## iOS Sui mobile wallets do not work with Walrus Sites +## iOS Sui mobile wallets do not work with the service-worker portal Service workers cannot be loaded inside an in-app browser on iOS, because of a limitation of the WebKit engine. As a consequence, Walrus Sites cannot be used within Sui-compatible wallet apps on -iOS. Therefore, Sui wallets cannot currently be used on a Walrus Site on iOS. Note, however, that -*browsing* a Walrus Site is still possible on iOS through any browser. Only the connection to the -wallet is impacted. +iOS. Therefore, Sui wallets cannot currently be used on a service-worker portal on iOS. Note, +however, that *browsing* a Walrus Site is still possible on iOS through any browser. -The connection with the Sui Wallet apps works on Android devices. +To provide a seamless experience for iOS users (and other users on browsers that do not support +service workers), we implemented a redidect to a server-side portal (). Whenever +a user on an iOS wallet browses a Walrus Site, the redirect will automatically take them to the +`.blob.store` server-side portal. This way, the user can still use the wallet. ```admonish note -This limitation only applies to Portal based on service workers. A web Portal will not +This limitation only applies to portals based on service workers. A web portal will not have this limitation. ``` @@ -58,15 +60,15 @@ have this limitation. With the current design, Walrus Sites cannot be used for progressive web apps (PWAs). -Two characteristics of the service-worker Portal prevent support for PWAs: +Two characteristics of the service-worker portal prevent support for PWAs: - Since the service worker needs to be registered for the page to work, the PWA's manifest file cannot be loaded by the browser directly. - There can only be one service worker registered per origin. Therefore, registering a PWA's service worker would remove the Walrus Sites service worker, breaking Walrus Sites' functionality. -Note that the server-side Portal does not share these limitations. However, for the moment, we -support both technologies: Walrus Sites must be able to load from both a service-worker Portal and a -server-side Portal, and therefore have to be built with the more restrictive feature set. For more +Note that the server-side portal does not share these limitations. However, for the moment, we +support both technologies: Walrus Sites must be able to load from both a service-worker portal and a +server-side portal, and therefore have to be built with the more restrictive feature set. For more details, see the [installation requirements for PWAs](https://en.wikipedia.org/wiki/Progressive_web_app#Installation_criteria). diff --git a/docs/walrus-sites/routing.md b/docs/walrus-sites/routing.md new file mode 100644 index 00000000..bfebe2fc --- /dev/null +++ b/docs/walrus-sites/routing.md @@ -0,0 +1,123 @@ +# Specifying headers and routing + +``` admonish tip title="New with Walrus Sites Testnet version" +The following features have been released with the Walrus Sites Testnet version. +``` + +In its base configuration, Walrus Sites serves static assets through a portal. However, many modern +web applications require more advanced features, such as custom headers and client-side routing. + +Therefore, the site-builder can read a `ws-resource.json` configuration file, in which you can +directly specify resource headers and routing rules. + +## The `ws-resources.json` file + +This file is optionally placed in the root of the site directory, and it is *not* uploaded with the +site's resources (in other words, the file is not part of the resulting Walrus Site and is not +served by the portal). + +If you don't want to use this default location, you can specify the path to the configuration file +with the `--ws-resources` flag when running the `publish` or `update` commands. + +The file is JSON-formatted, and looks like the following: + +``` JSON +{ + "headers": { + "/index.html": { + "Content-Type": "text/html; charset=utf-8", + "Cache-Control": "max-age=3500" + } + }, + "routes": { + "/*": "/index.html", + "/accounts/*": "/accounts.html", + "/path/assets/*": "/assets/asset_router.html" + } +} +``` + +We now describe in details the two sections of the configuration file, `headers` and `routes`. + +## Specifying HTTP response headers + +The `headers` section allows you to specify custom HTTP response headers for specific resources. +The keys in the `headers` object are the paths of the resources, and the values are lists of +key-value pairs corresponding to the headers that the portal will attach to the response. + +For example, in the configuration above, the file `index.html` will be served with the +`Content-Type` header set to `text/html; charset=utf-8` and the `Cache-Control` header set to +`max-age=3500`. + +This mechanism allows you to control various aspects of the resource delivery, such as caching, +encoding, and content types. + +```admonish +The resource path is always represented as starting from the root `/`. +``` + +### Default headers + +By default, no headers need to be specified, and the `ws-resources.json` file can be omitted. The +site-builder will automatically try to infer the `Content-Type` header based on the file extension, +and set the `Content-Encoding` to `identity` (no transformation). + +In case the content type cannot be inferred, the `Content-Type` will be set to +`application/octet-stream`. + +These defaults will be overridden by any headers specified in the `ws-resources.json` file. + +## Specifying client-side routing + +The `routes` section allows you to specify client-side routing rules for your site. This is useful +when you want to use a single-page application (SPA) framework, such as React or Angular. + +The configuration in the `routes` object is a mapping from route keys to resource paths. + +The **`routes` keys** are path patterns in the form `/path/to/some/*`, where the `*` character +represents a wildcard. + +```admonish +Currently, the wildcard *can only be only be specified at the end of the path*. +Therefore, `/path/*` is a valid path, while `/path/*/to` and `*/path/to/*` are not. +``` + +The **`routes` values** are the resource paths that should be served when the route key is matched. + +```admonish danger title="Important" +The paths in the values **must** be valid resource paths, meaning that they must be present among +the site's resources. The Walrus sites contract will **abort** if the user tries to create a route +that points to a non-existing resource. +``` + +The simple routing algorithm is as follows: + +- Whenever a resource path *is not found among the sites resources*, the portal tries to match the + path to the `routes`. +- All matching routes are then *lexicographically ordered*, and the *longest* match is chosen. +- The resource corresponding to this longest match is then served. + +```admonish +In other words, the portal will *always* serve a resource if present, and if not present will serve +the resource with the *longest matching prefix* among the routes. +``` + +Recall the example above: + +``` JSON +"routes": { + "/*": "/index.html", + "/path/*": "/accounts.html", + "/path/assets/*": "/assets/asset_router.html" +} +``` + +The following matchings will occur: + +- browsing `/any/other/test.html` will serve `/index.html`; +- browsing `/path/test.html` will serve `/accounts.html`, as it is a more specific match than the + previous one; +- similarly, browsing `/path/assets/test.html` will serve `/assets/asset_router.html`. + +`/index.html`, `/accounts.html`, and `/assets/asset_router.html` are all existing resources on the +Walrus Sites object on Sui. diff --git a/docs/walrus-sites/tutorial-install.md b/docs/walrus-sites/tutorial-install.md index 7315f1a6..8dd983f1 100644 --- a/docs/walrus-sites/tutorial-install.md +++ b/docs/walrus-sites/tutorial-install.md @@ -42,5 +42,5 @@ Commands: sitemap Show the pages composing the Walrus site at the given object ID help Print this message or the help of the given subcommand(s) -⋮ + ⋮ ``` diff --git a/docs/walrus-sites/tutorial-migration.md b/docs/walrus-sites/tutorial-migration.md new file mode 100644 index 00000000..1668c46e --- /dev/null +++ b/docs/walrus-sites/tutorial-migration.md @@ -0,0 +1,25 @@ +# Migrating your site from Devnet to Testnet + +The migration of a Walrus Site from the Devnet to the Testnet is a very simple manual process. +This is required because both the storage backing the sites (Walrus) and the contracts on Sui +implementing the Walrus Sites functionality have been updated. + +``` admonish tip +The migration will result in a new Site object on Sui (with a different object ID), and new blob +objects on Walrus Testnet. +``` + +The steps are the following: + +- Get the latest version of the `walrus` binary, as well as the latest Walrus configuration file, + following the [Walrus installation instructions](../usage/setup.md). +- Ensure you have the latest version of the `site-builder` binary by following the [installation + instructions](./tutorial-install.md) again. Remember to `git pull` if you are building from the + repo and have cloned it previously. Pulling the repo also guarantees you have the latest + sites configuration file, pointing to the correct contracts. +- Run the `site-builder` with the `publish` command on your site directory. This will create a new + Walrus Site object on Sui, using the new contracts, and store the site files anew on Walrus + Testnet. Note that this operation will create a new object ID for your site! +- Optional: If you had set up a SuiNS name for your site, you will need to point the name to the new + site's object ID. See the [tutorial on setting a SuiNS name](./tutorial-suins.md) for more + details. diff --git a/docs/walrus-sites/tutorial-publish.md b/docs/walrus-sites/tutorial-publish.md index a990b56a..0a1573fd 100644 --- a/docs/walrus-sites/tutorial-publish.md +++ b/docs/walrus-sites/tutorial-publish.md @@ -46,7 +46,7 @@ site builder. The configuration file is necessary to ensure that the `site-build correct Sui package for the Walrus Sites logic. More details on the configuration of the `site-builder` can be found under the [advanced -configuration](tutorial-config.md) section. +configuration](./builder-config.md) section. ## Update the site @@ -83,18 +83,3 @@ Browsing to the provided URL should reflect the change. You've updated the site! ```admonish note The wallet you are using must be the *owner* of the Walrus Site object to be able to update it. ``` - -## Additional commands - -The `site-builder` tool provides two additional utilities: - -- The `convert` command converts an object ID in hex format to the equivalent Base36 - format. This command is useful if you have the Sui object ID of a Walrus Site, and want to know - the subdomain where you can browse it. -- The `sitemap` command shows the resources that compose the Walrus Site at the given object ID. - -```admonish tip -In general, the `--help` flag is your friend, you can add it to get further details for the whole -CLI (`./target/release/site-builder --help`) or individual commands (e.g., -`./target/release/site-builder update --help`). -``` diff --git a/docs/walrus-sites/tutorial-suins.md b/docs/walrus-sites/tutorial-suins.md index c06464f9..5d059d60 100644 --- a/docs/walrus-sites/tutorial-suins.md +++ b/docs/walrus-sites/tutorial-suins.md @@ -27,18 +27,20 @@ in the bar the object ID of the Walrus Site, check that it is correct, and click After approving the transaction, we can now browse ! -### The CLI way +--- + +## The CLI way For completeness, we report here a manual way of setting the mapping between the SuiNS name and the Walrus Site, using the CLI. -#### Get the SuiNS object ID +### Get the SuiNS object ID Go to the ["names you own"](https://testnet.suins.io/account/my-names) section of the SuiNS website, click the three-dots menu on the top-right corner of the name, choose "View all info", and copy the `ObjectID`. In our case, this is `0x6412...`. -#### Send the SuiNS registration object to the address you use with the Sui CLI +### Send the SuiNS registration object to the address you use with the Sui CLI The steps that follow require that the SuiNS registration object is owned by the address you are using on the Sui CLI. Therefore, unless you use the same address in your browser wallet and the CLI, @@ -53,13 +55,12 @@ sui client active-address Then, from your browser wallet, select the "Assets" tab and look for the NFT of the SuiNS registration, which should look as follows: - ![the SuiNS registration inside the wallet](../assets/suins-asset.png) Click on it, scroll down to "Send NFT", and send it to the address discovered with the command above. Now, your Sui CLI address owns the registration NFT, and you can proceed to the next step. -#### Use the CLI to map the SuiNS name to the Walrus Site +### Use the CLI to map the SuiNS name to the Walrus Site This step associates the name `walrusgame` to the object ID of our Walrus Site. There are possibly many ways to achieve this, one is to issue the following transaction using the Sui CLI to create From d3619c0d882e805fbcc434e88a02b0fc64a30819 Mon Sep 17 00:00:00 2001 From: giac-mysten <124184614+giac-mysten@users.noreply.github.com> Date: Thu, 17 Oct 2024 14:10:07 +0800 Subject: [PATCH 32/50] chore: update the location of the site-builder config (#136) Updates the location of the site builder config to match the `walrus-sites` repo. Signed-off-by: giac-mysten --- .github/workflows/publish.yaml | 2 +- .github/workflows/update-binaries.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index d5cb48b3..e86344a9 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -68,5 +68,5 @@ jobs: run: > RUST_LOG=site_builder=debug,walrus=debug,info site-builder - --config walrus-sites/site-builder/assets/builder-example.yaml + --config walrus-sites/sites-config.yaml update build/html ${{ vars.WALRUS_SITE_OBJECT }} diff --git a/.github/workflows/update-binaries.yaml b/.github/workflows/update-binaries.yaml index f433ddb9..1068a899 100644 --- a/.github/workflows/update-binaries.yaml +++ b/.github/workflows/update-binaries.yaml @@ -45,5 +45,5 @@ jobs: run: > RUST_LOG=site_builder=debug,walrus=debug,info site-builder - --config walrus-sites/site-builder/assets/builder-example.yaml + --config walrus-sites/sites-config.yaml update --list-directory site ${{ vars.WALRUS_SITE_BIN_OBJECT }} From 90ef27deb3f4b198dcd06f475a062a985494e44e Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 17 Oct 2024 08:52:41 +0200 Subject: [PATCH 33/50] ci: update Walrus setup to Testnet (#137) See [this workflow run](https://github.com/MystenLabs/walrus-docs/actions/runs/11379789762/job/31657862945). --- .github/actions/set-up-walrus/action.yaml | 12 ++++++++---- .github/workflows/publish.yaml | 1 + .github/workflows/update-binaries.yaml | 1 + 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/actions/set-up-walrus/action.yaml b/.github/actions/set-up-walrus/action.yaml index 0d50422a..ab36999f 100644 --- a/.github/actions/set-up-walrus/action.yaml +++ b/.github/actions/set-up-walrus/action.yaml @@ -8,6 +8,9 @@ inputs: SUI_KEYSTORE: description: The content of the Sui keystore file required: true + WALRUS_CONFIG: + description: The content of the Walrus configuration file + required: true runs: using: "composite" @@ -54,11 +57,12 @@ runs: - name: Install and configure Walrus run: | # The bin directory was already created and added to $PATH in the build-mdbook action - curl https://storage.googleapis.com/mysten-walrus-binaries/walrus-latest-ubuntu-x86_64 -o bin/walrus + curl https://storage.googleapis.com/mysten-walrus-binaries/walrus-testnet-latest-ubuntu-x86_64 -o bin/walrus chmod +x bin/walrus - mkdir -p ~/.walrus - curl https://storage.googleapis.com/mysten-walrus-binaries/walrus-configs/client_config.yaml -o ~/.walrus/client_config.yaml - walrus -h # Ensure the walrus binary works + mkdir -p ~/.config/walrus + echo '${{ inputs.WALRUS_CONFIG }}' > ~/.config/walrus/client_config.yaml + walrus --version + walrus info # Ensure the walrus binary works shell: bash - name: Clone walrus-sites diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index e86344a9..e9df8a48 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -63,6 +63,7 @@ jobs: with: SUI_ADDRESS: "${{ vars.SUI_ADDRESS }}" SUI_KEYSTORE: "${{ secrets.SUI_KEYSTORE }}" + WALRUS_CONFIG: "${{ vars.WALRUS_CONFIG }}" - name: Update Walrus Site run: > diff --git a/.github/workflows/update-binaries.yaml b/.github/workflows/update-binaries.yaml index 1068a899..63f732fc 100644 --- a/.github/workflows/update-binaries.yaml +++ b/.github/workflows/update-binaries.yaml @@ -30,6 +30,7 @@ jobs: with: SUI_ADDRESS: "${{ vars.SUI_ADDRESS }}" SUI_KEYSTORE: "${{ secrets.SUI_KEYSTORE }}" + WALRUS_CONFIG: "${{ vars.WALRUS_CONFIG }}" - name: Create temporary directory run: "mkdir -p site" From 1e6f154ed009cb224d2c1076ce814ca18d91114c Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 17 Oct 2024 10:12:38 +0200 Subject: [PATCH 34/50] temporary exception for links to blob.store --- book.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/book.toml b/book.toml index 13e4d914..99f552f7 100644 --- a/book.toml +++ b/book.toml @@ -22,6 +22,7 @@ exclude = [ 'google\.com', 'x\.com', 'suiscan\.xyz', + 'blob\.store', # TODO: remove this when the portal no longer returns a 404 ] [preprocessor] From 8cdac4ae72ec0da86a2bbc182026720044920664 Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 17 Oct 2024 10:12:48 +0200 Subject: [PATCH 35/50] warnings about discontinuation of Walrus Devnet --- docs/README.md | 8 ++++++++ docs/blog/04_testnet_update.md | 24 +++++++++++++++--------- docs/usage/setup.md | 6 ++---- docs/walrus-sites/commands.md | 2 +- docs/walrus-sites/intro.md | 9 +++++---- 5 files changed, 31 insertions(+), 18 deletions(-) diff --git a/docs/README.md b/docs/README.md index c5849fd9..e73920f6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -33,6 +33,14 @@ to store anything that contains secrets or private data without additional measu confidentiality. ``` +```admonish danger title="Discontinuation of Walrus Devnet" +The previous Walrus Devnet instance is now deprecated and **will be shut down after 2024-10-31**. +All data stored on Walrus Devnet (including Walrus Sites) will no longer be accessible at that +point. You need to re-upload all data to Walrus Testnet if you want it to remain accessible. Walrus +Sites also need to be migrated as described on the dedicated [migration +page](./walrus-sites/tutorial-migration.md). +``` + ## Features - **Storage and retrieval:** Walrus supports storage operations to write and read blobs. It also diff --git a/docs/blog/04_testnet_update.md b/docs/blog/04_testnet_update.md index 66d653eb..e236e870 100644 --- a/docs/blog/04_testnet_update.md +++ b/docs/blog/04_testnet_update.md @@ -1,7 +1,6 @@ # Announcing Testnet - -Published on: 2024-10-XX +Published on: 2024-10-17 Today, a community of operators launches the first public Walrus Testnet. This is an important milestone in validating the operation of Walrus as a decentralized blob store, @@ -80,11 +79,10 @@ storage node committee changes: better shard allocation mechanisms upon changes stake; efficient ways to sync state between storage nodes; as well as better ways for storage nodes to follow Sui event streams. - -- Explore the [Walrus staking dApp](https://app.org) -- Look at recent activity on the [Walrus Explorer](https://app.org) +- Explore the [Walrus staking dApp](https://stake.walrus.site) +- Look at recent activity on the [Walrus Explorer](https://walruscan.com/testnet/home) -## New Move Contracts & documentation +## New Move contracts & documentation As part of the Testnet release of Walrus the documentation and Move Smart contracts have been updated, and can be found at the [Walrus-docs repository](https://github.com/MystenLabs/walrus-docs) @@ -97,6 +95,14 @@ update greatly increase the flexibility, speed, and security of Walrus Sites. De specify client-side routing rules, and add custom HTTP headers to the portals' responses for their site, expanding the possibilities for what Walrus Sites can do. -[Migrate now](../walrus-sites/tutorial-migration.md) to take advantage of these new features! -The old Walrus Sites, based on Walrus Devnet, will still be available for a short time. However, -Devnet will be wiped soon, so it is recommended to migrate as soon as possible. +[Migrate now](../walrus-sites/tutorial-migration.md) to take advantage of these new features! The +old Walrus Sites, based on Walrus Devnet, will still be available for a short time. However, Devnet +will be wiped soon (as described below), so it is recommended to migrate as soon as possible. + +## Discontinuation of Walrus Devnet + +The previous Walrus Devnet instance is now deprecated and **will be shut down after 2024-10-31**. +All data stored on Walrus Devnet (including Walrus Sites) will no longer be accessible at that +point. You need to re-upload all data to Walrus Testnet if you want it to remain accessible. Walrus +Sites also need to be migrated as described on the dedicated [migration +page](../walrus-sites/tutorial-migration.md). diff --git a/docs/usage/setup.md b/docs/usage/setup.md index 7bce0ae3..5df90078 100644 --- a/docs/usage/setup.md +++ b/docs/usage/setup.md @@ -124,10 +124,8 @@ Commands: ``` ```admonish tip -Our latest Walrus binaries are also available on Walrus itself, namely on . -Note, however, that you can only access this through a web browser and not through CLI tools like -cURL due to the service-worker architecture (see the [Walrus Sites docs](../walrus-sites/portal.md) -for further insights). +Our latest Testnet Walrus binaries are also available on Walrus itself, namely on +, for example, . ``` ### Previous versions (optional) diff --git a/docs/walrus-sites/commands.md b/docs/walrus-sites/commands.md index eb72267f..f96d3d4a 100644 --- a/docs/walrus-sites/commands.md +++ b/docs/walrus-sites/commands.md @@ -25,7 +25,7 @@ set to 200 epochs. If you are just uploading raw files without an `index.html`, you may want to use the `--list-directory` flag, which will automatically create an index page to browse the files. See for -example . +example . The `publish` command will also respect the instructions in the `ws-resources.json` configuration file. To know more, see the section on [specifying headers and routing](./routing.md). diff --git a/docs/walrus-sites/intro.md b/docs/walrus-sites/intro.md index 51a511a7..5fcbb650 100644 --- a/docs/walrus-sites/intro.md +++ b/docs/walrus-sites/intro.md @@ -13,11 +13,12 @@ site](./tutorial-migration.md) today to take advantage of these new features! ``` ```admonish danger title="Walrus Sites Devnet being discontinued" -Since the Walrus Devnet will be shut down soon, all the Walrus Sites stored on it will be wiped. To -minimize the downtime, the Devnet Walrus Sites will be available after the Testnet upgrade for two -weeks (until **2024-10-31**), to ensure that everyone has enough time to update. +Since the Walrus Devnet will [be shut down +soon](../README.md#admonition-discontinuation-of-walrus-devnet), all the Walrus Sites stored on it will +be wiped. To minimize the downtime, the Devnet Walrus Sites will be available after the Testnet +upgrade for two weeks (until **2024-10-31**), to ensure that everyone has enough time to update. -Make sure to reupload your sites to the Walrus Tevnet before the shutdown to avoid downtime! +Make sure to reupload your sites to the Walrus Testnet before the shutdown to avoid downtime! ``` At a high level, here are some of the most exciting features: From 84b3b5b33c2c667d01d1754dc4f8202f5e232756 Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 17 Oct 2024 10:22:52 +0200 Subject: [PATCH 36/50] clarify WAL/FROST relationship --- docs/README.md | 11 ++++++----- docs/blog/04_testnet_update.md | 5 ++++- docs/usage/client-cli.md | 28 +++++++++++++++------------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/docs/README.md b/docs/README.md index e73920f6..b463c387 100644 --- a/docs/README.md +++ b/docs/README.md @@ -60,11 +60,12 @@ page](./walrus-sites/tutorial-migration.md). long, extend its lifetime or optionally delete it. - **Epochs, tokenomics, and delegated proof of stake** Walrus is operated by a committee of storage - nodes that evolve between epochs. A native token, WAL (and its subdivision FROST), is used - to delegate stake to storage nodes, and those with high stake become part of the epoch committee. - The WAL token is also used for payments for storage. At the end of each epoch, rewards for - selecting storage nodes, storing and serving blobs are distributed to storage nodes and whose that - stake with them. All these processes are mediated by smart contracts on the Sui platform. + nodes that evolve between epochs. A native token, WAL (and its subdivision FROST, where 1 WAL is + equal to 1 billion FROST), is used to delegate stake to storage nodes, and those with high stake + become part of the epoch committee. The WAL token is also used for payments for storage. At the + end of each epoch, rewards for selecting storage nodes, storing and serving blobs are distributed + to storage nodes and whose that stake with them. All these processes are mediated by smart + contracts on the Sui platform. - **Flexible access:** Users can interact with Walrus through a command-line interface (CLI), software development kits (SDKs), and web2 HTTP technologies. Walrus is designed to work well diff --git a/docs/blog/04_testnet_update.md b/docs/blog/04_testnet_update.md index e236e870..f483c338 100644 --- a/docs/blog/04_testnet_update.md +++ b/docs/blog/04_testnet_update.md @@ -51,12 +51,15 @@ Payments for blob storage and extending blob expiry are denominated in Testnet W Walrus token issued on the Sui Testnet. Testnet WAL has no value, and an unlimited supply - so no need to covet or hoard it - its just for testing purposes and only issued on Sui Testnet. +WAL also has a smaller unit called FROST, similar to MIST for SUI. 1 WAL is equal to 1 billion +(1000000000) FROST. + To make Testnet WAL available to all who want to experiment with the Walrus Testnet we provide a utility and smart contract to convert Testnet SUI (which also has no value) into Testnet WAL using a one-to-one exchange rate. This is chosen arbitrarily, and generally one should not read too much into the actual WAL denominated costs of storage on Testnet. They have been chosen arbitrarily. -- Find out how to [request Test WAL tokens](../usage/setup.md#testnet-wal-faucet) through the CLI. +Find out how to [request Test WAL tokens](../usage/setup.md#testnet-wal-faucet) through the CLI. ## Decentralization through staking & unstaking diff --git a/docs/usage/client-cli.md b/docs/usage/client-cli.md index b5045629..84a7b378 100644 --- a/docs/usage/client-cli.md +++ b/docs/usage/client-cli.md @@ -15,36 +15,38 @@ their meaning. ## Walrus system information Information about the Walrus system is available through the `walrus info` command. For example, - +`walrus info` gives an overview of the number of storage nodes and shards in the system, the maximum +blob size, and the current cost in (Testnet) WAL for storing blobs: ```console $ walrus info Walrus system information -Current epoch: 54 +Current epoch: 2 Storage nodes -Number of nodes: 10 +Number of nodes: 25 Number of shards: 1000 Blob size Maximum blob size: 13.3 GiB (14,273,391,930 B) -Storage unit: 1.00 KiB +Storage unit: 1.00 MiB Approximate storage prices per epoch -Price per encoded storage unit: 5 FROST -Price to store metadata: 0.0003 WAL -Marginal price per additional 1 MiB (w/o metadata): 24,195 FROST +Price per encoded storage unit: 100 FROST +Price to store metadata: 6,200 FROST +Marginal price per additional 1 MiB (w/o metadata): 500 FROST Total price for example blob sizes -16.0 MiB unencoded (135 MiB encoded): 0.0007 WAL per epoch -512 MiB unencoded (2.33 GiB encoded): 0.012 WAL per epoch -13.3 GiB unencoded (60.5 GiB encoded): 0.317 WAL per epoch - +16.0 MiB unencoded (135 MiB encoded): 13,500 FROST per epoch +512 MiB unencoded (2.33 GiB encoded): 0.0002 WAL per epoch +13.3 GiB unencoded (60.5 GiB encoded): 0.0062 WAL per epoch ``` -gives an overview of the number of storage nodes and shards in the system, the maximum blob size, -and the current cost in (Testnet) WAL for storing blobs. (Note: 1 WAL = 1 000 000 000 FROST) +```admonish tip title="FROST and WAL" +FROST is the smaller unit of WAL, similar to MIST for SUI. The conversion is also the same as for +SUI: `1 WAL = 1 000 000 000 FROST`. +``` Additional information such as encoding parameters and sizes, BFT system information, and information on the storage nodes and their shard distribution can be viewed with the `--dev` From a66e58fcde92ee5f3c7fb3806c0b6264a1d1a27e Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 17 Oct 2024 10:29:26 +0200 Subject: [PATCH 37/50] add Windows binary and re-add Ubuntu generic binary --- docs/usage/setup.md | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/docs/usage/setup.md b/docs/usage/setup.md index 5df90078..37934e2f 100644 --- a/docs/usage/setup.md +++ b/docs/usage/setup.md @@ -71,34 +71,35 @@ when [running the CLI](./interacting.md). ## Installation -We currently provide the `walrus` client binary for macOS (Intel and Apple CPUs) and Ubuntu: - -| OS | CPU | Architecture | -| ------ | ------------- | ------------------------------------------------------------------------------------------------------------ | -| MacOS | Apple Silicon | [`macos-arm64`](https://storage.googleapis.com/mysten-walrus-binaries/walrus-testnet-latest-macos-arm64) | -| MacOS | Intel 64bit | [`macos-x86_64`](https://storage.googleapis.com/mysten-walrus-binaries/walrus-testnet-latest-macos-x86_64) | -| Ubuntu | Intel 64bit | [`ubuntu-x86_64`](https://storage.googleapis.com/mysten-walrus-binaries/walrus-testnet-latest-ubuntu-x86_64) | - - - - - +We currently provide the `walrus` client binary for macOS (Intel and Apple CPUs), Ubuntu, and +Windows: + +| OS | CPU | Architecture | +| ------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| Ubuntu | Intel 64bit | [`ubuntu-x86_64`](https://storage.googleapis.com/mysten-walrus-binaries/walrus-testnet-latest-ubuntu-x86_64) | +| Ubuntu | Intel 64bit (generic) | [`ubuntu-x86_64-generic`](https://storage.googleapis.com/mysten-walrus-binaries/walrus-testnet-latest-ubuntu-x86_64-generic) | +| MacOS | Apple Silicon | [`macos-arm64`](https://storage.googleapis.com/mysten-walrus-binaries/walrus-testnet-latest-macos-arm64) | +| MacOS | Intel 64bit | [`macos-x86_64`](https://storage.googleapis.com/mysten-walrus-binaries/walrus-testnet-latest-macos-x86_64) | +| Windows | Intel 64bit | [`windows-x86_64.exe`](https://storage.googleapis.com/mysten-walrus-binaries/walrus-testnet-latest-windows-x86_64.exe) | + +```admonish title="Windows" +We now offer a pre-built binary also for Windows. However, most of the remaining instructions assume +a UNIX-based system for the directory structure, commands, etc. If you use Windows, you may need to +adapt most of those. +``` You can download the latest build from our Google Cloud Storage (GCS) bucket (correctly setting the `$SYSTEM` variable)`: ```sh -SYSTEM=ubuntu-x86_64 # or macos-x86_64 or macos-arm64 +SYSTEM= # set this to your system: ubuntu-x86_64, ubuntu-x86_64-generic, macos-x86_64, macos-arm64, windows-x86_64.exe curl https://storage.googleapis.com/mysten-walrus-binaries/walrus-testnet-latest-$SYSTEM -o walrus chmod +x walrus ``` - - To be able to run it simply as `walrus`, move the binary to any directory included in your `$PATH` environment variable. Standard locations are `/usr/local/bin/`, `$HOME/bin/`, or From 9fe97c1cd91367ef1e07fcdf53c3e24f4964374c Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 17 Oct 2024 10:33:04 +0200 Subject: [PATCH 38/50] ci: generate preview even if linkcheck fails --- .github/workflows/pages-preview.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/pages-preview.yaml b/.github/workflows/pages-preview.yaml index 15942c5e..572df28b 100644 --- a/.github/workflows/pages-preview.yaml +++ b/.github/workflows/pages-preview.yaml @@ -21,11 +21,20 @@ permissions: pull-requests: write jobs: + build-with-linkcheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/build-mdbook + if: github.event.action != 'closed' + preview: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ./.github/actions/build-mdbook + with: + with_linkcheck: "false" if: github.event.action != 'closed' - name: Deploy preview uses: rossjrw/pr-preview-action@v1.4.8 From 8638dfff1019c0067d5875e92fea6e1dcd305d92 Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 17 Oct 2024 10:35:35 +0200 Subject: [PATCH 39/50] Revert "temporary exception for links to blob.store" This reverts commit 1e6f154ed009cb224d2c1076ce814ca18d91114c. --- book.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/book.toml b/book.toml index 99f552f7..13e4d914 100644 --- a/book.toml +++ b/book.toml @@ -22,7 +22,6 @@ exclude = [ 'google\.com', 'x\.com', 'suiscan\.xyz', - 'blob\.store', # TODO: remove this when the portal no longer returns a 404 ] [preprocessor] From 40ae5b42d554887257c65c0af46981d2180f2ebb Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 17 Oct 2024 11:43:19 +0200 Subject: [PATCH 40/50] slightly extend the glossary --- docs/glossary.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/glossary.md b/docs/glossary.md index 9f7acb64..12755dd4 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -7,22 +7,26 @@ concepts, their canonical name, and how they relate to or differ from other term Italicized terms in the description indicate other specific Walrus terms contained in the table. | Approved name | Description | -| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | storage node (SN) | entity storing data for Walrus; holds one or several *shards* | | blob | single unstructured data object stored on Walrus | +| permanent blob | blob which cannot be deleted by its owner and is guaranteed to be available until at least its expiry epoch (assuming it is valid) | +| deletable blob | blob which can be deleted by its owner at any time to be able to reuse the storage resource | | shard | (disjoint) subset of erasure-encoded data of all *blobs*; at every point in time, a *shard* is assigned to and stored on a single *SN* | +| RedStuff | our erasure-encoding approach, which uses two different encodings (*primary* and *secondary*) to enable shard recovery; details are available in the [whitepaper](./walrus.pdf) | | sliver | erasure-encoded data of one *shard* corresponding to a single blob for one of the two encodings; this contains several erasure-encoded symbols of that blob but not the *blob metadata* | +| sliver pair | the combination of a shard’s primary and secondary sliver | | blob ID | cryptographic ID computed from a *blob*’s *slivers* | | blob metadata | metadata of one *blob*; in particular, this contains a hash per *shard* to enable the authentication of *slivers* and recovery symbols | | (end) user | any entity/person that wants to store or read *blobs* on/from Walrus; can act as a Walrus client itself or use the simple interface exposed by *publishers* and *caches* | -| publisher | service interacting with Sui and the *SNs* to store *blobs* on Walrus; offers a simple `HTTP POST` endpoint to *end users* | -| aggregator | service that reconstructs *blobs* by interacting with *SNs* and exposes a simple `HTTP GET` endpoint to *end users* | +| publisher | service interacting with Sui and the *SNs* to store *blobs* on Walrus; offers a simple `HTTP POST` endpoint to *end users* | +| aggregator | service that reconstructs *blobs* by interacting with *SNs* and exposes a simple `HTTP GET` endpoint to *end users* | | cache | an *aggregator* with additional caching capabilities | | (Walrus) client | entity interacting directly with the *SNs*; this can be an *aggregator*/*cache*, a *publisher*, or an *end user* | | (blob) reconstruction | decoding of the primary *slivers* to obtain the blob; includes re-encoding the *blob* and checking the Merkle proofs | -| (shard/sliver) recovery | process of an *SN* recovering a *sliver* or full *shard* by obtaining recovery symbols from other *SNs* | +| (shard/sliver) recovery | process of an *SN* recovering a *sliver* or full *shard* by obtaining recovery symbols from other *SNs* | | storage attestation | process where *SNs* exchange challenges and responses to demonstrate that they are storing their currently assigned *shards* | -| certificate of availability (CoA) | a *blob ID* with signatures of *SNs* holding at least \(2f+1\) *shards* in a specific *epoch* | +| certificate of availability (CoA) | a *blob ID* with signatures of *SNs* holding at least \(2f+1\) *shards* in a specific *epoch* | | point of availability (PoA) | point in time when a *CoA* is submitted to Sui and the corresponding *blob* is guaranteed to be available until its expiration | | inconsistency proof | set of several recovery symbols with their Merkle proofs such that the decoded *sliver* does not match the corresponding hash; this proves an incorrect/inconsistent encoding by the client | | inconsistency certificate | an aggregated signature from 2/3 of *SNs* (weighted by their number of *shards*) that they have seen and stored an *inconsistency proof* for a *blob ID* | @@ -30,4 +34,4 @@ Italicized terms in the description indicate other specific Walrus terms contain | member | an *SN* that is part of a *committee* at some *epoch* | | storage epoch | the epoch for Walrus as distinct to the epoch for Sui | | availability period | the period specified in *storage epochs* for which a *blob* is certified to be available on Walrus | -| expiry | the end epoch at which a blob is no longer available and can be deleted | +| expiry | the end epoch at which a blob is no longer available and can be deleted; the end epoch is always exclusive | From 0eec53bde0c37d6a82953abf8b85091ef63be306 Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 17 Oct 2024 11:46:09 +0200 Subject: [PATCH 41/50] add WAL and FROST to the glossary --- docs/glossary.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/glossary.md b/docs/glossary.md index 12755dd4..2733fc3c 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -34,4 +34,6 @@ Italicized terms in the description indicate other specific Walrus terms contain | member | an *SN* that is part of a *committee* at some *epoch* | | storage epoch | the epoch for Walrus as distinct to the epoch for Sui | | availability period | the period specified in *storage epochs* for which a *blob* is certified to be available on Walrus | -| expiry | the end epoch at which a blob is no longer available and can be deleted; the end epoch is always exclusive | +| expiry | the end epoch at which a blob is no longer available and can be deleted; the end epoch is always exclusive | +| WAL | the native Token of Walrus | +| FROST | the smallest unit of WAL (similar to MIST for SUI); 1 WAL is equal to 1 billion (1000000000) FROST | From 5ad52ac8b1811d57f798e8dd63390cb6a8e60935 Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 17 Oct 2024 11:54:06 +0200 Subject: [PATCH 42/50] add client config --- .gitignore | 2 +- docs/client_config.yaml | 3 +++ docs/usage/setup.md | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 docs/client_config.yaml diff --git a/.gitignore b/.gitignore index e20a84bc..e5ab4eba 100644 --- a/.gitignore +++ b/.gitignore @@ -28,5 +28,5 @@ working_dir walrus !/contracts/walrus examples/CONFIG/bin/walrus -client_config.yaml +./client_config.yaml examples/CONFIG/config_dir/client_config.yaml diff --git a/docs/client_config.yaml b/docs/client_config.yaml new file mode 100644 index 00000000..ce2d09ed --- /dev/null +++ b/docs/client_config.yaml @@ -0,0 +1,3 @@ +system_object: 0x50b84b68eb9da4c6d904a929f43638481c09c03be6274b8569778fe085c1590d +staking_object: 0x37c0e4d7b36a2f64d51bba262a1791f844cfd88f31379f1b7c04244061d43914 +exchange_object: 0x0e60a946a527902c90bbc71240435728cd6dc26b9e8debc69f09b71671c3029b diff --git a/docs/usage/setup.md b/docs/usage/setup.md index 37934e2f..4cd0b84f 100644 --- a/docs/usage/setup.md +++ b/docs/usage/setup.md @@ -149,6 +149,9 @@ staking_object: 0x37c0e4d7b36a2f64d51bba262a1791f844cfd88f31379f1b7c04244061d439 exchange_object: 0x0e60a946a527902c90bbc71240435728cd6dc26b9e8debc69f09b71671c3029b ``` +The easiest way to obtain the latest configuration is by downloading it from +. + ### Custom path (optional) {#config-custom-path} By default, the Walrus client will look for the `client_config.yaml` (or `client_config.yml`) From 51457ed7acce02ea206e71411ec94866b0d3439c Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 17 Oct 2024 11:57:00 +0200 Subject: [PATCH 43/50] add warning about concurrency issues --- docs/usage/client-cli.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/usage/client-cli.md b/docs/usage/client-cli.md index 84a7b378..de8e5873 100644 --- a/docs/usage/client-cli.md +++ b/docs/usage/client-cli.md @@ -60,6 +60,12 @@ to store anything that contains secrets or private data without additional measu confidentiality. ``` +```admonish warning +It must be ensured that only a single process uses the Sui wallet for write actions (storing or +deleting). When using multiple instances of the client simultaneously, each of them must be pointed +to a different wallet. +``` + Storing blobs on Walrus can be achieved through the following command: ```sh From ef8df00819449e06c41230d9ec0f0a19f5e06d23 Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 17 Oct 2024 12:01:30 +0200 Subject: [PATCH 44/50] describe more store optimizations --- docs/usage/client-cli.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/usage/client-cli.md b/docs/usage/client-cli.md index de8e5873..3842fd10 100644 --- a/docs/usage/client-cli.md +++ b/docs/usage/client-cli.md @@ -75,9 +75,20 @@ walrus store The store command takes a CLI argument `--epochs ` (or `-e`) indicating the number of epochs the blob should be stored for. This defaults to 1 epoch, namely the current one. -If the blob is already stored on Walrus for a sufficient number of epochs the command does not store -it again. However, this behavior can be overwritten with the `--force` (or `-f`) CLI option, which -stores the blob again and creates a fresh blob object on Sui belonging to the wallet address. +```admonish tip title="Automatic optimizations" +When storing a blob, the client performs a number of automatic optimizations, including the +following: + +- If the blob is already stored as a *permanent blob* on Walrus for a sufficient number of epochs + the command does not store it again. This behavior can be overwritten with the `--force` (or `-f`) + CLI option, which stores the blob again and creates a fresh blob object on Sui belonging to the + wallet address. +- If the user's wallet has a compatible storage resource, this one is (re-)used instead of buying a + new one. +- If the blob is already certified on Walrus but as a *deletable* blob or not for a sufficient + number of epochs, the command skips sending data to the storage nodes and just collects the + availability certificate +``` The status of a blob can be queried through one of the following commands: From 2e6576ba89d101efab4efa5dcddb1a38d7ebf899 Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 17 Oct 2024 13:46:22 +0200 Subject: [PATCH 45/50] Add WAL faucet to CLI page --- docs/usage/client-cli.md | 5 +++++ docs/usage/setup.md | 9 +++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/usage/client-cli.md b/docs/usage/client-cli.md index 3842fd10..90f34ed9 100644 --- a/docs/usage/client-cli.md +++ b/docs/usage/client-cli.md @@ -66,6 +66,11 @@ deleting). When using multiple instances of the client simultaneously, each of t to a different wallet. ``` +```admonish tip title="Obtaining Testnet WAL" +You can exchange Testnet SUI for Testnet WAL by running `walrus get-wal`. See the [setup +page](./setup.md#testnet-wal-faucet) for further details. +``` + Storing blobs on Walrus can be achieved through the following command: ```sh diff --git a/docs/usage/setup.md b/docs/usage/setup.md index 4cd0b84f..33368f39 100644 --- a/docs/usage/setup.md +++ b/docs/usage/setup.md @@ -221,8 +221,8 @@ If you specify a wallet path, make sure your wallet is set up for Sui **Testnet* ## Testnet WAL faucet The Walrus Testnet uses Testnet WAL tokens to buy storage and stake. Testnet WAL tokens have no -value and can be exchanged for some Testnet SUI tokens, which also have no value, through the -command: +value and can be exchanged (at a 1:1 rate) for some Testnet SUI tokens, which also have no value, +through the following command: ```sh walrus get-wal @@ -245,5 +245,6 @@ sui client balance ``` By default, 0.5 SUI are exchanged for 0.5 WAL, but a different amount of SUI may be exchanged using -the `--amount` option, and a specific SUI/WAL exchange object may be used through the -`--exchange-id` option. The `walrus get-wal --help` command provides more information about those. +the `--amount` option (the value is in MIST/FROST), and a specific SUI/WAL exchange object may be +used through the `--exchange-id` option. The `walrus get-wal --help` command provides more +information about those. From 733e634c19adf37b02f5e023e3cc60d1c83deaad Mon Sep 17 00:00:00 2001 From: giac-mysten <124184614+giac-mysten@users.noreply.github.com> Date: Thu, 17 Oct 2024 19:48:19 +0800 Subject: [PATCH 46/50] docs: update walrus sites references and addresses Signed-off-by: giac-mysten --- docs/walrus-sites/intro.md | 6 +-- docs/walrus-sites/linking.md | 2 +- docs/walrus-sites/overview.md | 4 +- docs/walrus-sites/redirects.md | 5 +- docs/walrus-sites/tutorial-migration.md | 5 ++ docs/walrus-sites/tutorial-publish.md | 57 +++++++++++++--------- docs/walrus-sites/tutorial-suins.md | 63 +------------------------ 7 files changed, 51 insertions(+), 91 deletions(-) diff --git a/docs/walrus-sites/intro.md b/docs/walrus-sites/intro.md index 5fcbb650..695140cd 100644 --- a/docs/walrus-sites/intro.md +++ b/docs/walrus-sites/intro.md @@ -58,11 +58,11 @@ and has special characteristics (the background color, the image, ...) that are contents of the NFT. The URL to this per-NFT site looks something like this: -`https://4egmmrw9izzjn0dm2lkd3k0l8phk386z60ub1tpdc1jswbb5dr.walrus.site/`. You'll notice that the +`https://2hzpawjycxuiuh36047yocxapc69g8ercrypa7ccsk8ek4iqu6.walrus.site/`. You'll notice that the domain remains `walrus.site`, but the subdomain is a long and random-looking string. This string is actually the [Base36](https://en.wikipedia.org/wiki/Base36) encoding of the object ID of the NFT, which is -[0xb09b312b...](https://suiscan.xyz/testnet/object/0xb09b312b28049467dd6173b6cebb60ed5fe3046883e248632bf9fb20b7dbdaff). +[0x644bc958...](https://suiscan.xyz/testnet/object/0x644bc958052463785c026a13be051d579c8a4d2dc93b1874dce5839d4fb18c5e). In summary: @@ -71,7 +71,7 @@ In summary: - The subdomain on the URL points to a specific object on Sui that allows the browser to fetch and render the site resources. This pointer can be - a SuiNS name, such as `flatland` in `https://flatland.walrus.site`, or - - the Base36 encoding of a the Sui object ID, such as `0xb09b312b...` in the example above. + - the Base36 encoding of a the Sui object ID, such as `0x644bc958...` in the example above. Curious to know how this magic is possible? Read the [technical overview](./overview.md)! If you just want to get started trying Walrus Sites out, check the [tutorial](./tutorial.md). diff --git a/docs/walrus-sites/linking.md b/docs/walrus-sites/linking.md index 58dc947d..7644eca8 100644 --- a/docs/walrus-sites/linking.md +++ b/docs/walrus-sites/linking.md @@ -19,7 +19,7 @@ Here is the part that is a bit different. Assume there is some image that you ca Recall that, however, `https://walrus.site` is just one of the possibly many portals. I.e., the same resource is browsable from a local portal (`http://gallery.localhost:8080/walrus_arctic.webp`), or -from any other portal (e.g., `https://gallery.myotherportal.com/walrus_arctic.webp`). Therefore, how +from any other portal (e.g., `https://gallery.blob.store/walrus_arctic.webp`). Therefore, how can you link the resource in a *portal-independent way*? This is important for interoperability, availability, and respecting the user's choice of portal. diff --git a/docs/walrus-sites/overview.md b/docs/walrus-sites/overview.md index 375af310..61a6fa1b 100644 --- a/docs/walrus-sites/overview.md +++ b/docs/walrus-sites/overview.md @@ -50,7 +50,7 @@ This struct just holds the string of the path (`/index.html`); having a separate we will not have namespace collisions with other dynamic fields, possibly added by other packages. To see this in action, look at [a Walrus Site in the -explorer](https://suiscan.xyz/testnet/object/0x049b6d3f34789904efcc20254400b7dca5548ee35cd7b5b145a211f85b2532fa), +explorer](https://suiscan.xyz/testnet/object/0xd20b90149409ba5d005d4a2cd981db9494bc3cdb2f04c47ca1af98dd8f71610a), and check its dynamic fields. ### The site rendering path @@ -86,7 +86,7 @@ uniquely associated to the object ID of the Walrus Site through SuiNS. Walrus Sites also work without SuiNS: a site can *always* be browsed by using as subdomain the Base36 encoding of the Sui object ID of the site. For the Flatland dApp, this URL is: -. +. Base36 was chosen for two reasons, forced by the subdomain standards: diff --git a/docs/walrus-sites/redirects.md b/docs/walrus-sites/redirects.md index 871d4789..2591d6ca 100644 --- a/docs/walrus-sites/redirects.md +++ b/docs/walrus-sites/redirects.md @@ -30,7 +30,7 @@ display.add(b"walrus site address".to_string(), VISUALIZATION_SITE.to_string()); ... ``` -### How to personalize based on the NFT? +### How to change the site based on the NFT? The code above will only open the specified Walrus Site when browsing the object ID of the NFT. How do we ensure that the properties of the NFT can be used to personalize the site? @@ -39,3 +39,6 @@ This needs to be done in the `VISUALIZATION_SITE`: Since the subdomain is still NFT's object ID, the Walrus Site that is loaded can check its `origin` in JavaScript, and use the subdomain to determine the NFT, fetch it from chain, and use its internal fields to modify the displayed site. + +For an end-to-end example, see the `flatland` +[repo](https://github.com/MystenLabs/example-walrus-sites/tree/main/flatland). diff --git a/docs/walrus-sites/tutorial-migration.md b/docs/walrus-sites/tutorial-migration.md index 1668c46e..480a18cb 100644 --- a/docs/walrus-sites/tutorial-migration.md +++ b/docs/walrus-sites/tutorial-migration.md @@ -9,6 +9,11 @@ The migration will result in a new Site object on Sui (with a different object I objects on Walrus Testnet. ``` +``` admonish danger title="New default configuration path" +The default configuration file for the `site-builder` has been moved to the `./sites-config.yaml` +instead of the old `./site-builder/assets/builder-example.yaml`. +``` + The steps are the following: - Get the latest version of the `walrus` binary, as well as the latest Walrus configuration file, diff --git a/docs/walrus-sites/tutorial-publish.md b/docs/walrus-sites/tutorial-publish.md index 0a1573fd..f4368a2e 100644 --- a/docs/walrus-sites/tutorial-publish.md +++ b/docs/walrus-sites/tutorial-publish.md @@ -18,22 +18,34 @@ Since we have placed the `walrus` binary and configuration in their default loca the `./examples/snake` site is as simple as calling the publishing command: ``` sh -./target/release/site-builder --config site-builder/assets/builder-example.yaml publish ./examples/snake +./target/release/site-builder publish ./examples/snake --epochs 100 ``` -The output should look like the following: +``` admonish tip +Currently on Walrus testnet, the duration of an epoch is 1 day. If you want your site to stay up +longer, specify the number of epochs with the `--epochs` flag! +``` + +```admonish note +The site builder will look for the default configuration file `sites-config.yaml` in the +`./walrus-sites` directory. In case you are calling the `site-builder` command from a different +location, use the `--config` flag to specify the path to the configuration file. +``` + +The end of the output should look like the following: ``` txt -Operations performed: -- created resource /Oi-Regular.ttf with blob ID 2YLU3Usb-WoJAgoNSZUNAFnmyo8cfV8hJYt2YdHL2Hs -- created resource /file.png with blob ID R584P82qm4Dn8LoQMlzkGZS9IAkU0lNZTVlruOsUyOs -- created resource /index.html with blob ID SSzbpPfO2Tqk6xNyF1i-NG9I9CjUjuWnhUATVSs5nic -- created resource /walrus.png with blob ID SGrrw5NQyFWtqtxzLAQ1tLpcChGc0VNbtFRhfsQPuiM +Execution completed +Resource operations performed: + - created resource /Oi-Regular.ttf with blob ID 76npyqDyGF10-jP_ov-UBHpi-RaRFnxcWgslueGEfr0 + - created resource /file.svg with blob ID w70pYgtLmi--38Jg1sTGaLlZkQtximNMHXjxDQdXKa0 + - created resource /index.html with blob ID LVLk9VSnBrEgQ2HJHAgU3p8IarKypQpfn38aSeUZzzE + - created resource /walrus.svg with blob ID 866UDjMAy_BB8SsTcgjGEOFp2uAO9BbcVbLh5-_oBNE +The site routes were modified Created new site: test site -New site object ID: 0x5ac988828a0c9842d91e6d5bdd9552ec9fcdddf11c56bf82dff6d5566685a31e - -Browse the resulting site at: https://29gjzk8yjl1v7zm2etee1siyzaqfj9jaru5ufs6yyh1yqsgun2.walrus.site +New site object ID: 0x407a308190eb82b266be9cc28b888d04c5b2e5a503c7d0ffd3f69681ea83b73a +Browse the resulting site at: https://1lupgq2auevjruy7hs9z7tskqwjp5cc8c5ebhci4v57qyl4piy.walrus.site ``` This output tells you that, for each file in the folder, a new Walrus blob was created, and the @@ -41,9 +53,9 @@ respective blob ID. Further, it prints the object ID of the Walrus Site object o have a look in the explorer and use it to set the SuiNS name) and, finally, the URL at which you can browse the site. -Note here that we are passing the example config `assets/builder-example.yaml` as the config for the -site builder. The configuration file is necessary to ensure that the `site-builder` knows the -correct Sui package for the Walrus Sites logic. +Note here that we are passing the default config `./sites-config.yaml` as the config for the site +builder. The configuration file is necessary to ensure that the `site-builder` knows the correct Sui +package for the Walrus Sites logic. More details on the configuration of the `site-builder` can be found under the [advanced configuration](./builder-config.md) section. @@ -57,22 +69,23 @@ First, make this edit on in the `./examples/snake/index.html` file. Then, you can update the existing site by running the `update` command, providing the directory where to find the updated files (still `./example/snake`) and the object ID of the existing site -(`0x5ac988...`): +(`0x407a3081...`): ``` sh -./target/release/site-builder --config site-builder/assets/builder-example.yaml update ./examples/snake 0x5ac9888... +./target/release/site-builder update --epochs 100 examples/snake 0x407a3081... ``` The output this time should be: ``` txt -Operations performed: - - deleted resource /index.html with blob ID SSzbpPfO2Tqk6xNyF1i-NG9I9CjUjuWnhUATVSs5nic - - created resource /index.html with blob ID LXtY0VdY5kM-3Ph7gLvj8URdz5yiRa5DUy3ZxYqDView - -Updated site at object ID: 0x5ac988828a0c9842d91e6d5bdd9552ec9fcdddf11c56bf82dff6d5566685a31e - -Browse the resulting site at: https://29gjzk8yjl1v7zm2etee1siyzaqfj9jaru5ufs6yyh1yqsgun2.walrus.site +Execution completed +Resource operations performed: + - deleted resource /index.html with blob ID LVLk9VSnBrEgQ2HJHAgU3p8IarKypQpfn38aSeUZzzE + - created resource /index.html with blob ID pcZaosgEFtmP2d2IV3QdVhnUjajvQzY2ev8d9U_D5VY +The site routes were left unchanged + +Site object ID: 0x407a308190eb82b266be9cc28b888d04c5b2e5a503c7d0ffd3f69681ea83b73a +Browse the resulting site at: https://1lupgq2auevjruy7hs9z7tskqwjp5cc8c5ebhci4v57qyl4piy.walrus.site ``` Compared to the `publish` action, we can see that now the only actions performed were to delete the diff --git a/docs/walrus-sites/tutorial-suins.md b/docs/walrus-sites/tutorial-suins.md index 5d059d60..685a45b1 100644 --- a/docs/walrus-sites/tutorial-suins.md +++ b/docs/walrus-sites/tutorial-suins.md @@ -1,6 +1,6 @@ # Bonus: Set a SuiNS name -Browsing a URL like `https://29gjzk8yjl1v7zm2etee1siyzaqfj9jaru5ufs6yyh1yqsgun2.walrus.site` is not +Browsing a URL like `https://1lupgq2auevjruy7hs9z7tskqwjp5cc8c5ebhci4v57qyl4piy.walrus.site` is not particularly nice. Therefore, Walrus Sites allows to use SuiNS names (this is like DNS for Sui) to assign a human-readable name to a Walrus Site. To do so, you simply have to get a SuiNS name you like, and point it to the object ID of the Walrus Site (as provided by the `publish` or `update` @@ -26,64 +26,3 @@ the "three dots" menu icon above the name you want to map, and click "Link To Wa in the bar the object ID of the Walrus Site, check that it is correct, and click "Apply". After approving the transaction, we can now browse ! - ---- - -## The CLI way - -For completeness, we report here a manual way of setting the mapping between the SuiNS name and the -Walrus Site, using the CLI. - -### Get the SuiNS object ID - -Go to the ["names you own"](https://testnet.suins.io/account/my-names) section of the SuiNS website, -click the three-dots menu on the top-right corner of the name, choose "View all info", and copy the -`ObjectID`. In our case, this is `0x6412...`. - -### Send the SuiNS registration object to the address you use with the Sui CLI - -The steps that follow require that the SuiNS registration object is owned by the address you are -using on the Sui CLI. Therefore, unless you use the same address in your browser wallet and the CLI, -we need to send this registration object from the address you use in your browser wallet to the -address of your Sui CLI. - -To find the Sui CLI address, execute: - -``` sh -sui client active-address -``` - -Then, from your browser wallet, select the "Assets" tab and look for the NFT of the SuiNS -registration, which should look as follows: -![the SuiNS registration inside the wallet](../assets/suins-asset.png) - -Click on it, scroll down to "Send NFT", and send it to the address discovered with the command -above. Now, your Sui CLI address owns the registration NFT, and you can proceed to the next step. - -### Use the CLI to map the SuiNS name to the Walrus Site - -This step associates the name `walrusgame` to the object ID of our Walrus Site. There are possibly -many ways to achieve this, one is to issue the following transaction using the Sui CLI to create -this mapping: - -```sh -SUINS_UTILS_PACKAGE=0x7954ae683314ec7e156acbf0c0fc964ce035fd7f456fe7576848226502cfde1b -SUINS_CORE_OBJECT=0x300369e8909b9a6464da265b9a5a9ab6fe2158a040e84e808628cde7a07ee5a3 -MY_SUINS_REGISTRATION_OBJECT=0x6412... # adjust this to your own SuiNS object -MY_WALRUS_SITE_OBJECT=0x5ac9... # adjust this to your Walrus Site object -sui client call \ - --package $SUINS_UTILS_PACKAGE \ - --module direct_setup \ - --function set_target_address \ - --gas-budget 500000000 \ - --args $SUINS_CORE_OBJECT \ - --args $MY_SUINS_REGISTRATION_OBJECT \ - --args "[$MY_WALRUS_SITE_OBJECT]" \ - --args 0x6 -``` - -```admonish note -Note that the SuiNS package and object on Testnet may change. You can find the latest ones by -looking at the `TESTNET_CONFIG` in the [SuiNS -contract](https://github.com/MystenLabs/suins-contracts/blob/main/sdk/src/constants.ts). -``` From 67c1c2d6a2cee1e1c83ffbd8d6cd9387aa889c44 Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 17 Oct 2024 14:29:27 +0200 Subject: [PATCH 47/50] mention `generate-sui-wallet` command --- docs/usage/setup.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/usage/setup.md b/docs/usage/setup.md index 33368f39..7b9fab61 100644 --- a/docs/usage/setup.md +++ b/docs/usage/setup.md @@ -6,12 +6,17 @@ patterns (see [the next chapter](./interacting.md)). This chapter describes the [prerequisites](#prerequisites), [installation](#installation), and [configuration](#configuration) of the Walrus client. -## Prerequisites +## Prerequisites: Sui wallet and Testnet SUI {#prerequisites} - +```admonish tip title="Quick wallet setup" +If you just want to set up a new SUI wallet for Walrus, you can skip this section and use the +`walrus generate-sui-wallet` command after [installing Walrus](#installation). In that case, make +sure to set the `wallet_config` parameter in the [Walrus +configuration](#advanced-configuration-optional) to the newly generated wallet. +``` Interacting with Walrus requires a valid Sui Testnet wallet with some amount of SUI tokens. The -easiest way to set this up is via the Sui CLI; see the [installation +normal way to set this up is via the Sui CLI; see the [installation instructions](https://docs.sui.io/guides/developer/getting-started/sui-install) in the Sui documentation. From a0d20ce15bb6484682224e2aa86772f9247f9c2c Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 17 Oct 2024 14:33:39 +0200 Subject: [PATCH 48/50] clean up TODOs --- docs/dev-guide/sui-struct.md | 2 +- docs/operator-guide/aggregator.md | 2 +- docs/operator-guide/storage-node.md | 2 +- docs/usage/stake.md | 11 +++-------- examples/javascript/blob_upload_download_webapi.html | 2 -- 5 files changed, 6 insertions(+), 13 deletions(-) diff --git a/docs/dev-guide/sui-struct.md b/docs/dev-guide/sui-struct.md index 45221e9f..238f6917 100644 --- a/docs/dev-guide/sui-struct.md +++ b/docs/dev-guide/sui-struct.md @@ -191,4 +191,4 @@ public struct BlsCommitteeMember has store, copy, drop { ``` - + diff --git a/docs/operator-guide/aggregator.md b/docs/operator-guide/aggregator.md index 6f23c792..452fe853 100644 --- a/docs/operator-guide/aggregator.md +++ b/docs/operator-guide/aggregator.md @@ -1,5 +1,5 @@ # Operating an aggregator - + Below is an example of an aggregator node which hosts a HTTP endpoint that can be used to fetch data from Walrus over the web. diff --git a/docs/operator-guide/storage-node.md b/docs/operator-guide/storage-node.md index 815f6fcb..d9eb536b 100644 --- a/docs/operator-guide/storage-node.md +++ b/docs/operator-guide/storage-node.md @@ -1,5 +1,5 @@ # Operating a storage node - + The binary of the storage node is not yet publicly available. Prior to official network launch the code will be open-sourced. diff --git a/docs/usage/stake.md b/docs/usage/stake.md index c9c354fb..e10491bd 100644 --- a/docs/usage/stake.md +++ b/docs/usage/stake.md @@ -63,15 +63,10 @@ You can exchange your Testnet SUI to WAL using the dApp ### Unstake -- Find the `Staked Wal` you want to unstake - - Below the `Current Committee` list you will find all your `Staked Wal` +- Find the `Staked WAL` you want to unstake + - Below the `Current Committee` list you will find all your `Staked WAL` - Also you can expand a Storage Node and find all your stakes with that node -- Depending on the state of the `Staked Wal` you will be able to Unstake or Withdraw your funds +- Depending on the state of the `Staked WAL` you will be able to unstake or Withdraw your funds - Click the `Unstake` or `Withdraw` button - Click continue to confirm your action - (Follow the instructions in your wallet to approve the transaction) - - - -- How to monitor nodes for stake / apr etc -- Move contracts to stake / unstake diff --git a/examples/javascript/blob_upload_download_webapi.html b/examples/javascript/blob_upload_download_webapi.html index 30910ca1..12a2fcb3 100644 --- a/examples/javascript/blob_upload_download_webapi.html +++ b/examples/javascript/blob_upload_download_webapi.html @@ -212,7 +212,6 @@

Blob Upload

- @@ -222,7 +221,6 @@

Blob Upload

- From 6272bb4c5f6bcdb627c49fb887481a3a0701c052 Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 17 Oct 2024 14:40:39 +0200 Subject: [PATCH 49/50] minor edits on staking page --- docs/usage/stake.md | 68 +++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/docs/usage/stake.md b/docs/usage/stake.md index e10491bd..a24e373a 100644 --- a/docs/usage/stake.md +++ b/docs/usage/stake.md @@ -6,10 +6,10 @@ Shards are assigned to storage nodes every epoch, roughly proportional to the am that was delegated to them. By staking with a storage node, users also earn rewards, as they will receive a share of the storage fees. -Since moving shards from one storage node to another requires transferring a lot of data and +Since moving shards from one storage node to another requires transferring a lot of data, and storage nodes potentially need to expand their storage capacity, the selection of the committee for the next epoch is done ahead of time, in the middle of the previous epoch. This provides -sufficient time to storage node operators to provision additional resources, if needed. +sufficient time to storage-node operators to provision additional resources, if needed. For stake to affect the shard distribution in epoch `e` and become "active", it must be staked before the committee for this epoch has been selected, meaning that it has to be staked before @@ -20,53 +20,49 @@ that epoch. Unstaking has a similar delay: because unstaking funds only has an effect on the committee in the next committee selection, the stake will remain active until that committee takes over. This means that, to unstake at the start of epoch `e`, the user needs to "request withdrawal" -before the midpoint of epoch `e - 1`. Otherwise, i.e., if the user unstakes after this point, +before the midpoint of epoch `e - 1`. Otherwise, that is, if the user unstakes after this point, the stake will remain active, and continue to accrue rewards, throughout epoch `e`, and the balance and rewards will be available to withdraw at the start of epoch `e + 1`. -## How to stake +## How to stake with the Walrus Staking dApp -### Walrus Staking dApp +The Walrus Staking dApp allows users to stake (or unstake) to any of the storage nodes of the +system. -The Walrus Staking dApp allows users to stake (or unstake) to any of the storage nodes of the system +To use the dApp, visit and connect your wallet: -### How to use the dApp - -- Visit [https://stake.walrus.site](https://stake.walrus.site) -- Connect your wallet - - Click the `Connect Wallet` button at the top right corner - - Select the wallet (if the wallet was connected before this and the next step wont be required) - - Approve the connection - - (Make sure the selected wallet network is Testnet) +- Click the `Connect Wallet` button at the top right corner. +- Select the wallet (if the wallet was connected before, this and the next step won't be required). +- Approve the connection. +- (Make sure the selected wallet network is Testnet). ### Exchange Testnet SUI to WAL -To be able to stake you will need to have WAL in your wallet. -You can exchange your Testnet SUI to WAL using the dApp +To be able to stake you will need to have Testnet WAL in your wallet. +You can exchange your Testnet SUI to WAL using the dApp as follows: -- Click the `Get WAL` button -- Select the amount of SUI -- And click `Exchange` -- (Follow the instructions in your wallet to approve the transaction) +- Click the `Get WAL` button. +- Select the amount of SUI. This will be exchanged to WAL at a 1:1 rate. +- And click `Exchange`. +- *Follow the instructions in your wallet to approve the transaction.* ### Stake -- Find the Storage node that you want to stake to - - Below the system stats there is the list of the `Current Committee` of storage nodes - - You can select one of the nodes in that list - - or if the storage node is not in the current committee - - find all the Storage nodes at the bottom of the page -- Once you selected the Storage node click the stake button -- Select the amount of WAL -- Click Stake -- (Follow the instructions in your wallet to approve the transaction) +- Find the storage node that you want to stake to. + - Below the system stats, there is the list of the "Current Committee" of storage nodes. + - You can select one of the nodes in that list or, if the storage node is not in the current + committee, you find all the storage nodes at the bottom of the page. +- Once you selected the storage node, click the stake button. +- Select the amount of WAL. +- Click Stake. +- *Follow the instructions in your wallet to approve the transaction.* ### Unstake -- Find the `Staked WAL` you want to unstake - - Below the `Current Committee` list you will find all your `Staked WAL` - - Also you can expand a Storage Node and find all your stakes with that node -- Depending on the state of the `Staked WAL` you will be able to unstake or Withdraw your funds -- Click the `Unstake` or `Withdraw` button -- Click continue to confirm your action -- (Follow the instructions in your wallet to approve the transaction) +- Find the `Staked WAL` you want to unstake. + - Below the "Current Committee" list you will find all your `Staked WAL`. + - Also you can expand a storage node and find all your stakes with that node. +- Depending on the state of the `Staked WAL` you will be able to unstake or Withdraw your funds. +- Click the `Unstake` or `Withdraw` button. +- Click continue to confirm your action. +- *Follow the instructions in your wallet to approve the transaction.* From 84b4e52c9be047c4cce4dadc38087d7786109663 Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Thu, 17 Oct 2024 14:44:40 +0200 Subject: [PATCH 50/50] add nodeinfra agg and pub --- docs/usage/web-api.md | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/docs/usage/web-api.md b/docs/usage/web-api.md index 5a045dfd..7f065bdd 100644 --- a/docs/usage/web-api.md +++ b/docs/usage/web-api.md @@ -59,6 +59,7 @@ may still be temporarily unavailable: - `https://walrus-testnet-aggregator.stakin-nodes.com` - `https://testnet-aggregator-walrus.kiliglab.io` - `https://walrus-cache-testnet.latitude-sui.com` +- `https://walrus-testnet-aggregator.nodeinfra.com` - `https://walrus-tn.juicystake.io:9443` - `https://walrus-agg-testnet.chainode.tech:9002` - `https://walrus-testnet-aggregator.starduststaking.com:11444` @@ -70,11 +71,6 @@ may still be temporarily unavailable: - `http://walrus-testnet.stakingdefenseleague.com:9000` - `http://walrus.sui.thepassivetrust.com:9000` - - ### Public publishers - `https://publisher.walrus-testnet.walrus.space` @@ -84,6 +80,7 @@ Reported but currently not available: - `https://sui-walrus-testnet.bwarelabs.com/publisher` - `https://walrus-testnet-publisher.stakin-nodes.com` - `https://testnet-publisher-walrus.kiliglab.io` +- `https://walrus-testnet-publisher.nodeinfra.com` - `https://walrus-testnet.blockscope.net:11444` - `https://walrus-publish-testnet.chainode.tech:9003` - `https://walrus-testnet-publisher.starduststaking.com:11445` @@ -98,11 +95,6 @@ Reported but currently not available: - `http://walrus-testnet.stakingdefenseleague.com:9001` - `http://walrus.sui.thepassivetrust.com:9001` - - ## HTTP API Usage For the following examples, we assume you set the `AGGREGATOR` and `PUBLISHER` environment variables