Skip to content

Commit

Permalink
Add tiered-whitelist-merkletree
Browse files Browse the repository at this point in the history
  • Loading branch information
MightOfOaks committed Sep 16, 2024
1 parent ecd2958 commit e360893
Show file tree
Hide file tree
Showing 31 changed files with 1,891 additions and 0 deletions.
22 changes: 22 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[alias]
wasm = "build --release --lib --target wasm32-unknown-unknown"
unit-test = "test --lib"
schema = "run --bin schema"
11 changes: 11 additions & 0 deletions contracts/whitelists/tiered-whitelist-merkletree/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
root = true

[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.rs]
indent_size = 4
15 changes: 15 additions & 0 deletions contracts/whitelists/tiered-whitelist-merkletree/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Build results
/target

# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327)
.cargo-ok

# Text file backups
**/*.rs.bk

# macOS
.DS_Store

# IDEs
*.iml
.idea
45 changes: 45 additions & 0 deletions contracts/whitelists/tiered-whitelist-merkletree/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
[package]
name = "tiered-whitelist-merkletree"
authors = ["Martin Mo Kromsten <kromsten@pm.me>"]
description = "Stargaze Merkle Tree Whitelist Contract"
version = { workspace = true }
edition = { workspace = true }
homepage = { workspace = true }
repository = { workspace = true }
license = { workspace = true }

exclude = [
# Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication.
"contract.wasm",
"hash.txt",
]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
crate-type = ["cdylib", "rlib"]

[features]
# for more explicit tests, cargo test --features=backtraces
backtraces = ["cosmwasm-std/backtraces"]
# use library feature to disable all instantiate/execute/query exports
library = []

[dependencies]
cosmwasm-schema = { workspace = true }
cosmwasm-std = { workspace = true }
cw2 = { workspace = true }
cw-storage-plus = { workspace = true }
cw-utils = { workspace = true }
rust_decimal = { version = "1.14.3" }
schemars = { workspace = true }
serde = { workspace = true }
sg1 = { workspace = true }
sg-std = { workspace = true }
thiserror = { workspace = true }
url = { workspace = true }
hex = "0.4.3"
serde_json = "1.0.105"
rs_merkle = { version = "1.4.1", default-features = false }
semver = { workspace = true }

26 changes: 26 additions & 0 deletions contracts/whitelists/tiered-whitelist-merkletree/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Whitelist MerkleTree contract

A whitelist contract that relies on MerkleTree data structure for verifying inclusion of an address in a whitelist.

Only merkle root (and optionaly URI of a tree) are stored within the state. Inclusion can be verified by submitting a user address and hex-encoded list of merklee proofs. This approach allows significant reduction of gas usage during storage phase with a downside of having actual data off-chain and reliance on 3rd parties for providing inclusiong proofs.

Inclusion operation is a slighly more complex and costly when compared to the standard map-based whitelist. The contract uses **Sha256** for hashing concatenated proofs. Hashes are sorted on byte level prior to concatenation, which significantly simplifies the verification process by not requiring submission of leaf positions.

**Important:** Make sure that your algorithm for merkle tree construction also sort the hashes. See example of extending `rs-merkle` library in `tests/hasher.rs`

## Gas Usage

The contracts for the merkletree based whitelist and the updated minter that supports it were both deployed to the testnet to measure actual gas usage in production. The contracts were instantiated and tested with two different whitelist sizes: **703** and **91,750,400** entries

#### Instantiating
Naturally due to only needing to store a merkle tree root in the state of the contract there is no difference between instantiating a whitelist with the [smaller](https://testnet-explorer.publicawesome.dev/stargaze/tx/07BB768915A24C17C12982D3FE34ADF0453AA9231961197A8B4E5E228D5C6B54) and the [bigger](https://testnet-explorer.publicawesome.dev/stargaze/tx/14E2DFB03AFB2A711A6AF601FA43FAEADFC8D0BA8581DD9E02EEFFB582E8AFB7) list sizes and they both consume 190,350 units of gas.

#### Minting

Number of hashing operations required to check for inclusion of an address in a merkle tree is at most `Math.ceil[ log₂N ]` and in some cases even smaller depending on the depth of a leaf within a tree.

In case of the smaller tree with 704 records we had to submit 8 hash proofs and an example mint [transaction](https://testnet-explorer.publicawesome.dev/stargaze/tx/8692581537939E09BF5D81594B078436D4224F0944B515A421F096CEE480ECA9) took 635,345 units of gas

The bigger tree with ~90 million records [used](https://testnet-explorer.publicawesome.dev/stargaze/tx/670A76A64F0A64FB1A5077DADDB6C326A9A64B66999215345C47BA3F03265811) 647,448 units of gas and required 24 proofs only (up to 27 with deeper leaves).

The jump from computing 8 to computing 24 proofs (+16) only took additional 8 thousands units of gas. Keep in mind that another increase in 16 proofs allow us to check for inclusion in a tree with 1 trillion addresses.
15 changes: 15 additions & 0 deletions contracts/whitelists/tiered-whitelist-merkletree/rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# stable
newline_style = "unix"
hard_tabs = false
tab_spaces = 4

# unstable... should we require `rustup run nightly cargo fmt` ?
# or just update the style guide when they are stable?
#fn_single_line = true
#format_code_in_doc_comments = true
#overflow_delimited_expr = true
#reorder_impl_items = true
#struct_field_align_threshold = 20
#struct_lit_single_line = true
#report_todo = "Always"

Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ExecuteMsg",
"oneOf": [
{
"type": "object",
"required": [
"update_stage_config"
],
"properties": {
"update_stage_config": {
"$ref": "#/definitions/UpdateStageConfigMsg"
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"update_admins"
],
"properties": {
"update_admins": {
"type": "object",
"required": [
"admins"
],
"properties": {
"admins": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"freeze"
],
"properties": {
"freeze": {
"type": "object",
"additionalProperties": false
}
},
"additionalProperties": false
}
],
"definitions": {
"Coin": {
"type": "object",
"required": [
"amount",
"denom"
],
"properties": {
"amount": {
"$ref": "#/definitions/Uint128"
},
"denom": {
"type": "string"
}
}
},
"Timestamp": {
"description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
"allOf": [
{
"$ref": "#/definitions/Uint64"
}
]
},
"Uint128": {
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
"type": "string"
},
"Uint64": {
"description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
"type": "string"
},
"UpdateStageConfigMsg": {
"type": "object",
"required": [
"stage_id"
],
"properties": {
"end_time": {
"anyOf": [
{
"$ref": "#/definitions/Timestamp"
},
{
"type": "null"
}
]
},
"mint_price": {
"anyOf": [
{
"$ref": "#/definitions/Coin"
},
{
"type": "null"
}
]
},
"name": {
"type": [
"string",
"null"
]
},
"per_address_limit": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"stage_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"start_time": {
"anyOf": [
{
"$ref": "#/definitions/Timestamp"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
}
}
}
Loading

0 comments on commit e360893

Please sign in to comment.