diff --git a/packages/solana/Anchor.toml b/packages/solana/Anchor.toml index 6311454e..f5c43429 100644 --- a/packages/solana/Anchor.toml +++ b/packages/solana/Anchor.toml @@ -5,8 +5,10 @@ resolution = true skip-lint = false [programs.localnet] +debridge-reporter = "2TvQ6gqQGAifdV2cQ1f8zHGYV2t6wPUNTKzpcALt8rX7" hashi = "EyqiZf8Yt2CgVU5yPsj5e4EiGXeKrLhefWBn7CSqKPMC" help = "EJy8wh5fqP5LYqs1mNbH2S6jxKGkP64PkXRmZgQxk9CE" +reporter = "4wFzP8uwPvvW2kZGxxkL45SP2Wn3oRbfe1LdHheUFZ9k" solana = "4VFxPo4BF53PkVFBFjCQBFUn8bCxZZm6Dc2VgrJ5HXS8" [registry] diff --git a/packages/solana/Cargo.lock b/packages/solana/Cargo.lock index b06b8c95..9de09468 100644 --- a/packages/solana/Cargo.lock +++ b/packages/solana/Cargo.lock @@ -369,6 +369,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.7" @@ -631,6 +637,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg-match" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8100e46ff92eb85bf6dc2930c73f2a4f7176393c84a9446b3d501e1b354e7b34" + [[package]] name = "cfg_aliases" version = "0.2.1" @@ -737,6 +749,66 @@ dependencies = [ "zeroize", ] +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "debridge-reporter" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "debridge-solana-sdk", +] + +[[package]] +name = "debridge-solana-sdk" +version = "1.0.2" +source = "git+ssh://git@github.com/debridge-finance/debridge-solana-sdk.git#e442dd77f2cab1082a7140be3602314567e4b4b6" +dependencies = [ + "borsh 0.9.3", + "cfg-match", + "derive_builder", + "env_to_array", + "hex", + "lazy_static", + "sha3", + "solana-program", + "some-to-err", + "thiserror", +] + [[package]] name = "derivative" version = "2.2.0" @@ -748,6 +820,37 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_builder" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_macro" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" +dependencies = [ + "derive_builder_core", + "syn 1.0.109", +] + [[package]] name = "digest" version = "0.9.0" @@ -774,6 +877,20 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "env_to_array" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "820a716148b32af5a6bc12fd0f06463ba3b461e8e9b40e3000a0b928dceb4f94" +dependencies = [ + "base64 0.13.1", + "bs58 0.4.0", + "hex", + "itertools", + "proc-macro2", + "syn 1.0.109", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -786,6 +903,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "generic-array" version = "0.14.7" @@ -864,6 +987,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hmac" version = "0.8.1" @@ -885,6 +1014,12 @@ dependencies = [ "hmac", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "im" version = "15.1.0" @@ -1587,6 +1722,18 @@ dependencies = [ "syn 2.0.89", ] +[[package]] +name = "some-to-err" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d95fd601e937c6e1fe0f25ef8754008e7bf87e97a8a60a8a60b0a289f7e39ad" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.6.1" diff --git a/packages/solana/programs/debridge-reporter/Cargo.toml b/packages/solana/programs/debridge-reporter/Cargo.toml new file mode 100644 index 00000000..04a2d091 --- /dev/null +++ b/packages/solana/programs/debridge-reporter/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "debridge-reporter" +version = "0.1.0" +description = "Debridge reporter" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "debridge_reporter" + +[features] +default = [] +cpi = ["no-entrypoint"] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +idl-build = ["anchor-lang/idl-build"] + +[dependencies] +anchor-lang = "0.30.1" +debridge-solana-sdk = { git = "ssh://git@github.com/debridge-finance/debridge-solana-sdk.git", version = "1.0.2" } diff --git a/packages/solana/programs/debridge-reporter/Xargo.toml b/packages/solana/programs/debridge-reporter/Xargo.toml new file mode 100644 index 00000000..475fb71e --- /dev/null +++ b/packages/solana/programs/debridge-reporter/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/packages/solana/programs/debridge-reporter/src/contexts/dispatch_slot.rs b/packages/solana/programs/debridge-reporter/src/contexts/dispatch_slot.rs new file mode 100644 index 00000000..0fbefcab --- /dev/null +++ b/packages/solana/programs/debridge-reporter/src/contexts/dispatch_slot.rs @@ -0,0 +1,9 @@ +use anchor_lang::prelude::*; +use anchor_lang::solana_program::sysvar; + +#[derive(Accounts)] +pub struct DispatchSlot<'info> { + /// CHECK: We are reading from SlotHashes sysvar the latest slot hash + #[account(address = sysvar::slot_hashes::ID)] + pub slot_hashes: AccountInfo<'info>, +} diff --git a/packages/solana/programs/debridge-reporter/src/contexts/mod.rs b/packages/solana/programs/debridge-reporter/src/contexts/mod.rs new file mode 100644 index 00000000..ce3e13db --- /dev/null +++ b/packages/solana/programs/debridge-reporter/src/contexts/mod.rs @@ -0,0 +1,3 @@ +pub mod dispatch_slot; + +pub use dispatch_slot::*; diff --git a/packages/solana/programs/debridge-reporter/src/error.rs b/packages/solana/programs/debridge-reporter/src/error.rs new file mode 100644 index 00000000..c0863a9d --- /dev/null +++ b/packages/solana/programs/debridge-reporter/src/error.rs @@ -0,0 +1,13 @@ +use anchor_lang::prelude::*; + +#[error_code] +pub enum ErrorCode { + #[msg("Invalid latest hash hash.")] + InvalidLatestHashLength, + #[msg("Invalid slot hashes sysvar.")] + InvalidSlotHashesSysVar, + #[msg("Slot hashes not available.")] + SlotHashesNotAvailable, + #[msg("Slot not found")] + SlotNotFound, +} diff --git a/packages/solana/programs/debridge-reporter/src/lib.rs b/packages/solana/programs/debridge-reporter/src/lib.rs new file mode 100644 index 00000000..a1ff0262 --- /dev/null +++ b/packages/solana/programs/debridge-reporter/src/lib.rs @@ -0,0 +1,48 @@ +use anchor_lang::prelude::*; +use borsh::{BorshDeserialize, BorshSerialize}; +use debridge_solana_sdk::sending; + +declare_id!("2TvQ6gqQGAifdV2cQ1f8zHGYV2t6wPUNTKzpcALt8rX7"); + +pub mod contexts; +pub mod error; +pub mod utils; + +pub use contexts::*; +pub use utils::{get_slot, u64_to_u8_32}; + +#[derive(BorshSerialize)] +pub struct Message { + pub ids: Vec<[u8; 32]>, + pub hashes: Vec<[u8; 32]>, +} + +#[program] +pub mod debridge_reporter { + use super::*; + + pub fn dispatch_slot( + ctx: Context, + target_chain_id: [u8; 32], + receiver: Vec, + slot_number: u64, + ) -> Result<()> { + let (number, hash) = get_slot(&ctx.accounts.slot_hashes, slot_number)?; + + let ids: Vec<[u8; 32]> = vec![u64_to_u8_32(number)]; + let hashes: Vec<[u8; 32]> = vec![hash]; + let message = Message { ids, hashes }; + + sending::invoke_send_message( + message.try_to_vec()?, + target_chain_id, + receiver, + 0, // execution_fee = 0 means auto claim + vec![0u8; 32], // fallback address + ctx.remaining_accounts, + ) + .map_err(ProgramError::from)?; + + Ok(()) + } +} diff --git a/packages/solana/programs/debridge-reporter/src/utils.rs b/packages/solana/programs/debridge-reporter/src/utils.rs new file mode 100644 index 00000000..89bb4c27 --- /dev/null +++ b/packages/solana/programs/debridge-reporter/src/utils.rs @@ -0,0 +1,46 @@ +use anchor_lang::prelude::*; +use anchor_lang::solana_program::sysvar; + +use crate::error::ErrorCode; + +pub fn get_slot(slot_hashes: &AccountInfo, slot_number: u64) -> Result<(u64, [u8; 32])> { + if *slot_hashes.key != sysvar::slot_hashes::ID { + return Err(error!(ErrorCode::InvalidSlotHashesSysVar)); + } + + let data = slot_hashes.try_borrow_data()?; + let num_slot_hashes = u64::from_le_bytes(data[0..8].try_into().unwrap()); + let mut pos = 8; + + if num_slot_hashes == 0 { + return Err(error!(ErrorCode::SlotHashesNotAvailable)); + } + + for _i in 0..num_slot_hashes { + let current_slot_number = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap()); + pos += 8; + + let current_slot_hash: [u8; 32] = match &data[pos..pos + 32].try_into() { + Ok(hash) => *hash, + Err(_) => return Err(error!(ErrorCode::InvalidLatestHashLength)), + }; + + if current_slot_number == slot_number { + return Ok((current_slot_number, current_slot_hash)); + } + + pos += 32; + } + + Err(error!(ErrorCode::SlotNotFound)) +} + +pub fn u64_to_u8_32(number: u64) -> [u8; 32] { + let mut bytes = [0u8; 32]; + let number_bytes = number.to_be_bytes(); // Convert u64 to a big-endian 8-byte array + + // Copy the 8 bytes of the u64 into the last 8 bytes of the 32-byte array + bytes[24..].copy_from_slice(&number_bytes); + + bytes +} diff --git a/packages/solana/tests/debridge-reporter.ts b/packages/solana/tests/debridge-reporter.ts new file mode 100644 index 00000000..6027c141 --- /dev/null +++ b/packages/solana/tests/debridge-reporter.ts @@ -0,0 +1,126 @@ +/*import * as anchor from "@coral-xyz/anchor" +import { Program } from "@coral-xyz/anchor" +import { AccountMeta, PublicKey, SYSVAR_SLOT_HASHES_PUBKEY } from "@solana/web3.js" +// import { expect } from "chai" + +import { DebridgeReporter } from "../target/types/debridge_reporter" + +const accountsToMeta = () => { + const result: AccountMeta[] = [ + { + isSigner: false, + isWritable: true, + pubkey: new PublicKey("6SW1N9Rq2TqT3uQCD4F5zwtTTSFSarZmfyrk829SzsBX"), + }, + { + isSigner: false, + isWritable: true, + pubkey: new PublicKey("So11111111111111111111111111111111111111112"), + }, + { + isSigner: false, + isWritable: true, + pubkey: new PublicKey("8gjgVkHXTttCoSGGtzucFkJUWujQ8pgWuvnHCLSN7i3o"), + }, + { + isSigner: false, + isWritable: false, + pubkey: new PublicKey("7FmGdfJfDrrM6P68y7jijjj4xU9rH3hsUK2Kyp54iJUx"), + }, + { + isSigner: false, + isWritable: false, + pubkey: new PublicKey("8L81QZBfwA6Xi9zd49fyUfMRWJBCAxiUxd6jGHPnu1BQ"), + }, + { + isSigner: false, + isWritable: false, + pubkey: new PublicKey("DeSetTwWhjZq6Pz9Kfdo1KoS5NqtsM6G8ERbX4SSCSft"), + }, + { + isSigner: false, + isWritable: false, + pubkey: new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"), + }, + { + isSigner: false, + isWritable: true, + pubkey: new PublicKey("CcjkxrCJvfXrmds78hwCnovkdmTgE12wqojiVLrtW1qn"), + }, + { + isSigner: false, + isWritable: true, + pubkey: new PublicKey("5MgAaNomDg4Y88v7gJ7LSWAyoLpDfcfbXZGQQnFddjJT"), + }, + { + isSigner: false, + isWritable: true, + pubkey: new PublicKey("2LKQceMRwfJNZovtSbsHmfszDYM5kTZHajFry2nqD2pi"), + }, + { + isSigner: false, + isWritable: true, + pubkey: new PublicKey("BzoSTqbp8vZ54Baq2K4LTwGnC8fYvKiEFQDNxdEDnosG"), + }, + { + isSigner: false, + isWritable: false, + pubkey: new PublicKey("11111111111111111111111111111111"), + }, + { + isSigner: false, + isWritable: true, + pubkey: new PublicKey("dPLMV1ky3H61yRGFfNC6AYmzBePhsdes9oNZ7chPbYW"), + }, + { + isSigner: false, + isWritable: true, + pubkey: new PublicKey("2cU8vjsMnRcusX1WdwZy1AwCLrUWbDw6frnk3XDz3VVK"), + }, + { + isSigner: true, + isWritable: true, + pubkey: new PublicKey("FsiBNh2KcPrjZFMF7EBCWpUpAo95DfrMXB2U2XrqSFWF"), + }, + { + isSigner: false, + isWritable: false, + pubkey: new PublicKey("4kQYWVy6Vu8YUXVp5BgQC12ZX1HLRUfkK3bLzBFFjnNW"), + }, + { + isSigner: false, + isWritable: false, + pubkey: new PublicKey("APMGxdbtubfWLQUACsN2yv2pxkvAgWwuxBe8ohFYoB37"), + }, + { + isSigner: false, + isWritable: false, + pubkey: new PublicKey("DEbrdGj3HsRsAzx6uH4MKyREKxVAfBydijLUF3ygsFfh"), + }, + ] + + return result +} + +describe("debridge_reporter", () => { + const provider = anchor.AnchorProvider.local() + anchor.setProvider(provider) + + const reporter = anchor.workspace.DebridgeReporter as Program + + it("should dispatch a slot", async () => { + const targetChainId = Buffer.from("1".padStart(64, "0"), "hex") + const receiver = Buffer.from("1".padStart(40, "1"), "hex") + const slot = await provider.connection.getSlot() + const slotNumberToDispatch = new anchor.BN(slot - 1) + + await reporter.methods + .dispatchSlot(targetChainId, receiver, slotNumberToDispatch) + .accounts({ + slotHashes: SYSVAR_SLOT_HASHES_PUBKEY, + }) + .remainingAccounts([...accountsToMeta()]) + .rpc() + }) +}) +*/