Эх сурвалжийг харах

[sui 2/x]: `state`, `batch_price_attestation`, `set` (#710)

* state getters and setters, change Move.toml dependency to sui/integration_v2

* finish state.move

* add new line to pyth

* use deployer cap pattern for state module

* indent
optke3 2 жил өмнө
parent
commit
55bc1b95bb

+ 1 - 1
target_chains/sui/contracts/Move.toml

@@ -10,7 +10,7 @@ rev = "157ac72030d014f17d76cefe81f3915b4afab2c9"
 [dependencies.Wormhole]
 git = "https://github.com/wormhole-foundation/wormhole.git"
 subdir = "sui/wormhole"
-rev = "sui/wormhole-cleanup"
+rev = "sui/integration_v2"
 
 [addresses]
 pyth = "0x250"

+ 248 - 0
target_chains/sui/contracts/sources/batch_price_attestation.move

@@ -0,0 +1,248 @@
+module pyth::batch_price_attestation {
+
+    use sui::tx_context::{Self, TxContext};
+
+    use pyth::price_feed::{Self};
+    use pyth::price_info::{Self, PriceInfo};
+    use pyth::price_identifier::{Self};
+    use pyth::price_status;
+    use pyth::deserialize::{Self};
+    // TODO - Import Sui clock and use it for timekeeping instead of tx_context::epoch.
+    //        Replace epoch in deserialize_price_info with sui clock timestamp, and usage
+    //        of epoch in test_deserialize_batch_price_attestation.
+    // TODO - Use specific error messages in this module, specifically
+    //        for invalid_attestation_magic_value and invalid_batch_attestation_header_size.
+    use wormhole::cursor::{Self, Cursor};
+    use wormhole::bytes::{Self};
+
+    use std::vector::{Self};
+
+    #[test_only]
+    use pyth::price;
+    #[test_only]
+    use pyth::i64;
+
+    const MAGIC: u64 = 0x50325748; // "P2WH" (Pyth2Wormhole) raw ASCII bytes
+
+    struct BatchPriceAttestation {
+        header: Header,
+        attestation_size: u64,
+        attestation_count: u64,
+        price_infos: vector<PriceInfo>,
+    }
+
+    struct Header {
+        magic: u64,
+        version_major: u64,
+        version_minor: u64,
+        header_size: u64,
+        payload_id: u8,
+    }
+
+    fun deserialize_header(cur: &mut Cursor<u8>): Header {
+        let magic = (deserialize::deserialize_u32(cur) as u64);
+        assert!(magic == MAGIC, 0); // TODO - add specific error value - error::invalid_attestation_magic_value()
+        let version_major = deserialize::deserialize_u16(cur);
+        let version_minor = deserialize::deserialize_u16(cur);
+        let header_size = deserialize::deserialize_u16(cur);
+        let payload_id = deserialize::deserialize_u8(cur);
+
+        assert!(header_size >= 1, 0); // TODO - add specific error value - error::invalid_batch_attestation_header_size()
+        let unknown_header_bytes = header_size - 1;
+        let _unknown = bytes::take_bytes(cur, (unknown_header_bytes as u64));
+
+        Header {
+            magic: magic,
+            header_size: (header_size as u64),
+            version_minor: (version_minor as u64),
+            version_major: (version_major as u64),
+            payload_id: payload_id,
+        }
+    }
+
+    public fun destroy(batch: BatchPriceAttestation): vector<PriceInfo> {
+        let BatchPriceAttestation {
+            header: Header {
+                magic: _,
+                version_major: _,
+                version_minor: _,
+                header_size: _,
+                payload_id: _,
+            },
+            attestation_size: _,
+            attestation_count: _,
+            price_infos,
+        } = batch;
+        price_infos
+    }
+
+    public fun get_attestation_count(batch: &BatchPriceAttestation): u64 {
+        batch.attestation_count
+    }
+
+    public fun get_price_info(batch: &BatchPriceAttestation, index: u64): &PriceInfo {
+        vector::borrow(&batch.price_infos, index)
+    }
+
+    public fun deserialize(bytes: vector<u8>, ctx: &mut TxContext): BatchPriceAttestation {
+        let cur = cursor::new(bytes);
+        let header = deserialize_header(&mut cur);
+
+        let attestation_count = deserialize::deserialize_u16(&mut cur);
+        let attestation_size = deserialize::deserialize_u16(&mut cur);
+        let price_infos = vector::empty();
+
+        let i = 0;
+        while (i < attestation_count) {
+            let price_info = deserialize_price_info(&mut cur, ctx);
+            vector::push_back(&mut price_infos, price_info);
+
+            // Consume any excess bytes
+            let parsed_bytes = 32+32+8+8+4+8+8+1+4+4+8+8+8+8+8;
+            let _excess = bytes::take_bytes(&mut cur, (attestation_size - parsed_bytes as u64));
+
+            i = i + 1;
+        };
+        cursor::destroy_empty(cur);
+
+        BatchPriceAttestation {
+            header,
+            attestation_count: (attestation_count as u64),
+            attestation_size: (attestation_size as u64),
+            price_infos: price_infos,
+        }
+    }
+
+    fun deserialize_price_info(cur: &mut Cursor<u8>, ctx: &mut TxContext): PriceInfo {
+
+        // Skip obselete field
+        let _product_identifier = deserialize::deserialize_vector(cur, 32);
+        let price_identifier = price_identifier::from_byte_vec(deserialize::deserialize_vector(cur, 32));
+        let price = deserialize::deserialize_i64(cur);
+        let conf = deserialize::deserialize_u64(cur);
+        let expo = deserialize::deserialize_i32(cur);
+        let ema_price = deserialize::deserialize_i64(cur);
+        let ema_conf = deserialize::deserialize_u64(cur);
+        let status = price_status::from_u64((deserialize::deserialize_u8(cur) as u64));
+
+        // Skip obselete fields
+        let _num_publishers = deserialize::deserialize_u32(cur);
+        let _max_num_publishers = deserialize::deserialize_u32(cur);
+
+        let attestation_time = deserialize::deserialize_u64(cur);
+        let publish_time = deserialize::deserialize_u64(cur); //
+        let prev_publish_time = deserialize::deserialize_u64(cur);
+        let prev_price = deserialize::deserialize_i64(cur);
+        let prev_conf = deserialize::deserialize_u64(cur);
+
+        // Handle the case where the status is not trading. This logic will soon be moved into
+        // the attester.
+
+        // If status is trading, use the current price.
+        // If not, use the the last known trading price.
+        let current_price = pyth::price::new(price, conf, expo, publish_time);
+        if (status != price_status::new_trading()) {
+            current_price = pyth::price::new(prev_price, prev_conf, expo, prev_publish_time);
+        };
+
+        // If status is trading, use the timestamp of the aggregate as the timestamp for the
+        // EMA price. If not, the EMA will have last been updated when the aggregate last had
+        // trading status, so use prev_publish_time (the time when the aggregate last had trading status).
+        let ema_timestamp = publish_time;
+        if (status != price_status::new_trading()) {
+            ema_timestamp = prev_publish_time;
+        };
+
+        price_info::new(
+            attestation_time,
+            tx_context::epoch(ctx), //TODO - use Sui Clock to get timestamp in seconds
+            price_feed::new(
+                price_identifier,
+                current_price,
+                pyth::price::new(ema_price, ema_conf, expo, ema_timestamp),
+            )
+        )
+    }
+
+    #[test]
+    #[expected_failure]
+    fun test_deserialize_batch_price_attestation_invalid_magic() {
+        use sui::test_scenario::{Self, ctx};
+        let test = test_scenario::begin(@0x1234);
+
+        // A batch price attestation with a magic number of 0x50325749
+        let bytes = x"5032574900030000000102000400951436e0be37536be96f0896366089506a59763d036728332d3e3038047851aea7c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1000000000000049a0000000000000008fffffffb00000000000005dc0000000000000003000000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000006150000000000000007215258d81468614f6b7e194c5d145609394f67b041e93e6695dcc616faadd0603b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe000000000000041a0000000000000003fffffffb00000000000005cb0000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e4000000000000048600000000000000078ac9cf3ab299af710d735163726fdae0db8465280502eb9f801f74b3c1bd190333832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d00000000000003f20000000000000002fffffffb00000000000005e70000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e40000000000000685000000000000000861db714e9ff987b6fedf00d01f9fea6db7c30632d6fc83b7bc9459d7192bc44a21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db800000000000006cb0000000000000001fffffffb00000000000005e40000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000007970000000000000001";
+        destroy(deserialize(bytes, ctx(&mut test)));
+        test_scenario::end(test);
+    }
+
+    #[test]
+    fun test_deserialize_batch_price_attestation() {
+        use sui::test_scenario::{Self, ctx};
+        // Set the arrival time
+        let test = test_scenario::begin(@0x1234);
+        let arrival_time = tx_context::epoch(ctx(&mut test));
+
+        // A raw batch price attestation
+        // The first attestation has a status of UNKNOWN
+        let bytes = x"5032574800030000000102000400951436e0be37536be96f0896366089506a59763d036728332d3e3038047851aea7c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1000000000000049a0000000000000008fffffffb00000000000005dc0000000000000003000000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000006150000000000000007215258d81468614f6b7e194c5d145609394f67b041e93e6695dcc616faadd0603b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe000000000000041a0000000000000003fffffffb00000000000005cb0000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e4000000000000048600000000000000078ac9cf3ab299af710d735163726fdae0db8465280502eb9f801f74b3c1bd190333832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d00000000000003f20000000000000002fffffffb00000000000005e70000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e40000000000000685000000000000000861db714e9ff987b6fedf00d01f9fea6db7c30632d6fc83b7bc9459d7192bc44a21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db800000000000006cb0000000000000001fffffffb00000000000005e40000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000007970000000000000001";
+
+        let expected = BatchPriceAttestation {
+            header: Header {
+                magic: 0x50325748,
+                version_major: 3,
+                version_minor: 0,
+                payload_id: 2,
+                header_size: 1,
+            },
+            attestation_count: 4,
+            attestation_size: 149,
+            price_infos: vector<PriceInfo>[
+                price_info::new(
+                    1663680747,
+                    arrival_time,
+                    price_feed::new(
+                        price_identifier::from_byte_vec(x"c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1"),
+                        price::new(i64::new(1557, false), 7, i64::new(5, true), 1663680740),
+                        price::new(i64::new(1500, false), 3, i64::new(5, true), 1663680740),
+                    ),
+                ),
+                price_info::new(
+                    1663680747,
+                    arrival_time,
+                    price_feed::new(
+                        price_identifier::from_byte_vec(x"3b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe"),
+                        price::new(i64::new(1050, false), 3, i64::new(5, true), 1663680745),
+                        price::new(i64::new(1483, false), 3, i64::new(5, true), 1663680745),
+                    ),
+                ),
+                price_info::new(
+                    1663680747,
+                    arrival_time,
+                    price_feed::new(
+                        price_identifier::from_byte_vec(x"33832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d"),
+                        price::new(i64::new(1010, false), 2, i64::new(5, true), 1663680745),
+                        price::new(i64::new(1511, false), 3, i64::new(5, true), 1663680745),
+                    ),
+                ),
+                price_info::new(
+                    1663680747,
+                    arrival_time,
+                    price_feed::new(
+                        price_identifier::from_byte_vec(x"21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db8"),
+                        price::new(i64::new(1739, false), 1, i64::new(5, true), 1663680745),
+                        price::new(i64::new(1508, false), 3, i64::new(5, true), 1663680745),
+                    ),
+                ),
+            ],
+        };
+
+        let deserialized = deserialize(bytes, ctx(&mut test));
+
+        assert!(&expected == &deserialized, 1);
+        destroy(expected);
+        destroy(deserialized);
+
+        test_scenario::end(test);
+    }
+}

+ 1 - 0
target_chains/sui/contracts/sources/pyth.move

@@ -0,0 +1 @@
+module pyth::pyth {}

+ 46 - 0
target_chains/sui/contracts/sources/set.move

@@ -0,0 +1,46 @@
+/// A set data structure.
+module pyth::set {
+    use sui::table::{Self, Table};
+    use sui::tx_context::{TxContext};
+    use std::vector;
+
+    /// Empty struct. Used as the value type in mappings to encode a set
+    struct Unit has store, copy, drop {}
+
+    /// A set containing elements of type `A` with support for membership
+    /// checking.
+    struct Set<A: store + copy + drop> has store {
+        keys: vector<A>,
+        elems: Table<A, Unit>
+    }
+
+    /// Create a new Set.
+    public fun new<A: store + copy + drop>(ctx: &mut TxContext): Set<A> {
+        Set {
+            keys: vector::empty<A>(),
+            elems: table::new(ctx),
+        }
+    }
+
+    /// Add a new element to the set.
+    /// Aborts if the element already exists
+    public fun add<A: store + copy + drop>(set: &mut Set<A>, key: A) {
+        table::add(&mut set.elems, key, Unit {});
+        vector::push_back(&mut set.keys, key);
+    }
+
+    /// Returns true iff `set` contains an entry for `key`.
+    public fun contains<A: store + copy + drop>(set: &Set<A>, key: A): bool {
+        table::contains(&set.elems, key)
+    }
+
+    /// Removes all elements from the set
+    public fun empty<A: store + copy + drop>(set: &mut Set<A>) {
+        while (!vector::is_empty(&set.keys)) {
+            table::remove(&mut set.elems, vector::pop_back(&mut set.keys));
+        }
+    }
+
+    // TODO: destroy_empty, but this is tricky because std::table doesn't
+    // have this functionality.
+}

+ 117 - 0
target_chains/sui/contracts/sources/state.move

@@ -0,0 +1,117 @@
+module pyth::state {
+    use std::vector;
+    use sui::object::{Self, UID};
+    use sui::transfer::{Self};
+    use sui::tx_context::{Self, TxContext};
+
+    use pyth::data_source::{DataSource};
+    use pyth::set::{Self, Set};
+
+    friend pyth::pyth;
+
+    /// Capability for creating a bridge state object, granted to sender when this
+    /// module is deployed
+    struct DeployerCap has key, store {
+        id: UID
+    }
+
+    struct State has key {
+        id: UID,
+        // TODO - Make data_sources a dynamic field of State,
+        // inside of something embedded in State, because there will be
+        // 10k+ data sources in the future, and we want to minimize the
+        // size of State.
+        data_sources: Set<DataSource>,
+        governance_data_source: DataSource,
+        last_executed_governance_sequence: u64,
+        stale_price_threshold: u64,
+        base_update_fee: u64
+    }
+
+    fun init(ctx: &mut TxContext) {
+        transfer::transfer(
+            DeployerCap {
+                id: object::new(ctx)
+            },
+            tx_context::sender(ctx)
+        );
+    }
+
+    // Initialization
+    public(friend) fun init_and_share_state(
+        deployer: DeployerCap,
+        stale_price_threshold: u64,
+        base_update_fee: u64,
+        governance_data_source: DataSource,
+        sources: vector<DataSource>,
+        ctx: &mut TxContext
+    ) {
+        let DeployerCap { id } = deployer;
+        object::delete(id);
+
+        // Convert the vector of DataSource objects into a set
+        // of DataSource objects
+        let data_sources = set::new<DataSource>(ctx);
+        while (!vector::is_empty(&sources)) {
+            set::add(&mut data_sources, vector::pop_back(&mut sources));
+        };
+        transfer::share_object(
+            State {
+                id: object::new(ctx),
+                data_sources,
+                governance_data_source,
+                last_executed_governance_sequence: 0,
+                stale_price_threshold,
+                base_update_fee
+            }
+        );
+    }
+
+    // Accessors
+    public fun get_stale_price_threshold_secs(s: &State): u64 {
+        s.stale_price_threshold
+    }
+
+    public fun get_base_update_fee(s: &State): u64 {
+        s.base_update_fee
+    }
+
+    public fun is_valid_data_source(s: &State, data_source: DataSource): bool {
+        set::contains<DataSource>(&s.data_sources, data_source)
+    }
+
+    public fun is_valid_governance_data_source(s: &State, source: DataSource): bool {
+        s.governance_data_source == source
+    }
+
+    public fun get_last_executed_governance_sequence(s: &State): u64 {
+        s.last_executed_governance_sequence
+    }
+
+    // Setters
+    public(friend) fun set_data_sources(s: &mut State, new_sources: vector<DataSource>) {
+        // Empty the existing set of data sources instead of dropping it,
+        // because it does not have drop ability.
+        set::empty<DataSource>(&mut s.data_sources);
+        // Add new sources to state.data_sources.
+        while (!vector::is_empty(&new_sources)) {
+            set::add(&mut s.data_sources, vector::pop_back(&mut new_sources));
+        };
+    }
+
+    public(friend) fun set_last_executed_governance_sequence(s: &mut State, sequence: u64) {
+        s.last_executed_governance_sequence = sequence;
+    }
+
+    public(friend) fun set_governance_data_source(s: &mut State, source: DataSource) {
+        s. governance_data_source = source;
+    }
+
+    public(friend) fun set_base_update_fee(s: &mut State, fee: u64) {
+        s.base_update_fee = fee;
+    }
+
+    public(friend) fun set_stale_price_threshold_secs(s: &mut State, threshold_secs: u64) {
+        s.stale_price_threshold = threshold_secs;
+    }
+}