Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sui/implementation with accountant #600

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
cache/
out/
target/
build/

# Ignores development broadcast logs
!/broadcast
Expand Down
28 changes: 28 additions & 0 deletions sui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Sui NTT

The project is structured into three packages:

- [`ntt`](./packages/ntt) (NTT manager)
- [`ntt_common`](./packages_ntt_common)
- [`wormhole_transciever`](./packages/wormhole_transceiver)

In the NTT architecture, the NTT manager implements the token transfer logic, and sends/receives messages via potentially multiple transceivers in a threshold configuration.
Here, we decouple the implementation of the manager from the transceiver, so they don't need to know about each other. Indeed, the `ntt` and `wormhole_transceiver` packages don't depend on each other.

Instead, they communicate via permissioned structs defined in [`ntt_common`](./packages/ntt_common).

## `ntt_common`

The [`ntt_common`](./packages/ntt_common) package contains common struct definitions that are shared between the NTT manager and transceivers.

There are two flavours of these structs: _unpermissioned_ and _permissioned_.

By unpermissioned, we mean that these can be created and destructed by any module.
These define the structs in the wire format, and as such come with (de)serialiser functions too. Holding a value of these types gives no guarantees about the provenance of the data, so they are exclusively used for structuring information. These structs are defined in the [`messages`](./packages/ntt_common/sources/messages) folder.

On the other hand, construction/consumption of permissioned structs is restricted, and thus provide specific gurantees about the information contained within.
The NTT manager sends messages by creating a value of type [`OutboundMessage`](./packages/ntt_common/sources/outbound_message.move), which the transceiver consumes and processes.
In the other direction, the NTT manager receives messages by consuming [`ValidatedTransceiverMessage`](./packages/ntt_common/sources/validated_transceiver_message.move) structs that are created by the transceiver. See the documentation in those modules for more details on the implementation.
These inbound/outbound permissioned structs are passed between the manager and transceivers in a programmable transaction block, meaning that the contracts don't directly call each other. As such, care has been taken to restrict the capabilities of these structs sufficiently to ensure that the client constructing the PTB can not do the wrong thing.

The intention is for a single `ntt_common` module to be deployed, and shared between all NTT manager & transceiver instances.
29 changes: 29 additions & 0 deletions sui/packages/ntt/Move.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[package]
name = "Ntt"
edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move
license = "Apache 2.0"

[dependencies.Sui]
git = "https://github.com/MystenLabs/sui.git"
subdir = "crates/sui-framework/packages/sui-framework"
rev = "framework/testnet"
override = true

[dependencies.Wormhole]
# git = "https://github.com/wormhole-foundation/wormhole.git"
# rev = "sui/mainnet"
# TODO: we're using this fork temporarily which allows us to create VAAs for testing
git = "https://github.com/wormholelabs-xyz/wormhole.git"
rev = "sui/vaa-new-test-only"
subdir = "sui/wormhole"

[dependencies.NttCommon]
local = "../ntt_common"

[addresses]
ntt = "0x0"

[dev-dependencies]

[dev-addresses]
wormhole = "0x10"
7 changes: 7 additions & 0 deletions sui/packages/ntt/sources/auth.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module ntt::auth {
public struct Auth has drop {}

public(package) fun new_auth(): Auth {
Auth {}
}
}
57 changes: 57 additions & 0 deletions sui/packages/ntt/sources/datatypes/mode.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
module ntt::mode {
use wormhole::cursor;

#[error]
const EInvalidMode: vector<u8> =
b"Invalid mode byte in serialized state.";

public enum Mode has copy, store, drop {
Locking,
Burning
}

public fun locking(): Mode {
Mode::Locking
}

public fun burning(): Mode {
Mode::Burning
}

public fun is_locking(mode: &Mode): bool {
match (mode) {
Mode::Locking => true,
_ => false
}
}

public fun is_burning(mode: &Mode): bool {
match (mode) {
Mode::Burning => true,
_ => false
}
}

public fun serialize(mode: Mode): vector<u8> {
match (mode) {
Mode::Locking => vector[0],
Mode::Burning => vector[1]
}
}

public fun take_bytes(cur: &mut cursor::Cursor<u8>): Mode {
let byte = cur.poke();
match (byte) {
0 => Mode::Locking,
1 => Mode::Burning,
_ => abort(EInvalidMode)
}
}

public fun parse(buf: vector<u8>): Mode {
let mut cur = cursor::new(buf);
let mode = take_bytes(&mut cur);
cur.destroy_empty();
mode
}
}
41 changes: 41 additions & 0 deletions sui/packages/ntt/sources/datatypes/peer.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
module ntt::peer {
use wormhole::external_address::ExternalAddress;
use ntt::rate_limit::{Self, RateLimitState};

public struct Peer has store {
address: ExternalAddress,
token_decimals: u8,
inbound_rate_limit: RateLimitState,
}

// NOTE: this is public. There are no assumptions about a `Peer` being created internally.
// Instead, `Peer`s are always looked up from the state object whenever they
// are needed, and write access to the state object is protected.
public fun new(address: ExternalAddress, token_decimals: u8, inbound_limit: u64): Peer {
Peer {
address,
token_decimals,
inbound_rate_limit: rate_limit::new(inbound_limit),
}
}

public fun set_address(peer: &mut Peer, address: ExternalAddress) {
peer.address = address;
}

public fun set_token_decimals(peer: &mut Peer, token_decimals: u8) {
peer.token_decimals = token_decimals;
}

public fun borrow_address(peer: &Peer): &ExternalAddress {
&peer.address
}

public fun get_token_decimals(peer: &Peer): u8 {
peer.token_decimals
}

public fun borrow_inbound_rate_limit_mut(peer: &mut Peer): &mut RateLimitState {
&mut peer.inbound_rate_limit
}
}
129 changes: 129 additions & 0 deletions sui/packages/ntt/sources/inbox.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
module ntt::inbox {
use sui::clock::Clock;
use sui::table::{Self, Table};
use ntt_common::bitmap::{Self, Bitmap};
use ntt_common::ntt_manager_message::NttManagerMessage;

#[error]
const ETransferCannotBeRedeemed: vector<u8>
= b"Transfer cannot be redeemed yet";

#[error]
const ETransferAlreadyRedeemed: vector<u8>
= b"Transfer already redeemed";

// === Inbox ===

/// The inbox.
/// It's a key-value store where the key is a message containing some
/// payload 'K', and the value is the value 'K' along with information about
/// how many transceivers have voted for it, and whether it has been processed yet.
///
/// For security reasons, a mutable reference to an inbox should never be exposed publicly.
/// This is because priviliged functions are gated by a mutable reference to the inbox.
///
/// In practice, 'K' is instantiated with NativeTokenTransfer, but is written generically
/// here to make reasoning about the code easier (as the inbox doesn't care
/// about the things it stores)
public struct Inbox<K: store + copy + drop> has store {
entries: Table<InboxKey<K>, InboxItem<K>>
}

public fun new<K: store + copy + drop>(ctx: &mut TxContext): Inbox<K> {
Inbox {
entries: table::new(ctx),
}
}

// === Inbox key ===

/// The inbox key is a message from a chain. We include the entire ntt
/// manager message here, not just the payload 'K', because there might be
/// multiple messages with the same content, so the manager message metadata
/// (message id in particular) helps disambiguate. Similarly, manager
/// message ids are only locally unique (per chain), so we also include the
/// origin chain in the key.
///
/// By having transceivers `vote` on messages keyed by the message content,
/// we guarantee that when a particular message receives two votes, both of
/// those votes are actually for the exact same message.
public struct InboxKey<K> has store, copy, drop {
chain_id: u16,
message: NttManagerMessage<K>
}

/// A public constructor for inbox key.
/// No action is privileged by holding an `InboxKey`, so it's safe to make
/// its constructor public.
public fun new_inbox_key<K>(
chain_id: u16,
message: NttManagerMessage<K>
): InboxKey<K> {
InboxKey {
chain_id,
message
}
}

// === Inbox item ===

public struct InboxItem<K> has store {
votes: Bitmap,
release_status: ReleaseStatus,
data: K,
}

public enum ReleaseStatus has copy, drop, store {
NotApproved,
ReleaseAfter(u64),
Released,
}

public fun count_enabled_votes<K>(self: &InboxItem<K>, enabled: &Bitmap): u8 {
let both = self.votes.and(enabled);
both.count_ones()
}


public fun try_release<K>(inbox_item: &mut InboxItem<K>, clock: &Clock): bool {
match (inbox_item.release_status) {
ReleaseStatus::NotApproved => false,
ReleaseStatus::ReleaseAfter(release_timestamp) => {
if (release_timestamp <= clock.timestamp_ms()) {
inbox_item.release_status = ReleaseStatus::Released;
true
} else {
false
}
},
ReleaseStatus::Released => abort ETransferAlreadyRedeemed
}
}

public fun release_after<K>(inbox_item: &mut InboxItem<K>, release_timestamp: u64) {
if (inbox_item.release_status != ReleaseStatus::NotApproved) {
abort ETransferCannotBeRedeemed
};
inbox_item.release_status = ReleaseStatus::ReleaseAfter(release_timestamp);
}

public fun vote<K: store + copy + drop>(inbox: &mut Inbox<K>, transceiver_index: u8, entry: InboxKey<K>) {
let (_, _, data) = entry.message.destruct();
if (!inbox.entries.contains(entry)) {
inbox.entries.add(entry, InboxItem {
votes: bitmap::empty(),
release_status: ReleaseStatus::NotApproved,
data
})
};
inbox.entries.borrow_mut(entry).votes.enable(transceiver_index);
}

public fun borrow_inbox_item_mut<K: store + copy + drop>(inbox: &mut Inbox<K>, key: InboxKey<K>): &mut InboxItem<K> {
inbox.entries.borrow_mut(key)
}

public fun borrow_inbox_item<K: store + copy + drop>(inbox: &Inbox<K>, key: InboxKey<K>): &InboxItem<K> {
inbox.entries.borrow(key)
}
}
Loading
Loading