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

feat: Solana integration #83

Open
wants to merge 34 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5faaac4
feat(solana): <- starts this and adds a first implementation of an ad…
allemanfredi Nov 27, 2024
9c40e5b
feat(solana): starts adding hashi program
allemanfredi Nov 27, 2024
4abd108
refactor(solana): organizes programs's folders better
allemanfredi Nov 27, 2024
c222917
feat(solana): adds hashi tests
allemanfredi Nov 27, 2024
34593ca
feat(solana): starts debridge_reporter
allemanfredi Nov 29, 2024
bfc7770
feat(solana): adds shared folder
allemanfredi Nov 29, 2024
7ce941b
feat(solana): adds Message
allemanfredi Nov 29, 2024
3b323c7
feat(solana): adds a first version of wormhole-reporter
allemanfredi Nov 29, 2024
fd82a9f
chore(solana): rm not used file
allemanfredi Dec 2, 2024
70f14cf
feat(solana): adds abi encoding for message and adds tests for wormho…
allemanfredi Dec 5, 2024
f08a6fa
refactor(solana): adds some fx comments
allemanfredi Dec 5, 2024
b1589ba
feat(solana): adds slot dispatch within wormhole_reporter.initialize
allemanfredi Dec 9, 2024
18ce29f
chore(solana): rn wormhole_reporter feature testnet into devnet
allemanfredi Dec 9, 2024
5a25ba9
refactor(solana): updates wormhole_reporter tests based on new changes
allemanfredi Dec 9, 2024
de19b01
chore(solana): updates debridge-solana-sdk version
allemanfredi Dec 9, 2024
55fdac8
feat(solana): starts snapshotter
allemanfredi Dec 17, 2024
ed82a66
refactor(solana): rm not used wormhole_reporter errors
allemanfredi Dec 17, 2024
bb7e50a
feat(solana): adds nonce to snapshotter
allemanfredi Dec 17, 2024
6798baa
refactor(solana): rm dispatch_slot in favour of dispatch_root within …
allemanfredi Dec 18, 2024
54db4cb
refactor(solana): imports getFakeAccounts from utils
allemanfredi Dec 18, 2024
074a9b8
refactor(solana): adds missing comments to wormhole_reporter
allemanfredi Dec 18, 2024
3f611be
refactor(solana): refactors snapshotter comments
allemanfredi Dec 18, 2024
a1b35a0
feat(solana): rm dispatch_slot in favour of dispatch_root within debr…
allemanfredi Dec 18, 2024
de794ad
refactor(solana): adds missing comments to wormhole_reporter
allemanfredi Dec 18, 2024
f4da53d
refactor(solana): rn MerkleMountainRange into BatchMerkleTree
allemanfredi Dec 18, 2024
863fb42
refactor(solana): rn push_batch_of_leaves into push_batch
allemanfredi Dec 18, 2024
97f6091
refactor(solana): creates utils folder within tests
allemanfredi Dec 19, 2024
79ac5b8
feat(evm): adds verifyForeignSolanaAccount within HashiProver
allemanfredi Dec 19, 2024
1e48b5d
feat(solana): puts BashMerkleTree within shared
allemanfredi Dec 19, 2024
d758c61
chore(solana): adds missing descriptions
allemanfredi Dec 19, 2024
fbcb2e8
chore(solana): updates snapshotter comments
allemanfredi Dec 19, 2024
b8aebe7
refactor(solana): changes ids into the localnet ones
allemanfredi Jan 8, 2025
d24dc97
chore(solana): changes again ids
allemanfredi Jan 8, 2025
839f81a
feat(solana): implements migrations for snapshotter and wormhole_repo…
allemanfredi Jan 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions packages/evm/contracts/libraries/BatchMerkleTree.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.0;

library BatchMerkleTree {
error InvalidNodeLength();
error InvalidPosition();
error InvalidNodeExpected33Bytes();

function verify(bytes[][] calldata proof, bytes32 targetNode, bytes32 root) internal pure returns (bool) {
bytes[] memory batchProof;
for (uint256 i = 0; i < proof.length; i++) {
if (proof[i].length != 1) {
batchProof = proof[i];
}
}

bytes32 batchRoot = targetNode;
for (uint256 i = 0; i < batchProof.length; i++) {
bytes memory node = batchProof[i];

if (node.length != 33) revert InvalidNodeLength();
uint8 position = uint8(node[32]);
if (position != 1 && position != 2) revert InvalidPosition();
bytes32 data = bytes32(node);

bool isLeftNode = position == 1;
batchRoot = isLeftNode ? hashNodes(data, batchRoot) : hashNodes(batchRoot, data);
}

bytes32[] memory batchRoots = new bytes32[](proof.length);
for (uint256 i = 0; i < proof.length; i++) {
bytes[] memory batch = proof[i];
// targetNode is the onlyElement in this batch
if (batch.length == 0)
batchRoots[i] = targetNode;
// batch that contains only the root
else if (batch.length == 1) {
bytes memory batchRootNode = batch[0];
if (batchRootNode.length != 33) revert InvalidNodeExpected33Bytes();
uint8 position = uint8(batchRootNode[32]);
if (position != 0) revert InvalidPosition();
batchRoots[i] = bytes32(batchRootNode);
} else {
batchRoots[i] = batchRoot;
}
}

bytes32 calculatedRoot = batchRoots.length > 1 ? hashNodes(batchRoots[0], batchRoots[1]) : batchRoots[0];
for (uint256 i = 2; i < batchRoots.length; i++) {
calculatedRoot = hashNodes(calculatedRoot, batchRoots[i]);
}

return calculatedRoot == root;
}

function hashNodes(bytes32 node1, bytes32 node2) internal pure returns (bytes32) {
return sha256(abi.encodePacked(node1, node2));
}
}
19 changes: 18 additions & 1 deletion packages/evm/contracts/prover/HashiProver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pragma solidity ^0.8.0;

import { HashiProverLib } from "./HashiProverLib.sol";
import { AccountAndStorageProof, ReceiptProof } from "./HashiProverStructs.sol";
import { AccountAndStorageProof, ReceiptProof, SolanaAccountProof } from "./HashiProverStructs.sol";

contract HashiProver {
/// @notice Stores the address of the ShoyuBashi contract.
Expand Down Expand Up @@ -50,4 +50,21 @@ contract HashiProver {
function verifyForeignStorage(AccountAndStorageProof calldata proof) internal view returns (bytes[] memory) {
return HashiProverLib.verifyForeignStorage(proof, SHOYU_BASHI);
}

/**
* @dev Verifies the data of a Solana account against the merkle root generated by the Snapshotter
* on Solana and propagated using Hashi.
*
* @param proof A `SolanaAccountProof` struct containing proof details:
* - chainId: The chain ID of the foreign Solana based blockchain where the account resides.
* - nonce: A unique identifier used to locate the specific Merkle root in Hashi against which the proof verification is performed.
* - account: The Solana account data with this format: pubkey, lamports, data, owner, rent_epoch
* - accountProof: A Batch Merkle proof to validate the existence of the account
* in the state of the foreign blockchain.
*
* @return bytes The serialized Solana account data if the verification succeeds.
*/
function verifyForeignSolanaAccount(SolanaAccountProof calldata proof) internal view returns (bytes memory) {
return HashiProverLib.verifyForeignSolanaAccount(proof, SHOYU_BASHI);
}
}
27 changes: 26 additions & 1 deletion packages/evm/contracts/prover/HashiProverLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ pragma solidity ^0.8.0;
import { SecureMerkleTrie } from "@eth-optimism/contracts-bedrock/src/libraries/trie/SecureMerkleTrie.sol";
import { MerkleTrie } from "@eth-optimism/contracts-bedrock/src/libraries/trie/MerkleTrie.sol";
import { RLPReader } from "@eth-optimism/contracts-bedrock/src/libraries/rlp/RLPReader.sol";
import { AccountAndStorageProof, ReceiptProof } from "./HashiProverStructs.sol";
import { AccountAndStorageProof, ReceiptProof, SolanaAccountProof } from "./HashiProverStructs.sol";
import { BatchMerkleTree } from "../libraries/BatchMerkleTree.sol";
import { IShoyuBashi } from "../interfaces/IShoyuBashi.sol";

library HashiProverLib {
Expand All @@ -19,6 +20,7 @@ library HashiProverLib {
error InvalidStorageHash();
error InvalidStorageProofParams();
error UnsupportedTxType();
error InvalidAccountProof();

/**
* @dev Verifies and retrieves a specific event from a transaction receipt in a foreign blockchain.
Expand Down Expand Up @@ -92,6 +94,29 @@ library HashiProverLib {
return verifyStorageProof(storageHash, proof.storageKeys, proof.storageProof);
}

/**
* @dev Verifies the data of a Solana account against the merkle root generated by the Snapshotter
* on Solana and propagated using Hashi.
*
* @param proof A `SolanaAccountProof` struct containing proof details:
* - chainId: The chain ID of the foreign Solana based blockchain where the account resides.
* - nonce: A unique identifier used to locate the specific Merkle root in Hashi against which the proof verification is performed.
* - account: The Solana account data with this format: pubkey, lamports, data, owner, rent_epoch
* - accountProof: A Batch Merkle proof to validate the existence of the account
* in the state of the foreign blockchain.
* @param shoyuBashi The address of ShoyuBashi contract
*
* @return bytes The serialized Solana account data if the verification succeeds.
*/
function verifyForeignSolanaAccount(
SolanaAccountProof calldata proof,
address shoyuBashi
) internal view returns (bytes memory) {
bytes32 root = IShoyuBashi(shoyuBashi).getThresholdHash(proof.chainId, proof.nonce);
if (!BatchMerkleTree.verify(proof.accountProof, sha256(proof.account), root)) revert InvalidAccountProof();
return proof.account;
}

/**
* @notice Verifies a block header against the Hashi contract by checking its hash and, if needed, traversing ancestral blocks.
* @dev This function first checks if the provided block header hash matches the threshold hash stored in the ShoyuBashi contract.
Expand Down
7 changes: 7 additions & 0 deletions packages/evm/contracts/prover/HashiProverStructs.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,10 @@ struct ReceiptProof {
bytes transactionIndex; // The index of the transaction within the block's transaction list (RLP-encoded).
uint256 logIndex; // The index of the log entry within the transaction receipt being proven.
}

struct SolanaAccountProof {
uint256 chainId; // The ID of the blockchain where the proof is applicable.
uint256 nonce; // A unique identifier used to locate the specific Merkle root in Hashi for which the proof verification is performed.
bytes account; // The Solana account data with this format: pubkey, lamports, data, owner, rent_epoch
bytes[][] accountProof; // Merkle proof to verify the accunt within the merkle root stored in Hashi
}
19 changes: 18 additions & 1 deletion packages/evm/contracts/prover/HashiProverUpgradeable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity ^0.8.0;
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import { HashiProverLib } from "./HashiProverLib.sol";
import { AccountAndStorageProof, ReceiptProof } from "./HashiProverStructs.sol";
import { AccountAndStorageProof, ReceiptProof, SolanaAccountProof } from "./HashiProverStructs.sol";

contract HashiProverUpgradeable is Initializable, OwnableUpgradeable {
/// @notice Stores the address of the ShoyuBashi contract.
Expand Down Expand Up @@ -73,6 +73,23 @@ contract HashiProverUpgradeable is Initializable, OwnableUpgradeable {
return HashiProverLib.verifyForeignStorage(proof, shoyuBashi);
}

/**
* @dev Verifies the data of a Solana account against the merkle root generated by the Snapshotter
* on Solana and propagated using Hashi.
*
* @param proof A `SolanaAccountProof` struct containing proof details:
* - chainId: The chain ID of the foreign Solana based blockchain where the account resides.
* - nonce: A unique identifier used to locate the specific Merkle root in Hashi against which the proof verification is performed.
* - account: The Solana account data with this format: pubkey, lamports, data, owner, rent_epoch
* - accountProof: A Batch Merkle proof to validate the existence of the account
* in the state of the foreign blockchain.
*
* @return bytes The serialized Solana account data if the verification succeeds.
*/
function verifyForeignSolanaAccount(SolanaAccountProof calldata proof) internal view returns (bytes memory) {
return HashiProverLib.verifyForeignSolanaAccount(proof, shoyuBashi);
}

/// @notice This empty reserved space is put in place to allow future versions to add new variables without shifting down storage in the inheritance chain (see [OpenZeppelin's guide about storage gaps](https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps)).
uint256[49] private __gap;
}
4 changes: 4 additions & 0 deletions packages/evm/contracts/test/HashiProverTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ contract HashiProverTest is HashiProver {
function getEventValues(ReceiptProof calldata proof) external view returns (bytes memory) {
return verifyForeignEvent(proof);
}

function getSolanaAccount(SolanaAccountProof calldata proof) external view returns (bytes memory) {
return verifyForeignSolanaAccount(proof);
}
}
4 changes: 4 additions & 0 deletions packages/evm/contracts/test/HashiProverTestUpgradeable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,9 @@ contract HashiProverTestUpgradeable is UUPSUpgradeable, HashiProverUpgradeable {
return verifyForeignEvent(proof);
}

function getSolanaAccount(SolanaAccountProof calldata proof) external view returns (bytes memory) {
return verifyForeignSolanaAccount(proof);
}

function _authorizeUpgrade(address) internal override {}
}
41 changes: 41 additions & 0 deletions packages/evm/test/05_HashiProver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import {
RECEIPT_PROOF,
RECEIPT_PROOF_WITH_ANCESTRAL_BLOCKS,
} from "./proofs"
import { createHash, randomBytes } from "crypto"
import BatchMerkleTree from "./utils/batch-merkle-tree"

const SOURCE_CHAIN_ID = 10
const SOLANA_CHAIN_ID = 999

let hashi: Contract, adapter: Contract, shoyuBashi: Contract, hashiProvers: Contract[], owner: SignerWithAddress

Expand All @@ -37,6 +40,7 @@ describe("HashiProver", () => {
)

await shoyuBashi.enableAdapters(SOURCE_CHAIN_ID, [adapter.address], 1)
await shoyuBashi.enableAdapters(SOLANA_CHAIN_ID, [adapter.address], 1)
})

it("should't be able to get the value if the block is not available", async () => {
Expand Down Expand Up @@ -217,4 +221,41 @@ describe("HashiProver", () => {
)
}
})

it("should be able to read a solana account", async () => {
// NOTE: this simulates random
/**
* hashv(&[
pubkey.as_ref(),
&lamports.to_le_bytes(),
data,
owner.as_ref(),
&rent_epoch.to_le_bytes(),
])
*/
const fakeAccountData = Array.from(Array(25).keys()).map(() => randomBytes(64))
const fakeAccountDataHashes = fakeAccountData.map((_accHash) => {
const hash = createHash("sha-256")
hash.update(_accHash)
return hash.digest()
})

const tree = new BatchMerkleTree()
tree.pushBatch(fakeAccountDataHashes.slice(0, 10))
tree.pushBatch(fakeAccountDataHashes.slice(10, 20))
tree.pushBatch(fakeAccountDataHashes.slice(20, 21))
const targetAccountHash = fakeAccountDataHashes[20]
const targetAccount = fakeAccountData[20]
const proof = tree.getHexProof(targetAccountHash)

const sourceChainId = 999
const nonce = 0
await adapter.setHashes(sourceChainId, [nonce], [tree.root()])

for (const hashiProver of hashiProvers) {
expect(await hashiProver.getSolanaAccount([sourceChainId, nonce, targetAccount, proof])).to.be.eq(
"0x" + targetAccount.toString("hex"),
)
}
})
})
Loading
Loading