[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:
parent
96cb221a3a
commit
a7383a3648
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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(¤tDigest) == 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>) {
|
||||
|
|
|
@ -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
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue