Pārlūkot izejas kodu

[sui 11/x] - pyth merkle accumulator (#910)

* merkle tree impl

* - take leftmost 20 bytes in hash
- don't assign output of cursor::take_rest to _, instead just drop it

* push PREFIXes (MERKLE_LEAF_PREFIX, MERKLE_NODE_PREFIX) to front instead of back

* delete testXOR

* test construct merkle tree depth exceeded error

* invalid merkle proof test cases

* comments

* rename failure tests

* simplification for initializing a vector

* fix leafHash bug, add tests for hashLeaf and hashNode

* pyth accumulator start, extract_price_info_from_merkle_proof, parse_price_feed_message

* parse_price_feed_message, parse_and_verify_accumulator_updates

* implementation + debugging for merkle pyth accumulator

* edit merkle tree

* testNodehash

* test hash

* delete prints

* test case for parse and verify TEST_ACCUMULATOR_3_MSGS

* hot potato vector -> authenticated price infos

* refactor - move tests from pyth_accumulator to pyth to avoid dependency cycle

* remove _ from deserializing unused vaa

* add sui-contract.yml for github actions

* AuthenticatedPriceInfos -> AuthenticatedVector

* charge base update fee per call to update_single_price_feed

* add back multiple tests, including test_create_and_update_price_feeds_insufficient_fee, update cache, update cache old update

* test multiple price feed creation and update accumulator

* authenticated_price_infos.move -> authenticated_vector.move

* 5 * single_update_fee

* delete some comments, add accumulator test info

* don't make TEST_VAAS test_only in pyth.move

* remove #[test_only]s

* assert price info object contains correct price feed info

* factor out some constants from accumulator test cases to reduce duplicate code

* add sui-contract.yml file for github actions CI

* more refactor and clean-up

* assert price_info_object_1 is correct in test_create_and_update_price_feeds_with_batch_attestation_success

* removed the parse_and_verify_accumulator_message_with_worm_state entirely, and instead added the helper parse_vaa_bytes_from_accumulator_message

* edit comment

* update comment

* edit sui github ci

* fix for sui-contract.yml

* MINIMUM_SUPPORTED_MINOR_VERSION and MAJOR_VERSION

* remove test_get_price_feed_updates_from_accumulator and  parse_vaa_bytes_from_accumulator_message from pyth_accumulator.move

* test_parse_and_verify_accumulator_updates_with_extra_bytes_at_end_of_message

* sui contract yml update

* use rev to cargo install sui in github actions ci

* cargo install --locked for github CI
optke3 2 gadi atpakaļ
vecāks
revīzija
a7383a3648

+ 30 - 0
.github/workflows/sui-contract.yml

@@ -0,0 +1,30 @@
+on:
+  pull_request:
+    paths:
+      - target_chains/sui/contracts/**
+  push:
+    branches:
+      - main
+    paths:
+      - target_chains/sui/contracts/**
+
+name: Sui Contracts
+
+jobs:
+  sui-tests:
+    name: Sui tests
+    runs-on: ubuntu-latest
+    defaults:
+      run:
+        working-directory: target_chains/sui/contracts/
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Update rust
+        run: rustup update stable
+
+      - name: Install Sui CLI
+        run: cargo install --locked --git https://github.com/MystenLabs/sui.git --rev 09b2081498366df936abae26eea4b2d5cafb2788 sui
+
+      - name: Run tests
+        run: sui move test

+ 65 - 0
target_chains/sui/contracts/sources/authenticated_vector.move

@@ -0,0 +1,65 @@
+/// This class represents a collection of objects wrapped inside of a struct
+/// called AuthenticatedVector. Its constructor is non-public and can only be called
+/// by friend modules, making the creation of new AuthenticatedVector protected.
+module pyth::authenticated_vector {
+    use std::vector;
+
+    friend pyth::pyth;
+
+    // A vector of elements
+    struct AuthenticatedVector<T: copy + drop> has drop {
+        contents: vector<T>
+    }
+
+    // A public destroy function.
+    public fun destroy<T: copy + drop>(vec: AuthenticatedVector<T>){
+        let AuthenticatedVector {contents: _} = vec;
+    }
+
+    // Only certain on-chain functions are allowed to create a new hot potato vector.
+    public(friend) fun new<T: copy + drop>(vec: vector<T>): AuthenticatedVector<T>{
+        AuthenticatedVector {
+            contents: vec
+        }
+    }
+
+    public fun length<T: copy + drop>(vec: &AuthenticatedVector<T>): u64 {
+        vector::length(&vec.contents)
+    }
+
+    public fun is_empty<T: copy + drop>(vec: &AuthenticatedVector<T>): bool {
+        vector::is_empty(&vec.contents)
+    }
+
+    public fun borrow<T: copy + drop>(vec: &AuthenticatedVector<T>, i: u64): &T {
+        vector::borrow<T>(&vec.contents, i)
+    }
+
+    public(friend) fun pop_back<T: copy + drop>(vec: AuthenticatedVector<T>): (T, AuthenticatedVector<T>){
+        let elem = vector::pop_back<T>(&mut vec.contents);
+        return (elem, vec)
+    }
+
+    #[test_only]
+    struct A has copy, drop {
+        a : u64
+    }
+
+    #[test]
+    fun test_authenticated_vector(){
+        let vec_of_a = vector::empty<A>();
+        vector::push_back(&mut vec_of_a, A{a:5});
+        vector::push_back(&mut vec_of_a, A{a:11});
+        vector::push_back(&mut vec_of_a, A{a:23});
+
+        let vec = new<A>(vec_of_a);
+        let (b, vec) = pop_back<A>(vec);
+        assert!(b.a==23, 0);
+        (b, vec) = pop_back<A>(vec);
+        assert!(b.a==11, 0);
+        let (b, vec) = pop_back<A>(vec);
+        assert!(b.a==5, 0);
+
+        destroy<A>(vec);
+    }
+}

+ 0 - 65
target_chains/sui/contracts/sources/hot_potato_vector.move

@@ -1,65 +0,0 @@
-/// This class represents a vector of objects wrapped
-/// inside of a hot potato struct.
-module pyth::hot_potato_vector {
-    use std::vector;
-    const E_EMPTY_HOT_POTATO: u64 = 0;
-
-    friend pyth::pyth;
-
-    // A hot potato containing a vector of elements
-    struct HotPotatoVector<T: copy + drop> {
-        contents: vector<T>
-    }
-
-    // A public destroy function.
-    public fun destroy<T: copy + drop>(hot_potato_vector: HotPotatoVector<T>){
-        let HotPotatoVector {contents: _} = hot_potato_vector;
-    }
-
-    // Only certain on-chain functions are allowed to create a new hot potato vector.
-    public(friend) fun new<T: copy + drop>(vec: vector<T>): HotPotatoVector<T>{
-        HotPotatoVector {
-            contents: vec
-        }
-    }
-
-    public fun length<T: copy + drop>(potato: &HotPotatoVector<T>): u64 {
-        vector::length(&potato.contents)
-    }
-
-    public fun is_empty<T: copy + drop>(potato: &HotPotatoVector<T>): bool {
-        vector::is_empty(&potato.contents)
-    }
-
-    public fun borrow<T: copy + drop>(potato: &HotPotatoVector<T>, i: u64): &T {
-        vector::borrow<T>(&potato.contents, i)
-    }
-
-    public(friend) fun pop_back<T: copy + drop>(hot_potato_vector: HotPotatoVector<T>): (T, HotPotatoVector<T>){
-        let elem = vector::pop_back<T>(&mut hot_potato_vector.contents);
-        return (elem, hot_potato_vector)
-    }
-
-    #[test_only]
-    struct A has copy, drop {
-        a : u64
-    }
-
-    #[test]
-    fun test_hot_potato_vector(){
-        let vec_of_a = vector::empty<A>();
-        vector::push_back(&mut vec_of_a, A{a:5});
-        vector::push_back(&mut vec_of_a, A{a:11});
-        vector::push_back(&mut vec_of_a, A{a:23});
-
-        let hot_potato = new<A>(vec_of_a);
-        let (b, hot_potato) = pop_back<A>(hot_potato);
-        assert!(b.a==23, 0);
-        (b, hot_potato) = pop_back<A>(hot_potato);
-        assert!(b.a==11, 0);
-        let (b, hot_potato) = pop_back<A>(hot_potato);
-        assert!(b.a==5, 0);
-
-        destroy<A>(hot_potato);
-    }
-}

+ 4 - 6
target_chains/sui/contracts/sources/merkle_tree.move

@@ -80,16 +80,14 @@ module pyth::merkle_tree {
     }
 
     // isProofValid returns whether a merkle proof is valid
-    fun isProofValid(
+    public fun isProofValid(
         encodedProof: &mut Cursor<u8>,
         root: Bytes20,
         leafData: vector<u8>,
     ): bool {
-
         let currentDigest: Bytes20 = leafHash(&leafData);
         let proofSize: u8 = deserialize::deserialize_u8(encodedProof);
-        let i: u8 = 0;
-        while (i < proofSize){
+        while (proofSize > 0){
             let siblingDigest: Bytes20 = bytes20::new(
                 deserialize::deserialize_vector(encodedProof, 20)
             );
@@ -98,14 +96,14 @@ module pyth::merkle_tree {
                 currentDigest,
                 siblingDigest
             );
-            i = i + 1;
+            proofSize = proofSize - 1;
         };
         bytes20::data(&currentDigest) == bytes20::data(&root)
     }
 
     // constructProofs constructs a merkle tree and returns the root of the tree as
     // a Bytes20 as well as the vector of encoded proofs
-    fun constructProofs(
+    public fun constructProofs(
         messages: &vector<vector<u8>>,
         depth: u8
     ) : (Bytes20, vector<u8>) {

+ 1 - 1
target_chains/sui/contracts/sources/price_status.move

@@ -13,7 +13,7 @@ module pyth::price_status {
     }
 
     public fun from_u64(status: u64): PriceStatus {
-        assert!(status <= TRADING, 0); // error::invalid_price_status()
+        assert!(status <= TRADING, 0);
         PriceStatus {
             status: status
         }

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 665 - 187
target_chains/sui/contracts/sources/pyth.move


+ 116 - 0
target_chains/sui/contracts/sources/pyth_accumulator.move

@@ -0,0 +1,116 @@
+module pyth::accumulator {
+    use std::vector::{Self};
+    use sui::clock::{Clock, Self};
+    use wormhole::bytes20::{Self, Bytes20};
+    use wormhole::cursor::{Self, Cursor};
+    use pyth::deserialize::{Self};
+    use pyth::price_identifier::{Self};
+    use pyth::price_info::{Self, PriceInfo};
+    use pyth::price_feed::{Self};
+    use pyth::merkle_tree::{Self};
+
+    const PRICE_FEED_MESSAGE_TYPE: u64 = 0;
+    const E_INVALID_UPDATE_DATA: u64 = 245;
+    const E_INVALID_PROOF: u64 = 345;
+    const E_INVALID_WORMHOLE_MESSAGE: u64 = 454;
+    const E_INVALID_ACCUMULATOR_PAYLOAD: u64 = 554;
+
+    const ACCUMULATOR_UPDATE_WORMHOLE_VERIFICATION_MAGIC: u32 = 1096111958;
+    const PYTHNET_ACCUMULATOR_UPDATE_MAGIC: u64 = 1347305813;
+
+    const MINIMUM_SUPPORTED_MINOR_VERSION: u8 = 0;
+    const MAJOR_VERSION: u8 = 1;
+
+    friend pyth::pyth;
+    #[test_only]
+    friend pyth::pyth_tests;
+
+    // parse_and_verify_accumulator_message verifies that the price updates encoded in the
+    // accumulator message (accessed via cursor) belong to the merkle tree defined by the merkle root encoded in
+    // vaa_payload.
+    public(friend) fun parse_and_verify_accumulator_message(cursor: &mut Cursor<u8>, vaa_payload: vector<u8>, clock: &Clock): vector<PriceInfo> {
+        let major = deserialize::deserialize_u8(cursor);
+        assert!(major == MAJOR_VERSION, E_INVALID_ACCUMULATOR_PAYLOAD);
+
+        let minor = deserialize::deserialize_u8(cursor);
+        assert!(minor >= MINIMUM_SUPPORTED_MINOR_VERSION, E_INVALID_ACCUMULATOR_PAYLOAD);
+
+        let trailing_size = deserialize::deserialize_u8(cursor);
+        deserialize::deserialize_vector(cursor, (trailing_size as u64));
+
+        let proof_type = deserialize::deserialize_u8(cursor);
+        assert!(proof_type == 0, E_INVALID_ACCUMULATOR_PAYLOAD);
+
+        // Ignore the vaa in the accumulator message because presumably it has already been verified
+        // and passed to this function as input.
+        let vaa_size = deserialize::deserialize_u16(cursor);
+        deserialize::deserialize_vector(cursor, (vaa_size as u64));
+
+        let merkle_root_hash = parse_accumulator_merkle_root_from_vaa_payload(vaa_payload);
+        parse_and_verify_accumulator_updates(cursor, merkle_root_hash, clock)
+    }
+
+    // parse_accumulator_merkle_root_from_vaa_payload takes in some VAA bytes, verifies that the vaa
+    // corresponds to a merkle update, and finally returns the keccak hash of the merkle root.
+    // Note: this function is adapted from the Aptos Pyth accumulator
+    fun parse_accumulator_merkle_root_from_vaa_payload(message: vector<u8>): Bytes20 {
+        let msg_payload_cursor = cursor::new(message);
+        let payload_type = deserialize::deserialize_u32(&mut msg_payload_cursor);
+        assert!(payload_type == ACCUMULATOR_UPDATE_WORMHOLE_VERIFICATION_MAGIC, E_INVALID_WORMHOLE_MESSAGE);
+        let wh_message_payload_type = deserialize::deserialize_u8(&mut msg_payload_cursor);
+        assert!(wh_message_payload_type == 0, E_INVALID_WORMHOLE_MESSAGE); // Merkle variant
+        let _merkle_root_slot = deserialize::deserialize_u64(&mut msg_payload_cursor);
+        let _merkle_root_ring_size = deserialize::deserialize_u32(&mut msg_payload_cursor);
+        let merkle_root_hash = deserialize::deserialize_vector(&mut msg_payload_cursor, 20);
+        cursor::take_rest<u8>(msg_payload_cursor);
+        bytes20::new(merkle_root_hash)
+    }
+
+    // Note: this parsing function is adapted from the Aptos Pyth parse_price_feed_message function
+    fun parse_price_feed_message(message_cur: &mut Cursor<u8>, clock: &Clock): PriceInfo {
+        let message_type = deserialize::deserialize_u8(message_cur);
+
+        assert!(message_type == 0, 0); // PriceFeedMessage variant
+        let price_identifier = price_identifier::from_byte_vec(deserialize::deserialize_vector(message_cur, 32));
+        let price = deserialize::deserialize_i64(message_cur);
+        let conf = deserialize::deserialize_u64(message_cur);
+        let expo = deserialize::deserialize_i32(message_cur);
+        let publish_time = deserialize::deserialize_u64(message_cur);
+        let _prev_publish_time = deserialize::deserialize_i64(message_cur);
+        let ema_price = deserialize::deserialize_i64(message_cur);
+        let ema_conf = deserialize::deserialize_u64(message_cur);
+        let price_info = price_info::new_price_info(
+            clock::timestamp_ms(clock) / 1000, // not used anywhere kept for backward compatibility
+            clock::timestamp_ms(clock) / 1000,
+            price_feed::new(
+                price_identifier,
+                pyth::price::new(price, conf, expo, publish_time),
+                pyth::price::new(ema_price, ema_conf, expo, publish_time),
+            )
+        );
+        price_info
+    }
+
+    // parse_and_verify_accumulator_updates takes as input a merkle root and cursor over the encoded update message (containing encoded
+    // leafs and merkle proofs), iterates over each leaf/proof pair and verifies it is part of the tree, and finally outputs the set of
+    // decoded and authenticated PriceInfos.
+    fun parse_and_verify_accumulator_updates(cursor: &mut Cursor<u8>, merkle_root: Bytes20, clock: &Clock): vector<PriceInfo> {
+        let update_size = deserialize::deserialize_u8(cursor);
+        let price_info_updates: vector<PriceInfo> = vector[];
+        while (update_size > 0) {
+            let message_size = deserialize::deserialize_u16(cursor);
+            let message = deserialize::deserialize_vector(cursor, (message_size as u64)); //should be safe to go from u16 to u16
+            let message_cur = cursor::new(message);
+            let price_info = parse_price_feed_message(&mut message_cur, clock);
+            cursor::take_rest(message_cur);
+
+            vector::push_back(&mut price_info_updates, price_info);
+
+            // isProofValid pops the next merkle proof from the front of cursor and checks if it proves that message is part of the
+            // merkle tree defined by merkle_root
+            assert!(merkle_tree::isProofValid(cursor, merkle_root, message), E_INVALID_PROOF);
+            update_size = update_size - 1;
+        };
+        price_info_updates
+    }
+}

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

@@ -40,6 +40,11 @@ module pyth::state {
     /// state methods.
     struct LatestOnly has drop {}
 
+    #[test_only]
+    public fun create_latest_only_for_test():LatestOnly {
+        LatestOnly{}
+    }
+
     struct State has key, store {
         id: UID,
         governance_data_source: DataSource,

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels