[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
This commit is contained in:
optke3 2023-06-28 10:09:50 -05:00 committed by GitHub
parent 96cb221a3a
commit a7383a3648
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1118 additions and 492 deletions

30
.github/workflows/sui-contract.yml vendored Normal file
View File

@ -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

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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>) {

View File

@ -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
}

File diff suppressed because it is too large Load Diff

View File

@ -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
}
}

View File

@ -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,