├── .gitmodules ├── shell.nix ├── solana-bridges ├── ethereum │ ├── pass.txt │ ├── UTC--2020-09-17T02-34-16.613Z--0xabc6bbd0ad6aca2d25380fc7835fe088e7690c2c │ └── Genesis.json ├── ethereum-client │ ├── .gitignore │ ├── Xargo.toml │ ├── build.sh │ ├── watch.sh │ ├── deploy.sh │ ├── storage.sh │ ├── src │ │ ├── lib.rs │ │ ├── pow_proof.rs │ │ ├── tests │ │ │ ├── relayer_runs.rs │ │ │ ├── ethash_proof.rs │ │ │ └── blocks.rs │ │ ├── types.rs │ │ ├── instruction.rs │ │ ├── ledger_ring_buffer.rs │ │ ├── prove.rs │ │ ├── processor.rs │ │ └── eth.rs │ └── Cargo.toml ├── src-bin │ ├── RunEthereumTestnet.hs │ ├── RunSolanaTestnet.hs │ ├── RelaySolanaToEthereum.hs │ ├── DeploySolanaClient.hs │ ├── GenerateSolanaGenesis.hs │ └── RelayEthereumToSolana.hs ├── solana │ └── genesis.tar.bz2 ├── test-extras │ ├── ed25519.py │ ├── ed25519-test-sign.py │ ├── ed25519_orig.py │ └── sign.input.short ├── src │ ├── Ethereum │ │ ├── Contracts │ │ │ ├── Dist.hs │ │ │ ├── Bindings.hs │ │ │ └── TH.hs │ │ └── Contracts.hs │ └── Solana │ │ ├── RPC.hs │ │ ├── Utils.hs │ │ └── Types.hs ├── solana-client │ ├── watch.sh │ └── estimageGas.sh ├── test │ └── Main.hs └── solana-bridges.cabal ├── dep ├── hs-web3 │ ├── default.nix │ ├── github.json │ └── thunk.nix ├── nixpkgs │ ├── default.nix │ ├── github.json │ └── thunk.nix ├── which │ ├── default.nix │ ├── github.json │ └── thunk.nix ├── nix-thunk │ ├── default.nix │ ├── github.json │ └── thunk.nix └── gitignore.nix │ ├── default.nix │ ├── github.json │ └── thunk.nix ├── solana-client-tool ├── shell.nix ├── package.json ├── build.sh ├── .eslintrc.js ├── default.nix ├── index.js └── node-env.nix ├── .gitignore ├── Makefile ├── README.md └── default.nix /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | (import ./.).shell 2 | -------------------------------------------------------------------------------- /solana-bridges/ethereum/pass.txt: -------------------------------------------------------------------------------- 1 | pass 2 | -------------------------------------------------------------------------------- /solana-bridges/ethereum-client/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | target-x86 3 | target-bpf -------------------------------------------------------------------------------- /dep/hs-web3/default.nix: -------------------------------------------------------------------------------- 1 | # DO NOT HAND-EDIT THIS FILE 2 | import (import ./thunk.nix) -------------------------------------------------------------------------------- /dep/nixpkgs/default.nix: -------------------------------------------------------------------------------- 1 | # DO NOT HAND-EDIT THIS FILE 2 | import (import ./thunk.nix) -------------------------------------------------------------------------------- /dep/which/default.nix: -------------------------------------------------------------------------------- 1 | # DO NOT HAND-EDIT THIS FILE 2 | import (import ./thunk.nix) -------------------------------------------------------------------------------- /dep/nix-thunk/default.nix: -------------------------------------------------------------------------------- 1 | # DO NOT HAND-EDIT THIS FILE 2 | import (import ./thunk.nix) -------------------------------------------------------------------------------- /dep/gitignore.nix/default.nix: -------------------------------------------------------------------------------- 1 | # DO NOT HAND-EDIT THIS FILE 2 | import (import ./thunk.nix) -------------------------------------------------------------------------------- /solana-bridges/ethereum-client/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] -------------------------------------------------------------------------------- /solana-client-tool/shell.nix: -------------------------------------------------------------------------------- 1 | {pkgs ? (import ../dep/nixpkgs {})} : (import ./. {inherit pkgs;}).shell 2 | -------------------------------------------------------------------------------- /solana-bridges/src-bin/RunEthereumTestnet.hs: -------------------------------------------------------------------------------- 1 | import Solana.Relayer 2 | 3 | main :: IO () 4 | main = runEthereumTestnet 5 | -------------------------------------------------------------------------------- /solana-bridges/src-bin/RunSolanaTestnet.hs: -------------------------------------------------------------------------------- 1 | import Solana.Relayer 2 | 3 | main :: IO () 4 | main = runSolanaTestnet 5 | -------------------------------------------------------------------------------- /solana-bridges/src-bin/RelaySolanaToEthereum.hs: -------------------------------------------------------------------------------- 1 | import Solana.Relayer 2 | 3 | main :: IO () 4 | main = mainRelaySolanaToEthereum 5 | -------------------------------------------------------------------------------- /solana-bridges/src-bin/DeploySolanaClient.hs: -------------------------------------------------------------------------------- 1 | import Solana.Relayer 2 | 3 | main :: IO () 4 | main = mainDeploySolanaClientContract 5 | -------------------------------------------------------------------------------- /solana-bridges/src-bin/GenerateSolanaGenesis.hs: -------------------------------------------------------------------------------- 1 | import Solana.Relayer 2 | 3 | main :: IO () 4 | main = print =<< makeGenesisArchive 5 | -------------------------------------------------------------------------------- /solana-bridges/solana/genesis.tar.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obsidiansystems/solana-bridges/HEAD/solana-bridges/solana/genesis.tar.bz2 -------------------------------------------------------------------------------- /solana-bridges/src-bin/RelayEthereumToSolana.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Solana.Relayer 4 | 5 | main :: IO () 6 | main = mainRelayEthereumToSolana 7 | -------------------------------------------------------------------------------- /solana-bridges/test-extras/ed25519.py: -------------------------------------------------------------------------------- 1 | if __import__("os").environ.get("ed25519_py") == "optimised": 2 | from ed25519_optimised import * 3 | else: 4 | from ed25519_orig import * 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # nix 2 | result 3 | result-* 4 | 5 | # builds 6 | dist 7 | dist-newstyle 8 | 9 | # data from runs 10 | .run-* 11 | .run-latest 12 | 13 | # emacs 14 | .#* 15 | 16 | # ethereum 17 | .ethash 18 | -------------------------------------------------------------------------------- /dep/hs-web3/github.json: -------------------------------------------------------------------------------- 1 | { 2 | "owner": "obsidiansystems", 3 | "repo": "hs-web3", 4 | "branch": "sol", 5 | "private": false, 6 | "rev": "e9ba56b308d6a5983edfd97c0403c4ca56c02626", 7 | "sha256": "1n04dx3xmjbhsjwg146zzb0p5m88nj9cfbgl9n3z62hfkcvhbb6m" 8 | } 9 | -------------------------------------------------------------------------------- /dep/nixpkgs/github.json: -------------------------------------------------------------------------------- 1 | { 2 | "owner": "NixOS", 3 | "repo": "nixpkgs", 4 | "branch": "release-20.09", 5 | "private": false, 6 | "rev": "a31736120c5de6e632f5a0ba1ed34e53fc1c1b00", 7 | "sha256": "0xfjizw6w84w1fj47hxzw2vwgjlszzmsjb8k8cgqhb379vmkxjfl" 8 | } 9 | -------------------------------------------------------------------------------- /dep/which/github.json: -------------------------------------------------------------------------------- 1 | { 2 | "owner": "obsidiansystems", 3 | "repo": "which", 4 | "branch": "develop", 5 | "private": false, 6 | "rev": "a7a86bfa1d05d81de4a12a89315bd383763b98ea", 7 | "sha256": "1635wh4psqbhybbvgjr9gy6f051sb27zlgfamrqw14cdrqdvk5m8" 8 | } 9 | -------------------------------------------------------------------------------- /dep/nix-thunk/github.json: -------------------------------------------------------------------------------- 1 | { 2 | "owner": "obsidiansystems", 3 | "repo": "nix-thunk", 4 | "branch": "master", 5 | "private": false, 6 | "rev": "aaf28f73b7168aa1621189e915fa0c8fbc2ca57f", 7 | "sha256": "17arlp9vgg7c95m6qmnaqsijfgi18xj1jqr5frsncs8jz245mh2r" 8 | } 9 | -------------------------------------------------------------------------------- /dep/gitignore.nix/github.json: -------------------------------------------------------------------------------- 1 | { 2 | "owner": "hercules-ci", 3 | "repo": "gitignore.nix", 4 | "branch": "master", 5 | "private": false, 6 | "rev": "c4662e662462e7bf3c2a968483478a665d00e717", 7 | "sha256": "1npnx0h6bd0d7ql93ka7azhj40zgjp815fw2r6smg8ch9p7mzdlx" 8 | } 9 | -------------------------------------------------------------------------------- /solana-bridges/ethereum-client/build.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env nix-shell 2 | #! nix-shell ../../default.nix -A shells.ethereum-client-bpf -i bash 3 | 4 | set -uo pipefail 5 | cd "$(dirname $0)" 6 | 7 | export CC=$SOLANA_LLVM_CC 8 | export AR=$SOLANA_LLVM_AR 9 | xargo build --target bpfel-unknown-unknown --release --no-default-features --features program 10 | -------------------------------------------------------------------------------- /solana-bridges/ethereum-client/watch.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env nix-shell 2 | #! nix-shell ../../default.nix -A shells.ethereum-client-x86 -i bash 3 | 4 | set -uo pipefail 5 | cd "$(dirname $0)" 6 | 7 | RUST_BACKTRACE=1 \ 8 | RUST_LOG=quickcheck \ 9 | CARGO_TARGET_DIR='target-x86' \ 10 | cargo watch -c -x 'test --features program -- -Z unstable-options --report-time' 11 | -------------------------------------------------------------------------------- /solana-bridges/src/Ethereum/Contracts/Dist.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE TemplateHaskell #-} 3 | module Ethereum.Contracts.Dist where 4 | 5 | import qualified Data.ByteString as BS 6 | import Ethereum.Contracts.TH 7 | 8 | solanaClientContractAbi :: String 9 | solanaClientContractBin :: BS.ByteString 10 | (solanaClientContractAbi, solanaClientContractBin) = $(compileSolidity) 11 | -------------------------------------------------------------------------------- /solana-bridges/ethereum-client/deploy.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env nix-shell 2 | #! nix-shell ../../default.nix -A shells.scripts -i bash 3 | 4 | set -uo pipefail 5 | cd "$(dirname $0)" 6 | 7 | PROGRAM_ID=$(solana deploy target-bpf/bpfel-unknown-unknown/release/solana_ethereum_client.so --use-deprecated-loader | jq .programId -r) 8 | $(nix-build ../../default.nix -A solana-client-tool)/bin/solana-bridge-tool alloc --program-id $PROGRAM_ID --space 99999 9 | -------------------------------------------------------------------------------- /solana-client-tool/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solana-ethereum-client-tool", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "BSD", 11 | "dependencies": { 12 | "@solana/web3.js": "^0.78.2", 13 | "yargs": "" 14 | }, 15 | "bin": { 16 | "solana-bridge-tool": "index.js" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /solana-bridges/src/Ethereum/Contracts/Bindings.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE DeriveGeneric #-} 3 | {-# LANGUAGE FlexibleInstances #-} 4 | {-# LANGUAGE MultiParamTypeClasses #-} 5 | {-# LANGUAGE OverloadedStrings #-} 6 | {-# LANGUAGE TemplateHaskell #-} 7 | module Ethereum.Contracts.Bindings where 8 | 9 | import Network.Ethereum.Contract.TH (quoteAbiDec) 10 | 11 | import Ethereum.Contracts.Dist 12 | 13 | $(quoteAbiDec solanaClientContractAbi) 14 | -------------------------------------------------------------------------------- /solana-bridges/ethereum-client/storage.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env nix-shell 2 | #! nix-shell ../../default.nix -A shells.ethereum-client-bpf -i bash 3 | set -uo pipefail 4 | 5 | if [ "$#" -ne 1 ] ; then 6 | echo "Usage: $0 .json" 7 | exit 1 8 | fi 9 | 10 | CONFIG_FILE=$1 11 | shift 12 | ACCOUNT=$(jq -r .accountId < $CONFIG_FILE) 13 | 14 | watch -n 1 "solana account $ACCOUNT --output json | jq -r '.account.data[0]' | base64 -d | hexdump -C" 15 | -------------------------------------------------------------------------------- /solana-bridges/solana-client/watch.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env nix-shell 2 | #! nix-shell ../../default.nix -A shells.solana-client-evm -i bash 3 | 4 | set -uo pipefail 5 | cd "$(dirname $0)" 6 | 7 | root="$(git rev-parse --show-toplevel)" 8 | 9 | watchdirs=("$(pwd)") 10 | 11 | while true; do 12 | clear 13 | solc --optimize SolanaClient.sol 14 | if ! inotifywait -qre close_write "${watchdirs[@]}"; then 15 | exit "inotifywait failed" 16 | fi 17 | echo 18 | done 19 | -------------------------------------------------------------------------------- /dep/which/thunk.nix: -------------------------------------------------------------------------------- 1 | # DO NOT HAND-EDIT THIS FILE 2 | let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }: 3 | if !fetchSubmodules && !private then builtins.fetchTarball { 4 | url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256; 5 | } else (import {}).fetchFromGitHub { 6 | inherit owner repo rev sha256 fetchSubmodules private; 7 | }; 8 | json = builtins.fromJSON (builtins.readFile ./github.json); 9 | in fetch json -------------------------------------------------------------------------------- /solana-bridges/ethereum-client/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "program")] 2 | pub mod epoch_roots; 3 | pub mod eth; 4 | pub mod instruction; 5 | pub mod ledger_ring_buffer; 6 | pub mod pow_proof; 7 | pub mod processor; 8 | pub mod prove; 9 | pub mod types; 10 | 11 | #[cfg(test)] 12 | mod tests; 13 | 14 | use processor::process_instruction; 15 | use solana_sdk::entrypoint_deprecated; 16 | 17 | // Declare and export the program's entrypoint 18 | entrypoint_deprecated!(process_instruction); 19 | -------------------------------------------------------------------------------- /solana-bridges/solana-client/estimageGas.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env nix-shell 2 | #! nix-shell ../../default.nix -A shells.solana-client-evm -i bash 3 | 4 | set -uo pipefail 5 | cd "$(dirname $0)" 6 | 7 | OUTPUT=$(solc --optimize --gas SolanaClient.sol) 8 | echo "$OUTPUT" 9 | echo "" 10 | if (echo "$OUTPUT" | grep -q "infinite\$"); then 11 | # https://ethereum.stackexchange.com/a/39221 12 | echo "Note: any backward jumps or loops in the assembly code will report infinite gas"; 13 | fi; 14 | -------------------------------------------------------------------------------- /dep/hs-web3/thunk.nix: -------------------------------------------------------------------------------- 1 | # DO NOT HAND-EDIT THIS FILE 2 | let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }: 3 | if !fetchSubmodules && !private then builtins.fetchTarball { 4 | url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256; 5 | } else (import {}).fetchFromGitHub { 6 | inherit owner repo rev sha256 fetchSubmodules private; 7 | }; 8 | json = builtins.fromJSON (builtins.readFile ./github.json); 9 | in fetch json -------------------------------------------------------------------------------- /dep/nix-thunk/thunk.nix: -------------------------------------------------------------------------------- 1 | # DO NOT HAND-EDIT THIS FILE 2 | let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }: 3 | if !fetchSubmodules && !private then builtins.fetchTarball { 4 | url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256; 5 | } else (import {}).fetchFromGitHub { 6 | inherit owner repo rev sha256 fetchSubmodules private; 7 | }; 8 | json = builtins.fromJSON (builtins.readFile ./github.json); 9 | in fetch json -------------------------------------------------------------------------------- /dep/nixpkgs/thunk.nix: -------------------------------------------------------------------------------- 1 | # DO NOT HAND-EDIT THIS FILE 2 | let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }: 3 | if !fetchSubmodules && !private then builtins.fetchTarball { 4 | url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256; 5 | } else (import {}).fetchFromGitHub { 6 | inherit owner repo rev sha256 fetchSubmodules private; 7 | }; 8 | json = builtins.fromJSON (builtins.readFile ./github.json); 9 | in fetch json -------------------------------------------------------------------------------- /dep/gitignore.nix/thunk.nix: -------------------------------------------------------------------------------- 1 | # DO NOT HAND-EDIT THIS FILE 2 | let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }: 3 | if !fetchSubmodules && !private then builtins.fetchTarball { 4 | url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256; 5 | } else (import {}).fetchFromGitHub { 6 | inherit owner repo rev sha256 fetchSubmodules private; 7 | }; 8 | json = builtins.fromJSON (builtins.readFile ./github.json); 9 | in fetch json -------------------------------------------------------------------------------- /solana-client-tool/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix-shell 2 | #! nix-shell -I nixpkgs=../dep/nixpkgs/default.nix -i bash -p nodePackages.node2nix 3 | 4 | node2nix --bypass-cache --pkg-name 'nodejs-12_x' --lock ./package-lock.json 5 | 6 | patch -p0 < { 4 | inherit system; 5 | }, system ? builtins.currentSystem, nodejs ? pkgs."nodejs-12_x"}: 6 | 7 | let 8 | nodeEnv = import ./node-env.nix { 9 | inherit (pkgs) stdenv python2 utillinux runCommand writeTextFile; 10 | inherit nodejs; 11 | libtool = if pkgs.stdenv.isDarwin then pkgs.darwin.cctools else null; 12 | }; 13 | in 14 | import ./node-packages.nix { 15 | inherit (pkgs) fetchurl fetchgit; 16 | inherit nodeEnv; 17 | globalBuildInputs = [pkgs.nodePackages.node-gyp-build]; 18 | } 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | nix-build 3 | 4 | repl: 5 | nix-shell --run "cd solana-bridges && cabal new-repl" 6 | 7 | watch: 8 | nix-shell --run "cd solana-bridges && ghcid -c 'cabal new-repl' --restart solana-bridges.cabal --restart 'solana-client/SolanaClient.sol'" 9 | 10 | run-ethereum-testnet: 11 | $$(nix-build -A solana-bridges)/bin/run-ethereum-testnet 12 | 13 | run-solana-testnet: 14 | $$(nix-build -A solana)/bin/solana config set --url http://localhost:8899 15 | $$(nix-build -A run-solana-testnet)/bin/run-solana-testnet solana-bridges/solana/genesis.tar.bz2 16 | 17 | test-solana-client: 18 | nix-shell --run "cd solana-bridges && ghcid -c 'cabal new-repl' --restart solana-bridges.cabal --restart 'solana-client/SolanaClient.sol' --test 'Solana.Utils.testSolanaClient'" 19 | -------------------------------------------------------------------------------- /solana-bridges/test/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE LambdaCase #-} 2 | {-# LANGUAGE NumDecimals #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE TypeApplications #-} 5 | 6 | import Control.Monad.Except (runExceptT) 7 | import Crypto.Hash (Digest, SHA256) 8 | import Data.Bool (bool) 9 | import qualified Data.ByteArray as ByteArray 10 | import Data.ByteString (ByteString) 11 | import qualified Data.ByteString as BS 12 | import qualified Data.ByteString.Char8 as BSC 13 | import Data.Solidity.Prim.Address (Address) 14 | import Data.Tree (Tree (..)) 15 | import GHC.Word (Word64) 16 | import Network.Web3.Provider (Provider) 17 | import Test.Hspec (it, describe, hspec, shouldBe) 18 | 19 | import Ethereum.Contracts 20 | import Solana.Relayer 21 | import Solana.Utils 22 | 23 | main :: IO () 24 | main = pure () 25 | -------------------------------------------------------------------------------- /solana-bridges/src/Ethereum/Contracts/TH.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TemplateHaskell #-} 2 | 3 | module Ethereum.Contracts.TH where 4 | 5 | import qualified Data.ByteString as BS 6 | import qualified Data.Text as T 7 | import qualified Data.Text.Encoding as T 8 | import Instances.TH.Lift () 9 | import Language.Haskell.TH 10 | import Language.Haskell.TH.Syntax 11 | import System.Process 12 | 13 | compileSolidity :: Q Exp 14 | compileSolidity = do 15 | let contract = "SolanaClient" 16 | client = "solana-client" 17 | source = client <> "/" <> contract <> ".sol" 18 | dist = client <> "/dist" 19 | 20 | -- https://gitlab.haskell.org/ghc/ghc/-/issues/18330 21 | addDependentFile source 22 | 23 | (abi, bin) <- runIO $ do 24 | callProcess "solc" 25 | [ "--optimize" 26 | , "--abi", "--bin" 27 | , source 28 | , "-o", dist, "--overwrite" 29 | ] 30 | abi <- BS.readFile $ dist <> "/" <> contract <> ".abi" 31 | bin <- BS.readFile $ dist <> "/" <> contract <> ".bin" 32 | pure (abi, bin) 33 | 34 | [| (T.unpack (T.decodeUtf8 abi), bin) |] 35 | -------------------------------------------------------------------------------- /solana-bridges/ethereum/Genesis.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "chainId": 1234567, 4 | "homesteadBlock": 0, 5 | "eip150Block": 0, 6 | "eip155Block": 0, 7 | "eip158Block": 0, 8 | "byzantiumBlock": 0, 9 | "constantinopleBlock": 0, 10 | "petersburgBlock": 0, 11 | "istanbulBlock": 0 12 | }, 13 | "coinbase": "0x0000000000000000000000000000000000000000", 14 | "difficulty": "0x100", 15 | "extraData": "", 16 | "gasLimit": "0xffffffff", 17 | "nonce": "0x0000000000000042", 18 | "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000", 19 | "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", 20 | "timestamp": "0x00", 21 | "alloc": { 22 | "0x0000000000000000000000000000000000000001": { 23 | "balance": "11111111" 24 | }, 25 | "0x0000000000000000000000000000000000000002": { 26 | "balance": "222222222" 27 | }, 28 | "0xabc6bBD0aD6aca2D25380FC7835fe088E7690c2C": { 29 | "balance": "333333333" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /solana-bridges/ethereum-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "solana-ethereum-client" 3 | version = "0.0.1" 4 | description = "Ethereum light client for Solana" 5 | authors = ["Obsidian Systems LLC "] 6 | repository = "https://github.com/obsidiansystems/solana-bridges" 7 | #license = "Apache-2.0" 8 | edition = "2018" 9 | 10 | [features] 11 | no-entrypoint = [] 12 | program = ["solana-sdk/program"] 13 | 14 | [dependencies] 15 | solana-program = { version = "=1.4.8", default-features = false } 16 | solana-sdk = { version = "=1.4.8", default-features = false } 17 | getrandom = { version = "0.1.14", features = ["dummy"] } 18 | rlp = "0.4.5" 19 | rlp-derive = "0.1.0" 20 | hex = "0.4.2" 21 | arrayref = "0.3.6" 22 | ethereum-types = "0.9.2" 23 | ethash = "0.4" 24 | tiny-keccak = { version = "2.0", features = ["keccak"] } 25 | hex-literal = "0.2.1" 26 | serde = { version = "1.0", features = ["derive"] } 27 | serde_json = "1.0" 28 | 29 | [dev-dependencies] 30 | quickcheck = "0.9" 31 | quickcheck_macros = "0.9" 32 | 33 | [patch.crates-io.ethash] 34 | git = "https://github.com/obsidiansystems/ethash" 35 | branch = "solana" 36 | 37 | [patch.crates-io.parity-scale-codec] 38 | git = "https://github.com/obsidiansystems/parity-scale-codec" 39 | branch = "solana" 40 | 41 | [patch.crates-io.rlp] 42 | git = "https://github.com/obsidiansystems/parity-common" 43 | branch = "solana" 44 | 45 | [patch.crates-io.uint] 46 | git = "https://github.com/obsidiansystems/parity-common" 47 | branch = "solana" 48 | 49 | [lib] 50 | name = "solana_ethereum_client" 51 | crate-type = ["cdylib", "lib"] 52 | -------------------------------------------------------------------------------- /solana-bridges/test-extras/ed25519-test-sign.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import binascii 3 | import ed25519 4 | 5 | # examples of inputs: see sign.input 6 | # should produce no output: python sign.py < sign.input 7 | 8 | # warning: currently 37 seconds/line on a fast machine 9 | 10 | # fields on each input line: sk, pk, m, sm 11 | # each field hex 12 | # each field colon-terminated 13 | # sk includes pk at end 14 | # sm includes m at end 15 | 16 | 17 | while 1: 18 | line = sys.stdin.readline() 19 | print line 20 | if not line: break 21 | x = line.split(':') 22 | sk = binascii.unhexlify(x[0][0:64]) 23 | m = binascii.unhexlify(x[2]) 24 | if hasattr(ed25519, "publickey") or hasattr(ed25519, "signature"): 25 | pk = ed25519.publickey(sk) 26 | s = ed25519.signature(m,sk,pk) 27 | else: 28 | pk = binascii.unhexlify(x[1]) 29 | s = binascii.unhexlify(x[3][0:128]) 30 | 31 | ed25519.checkvalid(binascii.unhexlify(x[3][0:128]), m, pk) 32 | ed25519.checkvalid(s,m,pk) 33 | forgedsuccess = 0 34 | try: 35 | if len(m) == 0: 36 | forgedm = "x" 37 | else: 38 | forgedmlen = len(m) 39 | forgedm = ''.join([chr(ord(m[i])+(i==forgedmlen-1)) for i in range(forgedmlen)]) 40 | ed25519.checkvalid(s,forgedm,pk) 41 | forgedsuccess = 1 42 | except: 43 | pass 44 | assert not forgedsuccess 45 | assert x[0] == binascii.hexlify(sk + pk) 46 | assert x[1] == binascii.hexlify(pk) 47 | assert x[3] == binascii.hexlify(s + m) 48 | 49 | if getattr(ed25519, "assertions", None) is not None: 50 | import datetime 51 | import json 52 | assertionsFilename = "ed25519-assertions-{0:%Y%m%dT%H%M%S}.txt".format(datetime.datetime.now()) 53 | with open(assertionsFilename, "w") as extraTests: 54 | for (k, v) in ed25519.assertions.iteritems(): 55 | extraTests.write(json.dumps(v)) 56 | extraTests.write("\n") 57 | print assertionsFilename 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solana bridges 2 | 3 | - [Setup](#setup) 4 | - [Solana to Ethereum](#solana-to-ethereum-bridge) 5 | - [Ethereum to Solana](#ethereum-to-solana-bridge) 6 | 7 | ## Setup 8 | The Nix package manager is used for handling dependencies. 9 | The main build can take over 10 minutes the first time it's run, but it should be much faster afterwards. 10 | 11 | ```shell 12 | git clone https://github.com/obsidiansystems/solana-bridges.git 13 | cd solana-bridges 14 | nix-build 15 | ``` 16 | 17 | #### Cleanup 18 | The following directories will be populated by various tools used by this repo: 19 | - `~/.ethash` 20 | - `~/.ethashproof` 21 | - `~/.config/solana` 22 | - `~/.cargo` 23 | - `~/.xargo` 24 | 25 | Take caution when removing them since other programs might also be using these directories. 26 | 27 | #### Run an ethereum testnet locally 28 | On a separate terminal 29 | ```shell 30 | make run-ethereum-testnet 31 | ``` 32 | Leave the testnet running on this terminal 33 | 34 | #### Run a solana testnet locally 35 | On a separate terminal 36 | 37 | ```shell 38 | make run-solana-testnet 39 | ``` 40 | Leave the testnet running on this terminal 41 | 42 | ## Solana to Ethereum bridge 43 | 44 | #### Deploy contract 45 | ```shell 46 | $(nix-build -A solana-bridges)/bin/deploy-solana-client > solana-to-ethereum-config.json 47 | ``` 48 | Output should end with something similar to 49 | 50 | `Contract deployed at address: 0xCb15617c1190448F318b8179263a72deF2EE782a` 51 | 52 | #### Start relayer 53 | ```shell 54 | $(nix-build -A solana-bridges)/bin/relay-solana-to-ethereum solana-to-ethereum-config.json 55 | ``` 56 | 57 | ## Ethereum to Solana bridge 58 | 59 | #### Create and fund an account 60 | ```shell 61 | $(nix-build -A solana)/bin/solana-keygen new --no-passphrase 62 | $(nix-build -A solana)/bin/solana airdrop 1000 --faucet-host 127.0.0.1 63 | ``` 64 | 65 | #### Build contract 66 | ```shell 67 | ./solana-bridges/ethereum-client/build.sh 68 | ``` 69 | 70 | #### Deploy contract 71 | ```shell 72 | ./solana-bridges/ethereum-client/deploy.sh > ethereum-to-solana-config.json 73 | ``` 74 | 75 | #### Start relayer 76 | ```shell 77 | $(nix-build -A solana-bridges)/bin/relay-ethereum-to-solana ethereum-to-solana-config.json 78 | ``` 79 | -------------------------------------------------------------------------------- /solana-bridges/ethereum-client/src/pow_proof.rs: -------------------------------------------------------------------------------- 1 | use arrayref::{ 2 | array_ref, 3 | mut_array_refs, 4 | }; 5 | 6 | use ethereum_types::{H128, H512}; 7 | 8 | use solana_sdk::hash::hash as sha256; 9 | 10 | use rlp_derive::{RlpDecodable as RlpDecodableDerive, RlpEncodable as RlpEncodableDerive}; 11 | 12 | use crate::{ 13 | eth::*, 14 | epoch_roots::EPOCH_ROOTS, 15 | ledger_ring_buffer::*, 16 | }; 17 | 18 | #[derive(Debug, Eq, PartialEq, Clone, Copy, RlpEncodableDerive, RlpDecodableDerive)] 19 | pub struct AccessedElement { 20 | pub address: u32, 21 | pub value: H512, 22 | } 23 | 24 | // factored array to avoid fixed size array trait limits 25 | #[derive(Debug, Eq, PartialEq, Clone, Copy)] 26 | pub struct AccessedElements(pub [[AccessedElement; 4]; 32]); 27 | 28 | impl std::ops::Index for AccessedElements { 29 | type Output = AccessedElement; 30 | fn index(&self, n: u8) -> &Self::Output { 31 | &self.0[(n / 4) as usize][(n % 4) as usize] 32 | } 33 | } 34 | 35 | impl std::ops::IndexMut for AccessedElements { 36 | fn index_mut(&mut self, n: u8) -> &mut Self::Output { 37 | &mut self.0[(n / 4) as usize][(n % 4) as usize] 38 | } 39 | } 40 | 41 | 42 | pub fn hash_h128(arr: &[u8]) -> H128 { 43 | let data = sha256(arr).0; 44 | H128(*array_ref!(&data, 16, 16)) 45 | } 46 | 47 | #[derive(Debug, Eq, PartialEq, Clone, Copy, RlpEncodableDerive, RlpDecodableDerive)] 48 | pub struct ElementPair { 49 | pub e0: H512, 50 | pub e1: H512, 51 | } 52 | 53 | impl ElementPair { 54 | pub fn reduce(&self) -> H128 { 55 | let mut data = [0u8; 128]; 56 | { 57 | { 58 | let (a_dst, b_dst) = mut_array_refs!(&mut data, 64, 64); 59 | *a_dst = self.e0.0; 60 | *b_dst = self.e1.0; 61 | } 62 | { 63 | let (a_dst, b_dst, c_dst, d_dst) = mut_array_refs!(&mut data, 32, 32, 32, 32); 64 | a_dst.reverse(); 65 | b_dst.reverse(); 66 | c_dst.reverse(); 67 | d_dst.reverse(); 68 | } 69 | } 70 | hash_h128(&data) 71 | } 72 | 73 | } 74 | 75 | pub fn combine_h128(l: H128, r: H128) -> H128 { 76 | let mut data = [0u8; 64]; 77 | let (_, l_dst, _, r_dst) = mut_array_refs!(&mut data, 16, 16, 16, 16); 78 | *l_dst = l.0; 79 | *r_dst = r.0; 80 | 81 | hash_h128(&data) 82 | } 83 | 84 | pub fn apply_pow_element_merkle_proof(elems: &ElementPair, merkle_spine: &[H128], mut index: u32) -> H128 { 85 | index /= 2; // because we are looking at a pair of elements. 86 | 87 | let mut accum = elems.reduce(); 88 | 89 | for (i, &sibling) in merkle_spine.iter().enumerate() { 90 | if (index >> i as u64) % 2 == 0 { 91 | accum = combine_h128(accum, sibling); 92 | } else { 93 | accum = combine_h128(sibling, accum); 94 | } 95 | } 96 | 97 | accum 98 | } 99 | 100 | pub fn get_wanted_merkle_root(height: u64) -> H128 { 101 | EPOCH_ROOTS[height_to_epoch(height) as usize] 102 | } 103 | 104 | #[cfg(not(target_arch = "bpf"))] 105 | pub fn verify_pow_indexes(ri: &mut RingItem) -> bool { 106 | let mut iter = ri.elements.0.iter_mut().flat_map(|x| x.iter_mut()); 107 | verify_pow(&ri.header, |wanted_addr| { 108 | let a = iter.next().unwrap(); 109 | // Set for challengers, now that we know what it is. 110 | a.address = wanted_addr; 111 | a.value 112 | }) 113 | } 114 | 115 | //TODO: remove once Keccak syscalls are added - currently runs into instruction limit 116 | #[cfg(target_arch = "bpf")] 117 | pub fn verify_pow_indexes(_ri: &mut RingItem) -> bool { 118 | return true; 119 | } 120 | -------------------------------------------------------------------------------- /solana-bridges/test-extras/ed25519_orig.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import sys 3 | sys.setrecursionlimit(sys.getrecursionlimit() * 10) 4 | 5 | b = 256 6 | q = 2**255 - 19 7 | l = 2**252 + 27742317777372353535851937790883648493 8 | 9 | def H(m): 10 | return hashlib.sha512(m).digest() 11 | 12 | assertions = {} 13 | 14 | def accumassertions(fn): 15 | import binascii 16 | def fixbytes(x): 17 | if isinstance(x, str): return binascii.hexlify(x) 18 | else: return x 19 | def wrapped(*args, **kwargs): 20 | key = "{0}{1}".format(fn.func_name,tuple(args)) 21 | assert kwargs == {} 22 | result = fn(*args) 23 | assert result is not None 24 | old_result = assertions.get(key) 25 | if old_result is not None: 26 | assert fixbytes(result) == old_result["result"] 27 | assertions[key] = { 28 | "fn": fn.func_name, 29 | "args": map(fixbytes, args), 30 | "result": fixbytes(result), 31 | } 32 | return result 33 | 34 | return wrapped 35 | 36 | # @accumassertions 37 | def expmod(b,e,m): 38 | if e == 0: return 1 39 | t = expmod(b,e/2,m)**2 % m 40 | if e & 1: t = (t*b) % m 41 | return t 42 | 43 | @accumassertions 44 | def inv(x): 45 | return expmod(x,q-2,q) 46 | 47 | d = -121665 * inv(121666) 48 | I = expmod(2,(q-1)/4,q) 49 | 50 | @accumassertions 51 | def xrecover(y): 52 | xx = (y*y-1) * inv(d*y*y+1) 53 | x = expmod(xx,(q+3)/8,q) 54 | if (x*x - xx) % q != 0: x = (x*I) % q 55 | if x % 2 != 0: x = q-x 56 | return x 57 | 58 | By = 4 * inv(5) 59 | Bx = xrecover(By) 60 | B = [Bx % q,By % q] 61 | 62 | @accumassertions 63 | def edwards(P,Q): 64 | x1 = P[0] 65 | y1 = P[1] 66 | x2 = Q[0] 67 | y2 = Q[1] 68 | x3 = (x1*y2+x2*y1) * inv(1+d*x1*x2*y1*y2) 69 | y3 = (y1*y2+x1*x2) * inv(1-d*x1*x2*y1*y2) 70 | return [x3 % q,y3 % q] 71 | 72 | @accumassertions 73 | def scalarmult(P,e): 74 | if e == 0: return [0,1] 75 | Q = scalarmult(P,e/2) 76 | Q = edwards(Q,Q) 77 | if e & 1: Q = edwards(Q,P) 78 | return Q 79 | 80 | @accumassertions 81 | def encodeint(y): 82 | bits = [(y >> i) & 1 for i in range(b)] 83 | return ''.join([chr(sum([bits[i * 8 + j] << j for j in range(8)])) for i in range(b/8)]) 84 | 85 | @accumassertions 86 | def encodepoint(P): 87 | x = P[0] 88 | y = P[1] 89 | bits = [(y >> i) & 1 for i in range(b - 1)] + [x & 1] 90 | return ''.join([chr(sum([bits[i * 8 + j] << j for j in range(8)])) for i in range(b/8)]) 91 | 92 | def bit(h,i): 93 | return (ord(h[i/8]) >> (i%8)) & 1 94 | 95 | def publickey(sk): 96 | h = H(sk) 97 | a = 2**(b-2) + sum(2**i * bit(h,i) for i in range(3,b-2)) 98 | A = scalarmult(B,a) 99 | return encodepoint(A) 100 | 101 | @accumassertions 102 | def Hint(m): 103 | h = H(m) 104 | return sum(2**i * bit(h,i) for i in range(2*b)) 105 | 106 | def signature(m,sk,pk): 107 | h = H(sk) 108 | a = 2**(b-2) + sum(2**i * bit(h,i) for i in range(3,b-2)) 109 | r = Hint(''.join([h[i] for i in range(b/8,b/4)]) + m) 110 | R = scalarmult(B,r) 111 | S = (r + Hint(encodepoint(R) + pk + m) * a) % l 112 | return encodepoint(R) + encodeint(S) 113 | 114 | @accumassertions 115 | def isoncurve(P): 116 | x = P[0] 117 | y = P[1] 118 | return (-x*x + y*y - 1 - d*x*x*y*y) % q == 0 119 | 120 | assert isoncurve(B) 121 | 122 | @accumassertions 123 | def decodeint(s): 124 | return sum(2**i * bit(s,i) for i in range(0,b)) 125 | 126 | @accumassertions 127 | def decodepoint(s): 128 | y = sum(2**i * bit(s,i) for i in range(0,b-1)) 129 | x = xrecover(y) 130 | if x & 1 != bit(s,b-1): x = q-x 131 | P = [x,y] 132 | if not isoncurve(P): raise Exception("decoding point that is not on curve") 133 | return P 134 | 135 | def checkvalid(s,m,pk): 136 | if len(s) != b/4: raise Exception("signature length is wrong") 137 | if len(pk) != b/8: raise Exception("public-key length is wrong") 138 | R = decodepoint(s[0:b/8]) 139 | A = decodepoint(pk) 140 | S = decodeint(s[b/8:b/4]) 141 | h = Hint(encodepoint(R) + pk + m) 142 | BS = scalarmult(B,S) 143 | assert isoncurve(BS) 144 | Ah = scalarmult(A,h) 145 | assert isoncurve(Ah) 146 | RAh = edwards(R, Ah) 147 | assert isoncurve(RAh) 148 | if BS != RAh: 149 | raise Exception("signature does not pass verification") 150 | -------------------------------------------------------------------------------- /solana-bridges/ethereum-client/src/tests/relayer_runs.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use hex_literal::hex; 4 | 5 | pub const RUN_0: [&[u8]; 2] = [ 6 | &hex!("01f9021883020100f90211a08ca2e310045bfbda73175cd3ea9a08c053df98d034753ce150cde60782dd5821a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000001a048ff539a83d056d348d700fecb97bab5e1b43c68af1b8c298c530d9874c168eda056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000830200000184ffc0000180845f930ce999d883010914846765746888676f312e31352e31856c696e7578a0659efd621e3d4d23833ff507b5bb73e246a77842870ab467549ef94fdde4d3cb881bbfe6bac2f49bcc"), 7 | &hex!("02f90211a0d3a6a9eeeb91a8f538a9009abd5e5d672933bb55ad7d61982daf639b97ab9371a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000001a0990f4a811b047d6a51da057f5dee44cbb9e947771f27b434f248b5a97bea5b56a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000830200400284ff80100280845f930ced99d883010914846765746888676f312e31352e31856c696e7578a0546364462cce668ea35bd28098e5a0cb7cbee711c717e2d7ce1a0e0470b1523d88110e7cd4410c9a7e"), 8 | ]; 9 | 10 | pub const RUN_1: [&[u8]; 2] = [ 11 | &hex!("01f9021883020100f90211a08ca2e310045bfbda73175cd3ea9a08c053df98d034753ce150cde60782dd5821a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000001a048ff539a83d056d348d700fecb97bab5e1b43c68af1b8c298c530d9874c168eda056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000830200000184ffc0000180845f930ce999d883010914846765746888676f312e31352e31856c696e7578a0659efd621e3d4d23833ff507b5bb73e246a77842870ab467549ef94fdde4d3cb881bbfe6bac2f49bcc"), 12 | &hex!("02f90211a0d3a6a9eeeb91a8f538a9009abd5e5d672933bb55ad7d61982daf639b97ab9371a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000001a0990f4a811b047d6a51da057f5dee44cbb9e947771f27b434f248b5a97bea5b56a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000830200400284ff80100280845f930ced99d883010914846765746888676f312e31352e31856c696e7578a0546364462cce668ea35bd28098e5a0cb7cbee711c717e2d7ce1a0e0470b1523d88110e7cd4410c9a7e"), 13 | ]; 14 | -------------------------------------------------------------------------------- /solana-bridges/solana-bridges.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: >=1.10 2 | 3 | name: solana-bridges 4 | version: 0.1.0.0 5 | author: Obsidian Systems LLC 6 | maintainer: maintainer@obsidian.systems 7 | build-type: Simple 8 | extra-source-files: solana-bridges/solidity/SolanaClient.sol 9 | 10 | library 11 | hs-source-dirs: src 12 | default-language: Haskell2010 13 | ghc-options: -Wall 14 | exposed-modules: Solana.Relayer 15 | , Solana.Types 16 | , Solana.RPC 17 | , Solana.Utils 18 | , Ethereum.Contracts 19 | , Ethereum.Contracts.Bindings 20 | , Ethereum.Contracts.Dist 21 | , Ethereum.Contracts.TH 22 | 23 | 24 | build-depends: base >=4.13 && <4.14 25 | , aeson 26 | , async 27 | , base16-bytestring 28 | , base58-bytestring 29 | , base64-bytestring 30 | , bytestring 31 | , binary 32 | , cryptonite 33 | , containers 34 | , constraints 35 | , cryptonite 36 | , memory 37 | , data-default 38 | , exceptions 39 | , directory 40 | , file-embed 41 | , lens 42 | , lens-aeson 43 | , mtl 44 | , ethereum-rlp 45 | , network-uri 46 | , process 47 | , process-extras 48 | , split 49 | , time 50 | , template-haskell 51 | , th-lift-instances 52 | , temporary 53 | , transformers 54 | , text 55 | , unix 56 | , web3 57 | , which 58 | , websockets 59 | , network 60 | , http-client 61 | , http-types 62 | , http-client-tls 63 | , hspec 64 | 65 | executable relay-ethereum-to-solana 66 | main-is: RelayEthereumToSolana.hs 67 | hs-source-dirs: src-bin 68 | default-language: Haskell2010 69 | ghc-options: -Wall -threaded 70 | build-depends: base >=4.13 && <4.14 71 | , solana-bridges 72 | 73 | executable relay-solana-to-ethereum 74 | main-is: RelaySolanaToEthereum.hs 75 | hs-source-dirs: src-bin 76 | default-language: Haskell2010 77 | ghc-options: -Wall -threaded 78 | build-depends: base >=4.13 && <4.14 79 | , solana-bridges 80 | 81 | executable deploy-solana-client 82 | main-is: DeploySolanaClient.hs 83 | hs-source-dirs: src-bin 84 | default-language: Haskell2010 85 | ghc-options: -Wall -threaded 86 | build-depends: base >=4.13 && <4.14 87 | , solana-bridges 88 | 89 | executable run-ethereum-testnet 90 | main-is: RunEthereumTestnet.hs 91 | hs-source-dirs: src-bin 92 | default-language: Haskell2010 93 | ghc-options: -Wall -threaded 94 | build-depends: base >=4.13 && <4.14 95 | , solana-bridges 96 | 97 | executable run-solana-testnet 98 | main-is: RunSolanaTestnet.hs 99 | hs-source-dirs: src-bin 100 | default-language: Haskell2010 101 | ghc-options: -Wall -threaded 102 | build-depends: base >=4.13 && <4.14 103 | , solana-bridges 104 | 105 | executable generate-solana-genesis 106 | main-is: GenerateSolanaGenesis.hs 107 | hs-source-dirs: src-bin 108 | default-language: Haskell2010 109 | ghc-options: -Wall -threaded 110 | build-depends: base >=4.13 && <4.14 111 | , solana-bridges 112 | 113 | test-suite contracts 114 | default-language: Haskell2010 115 | type: exitcode-stdio-1.0 116 | main-is: Main.hs 117 | hs-source-dirs: test 118 | ghc-options: -Wall -threaded 119 | build-depends: base 120 | , solana-bridges 121 | , async 122 | , bytestring 123 | , containers 124 | , cryptonite 125 | , data-default 126 | , hspec 127 | , hspec-expectations 128 | , HUnit 129 | , memory 130 | , mtl 131 | , web3 132 | -------------------------------------------------------------------------------- /solana-bridges/test-extras/sign.input.short: -------------------------------------------------------------------------------- 1 | 9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a:d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a::e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b: 2 | 4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c:3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c:72:92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c0072: 3 | f5e5767cf153319517630f226876b86c8160cc583bc013744c6bf255f5cc0ee5278117fc144c72340f67d0f2316e8386ceffbf2b2428c9c51fef7c597f1d426e:278117fc144c72340f67d0f2316e8386ceffbf2b2428c9c51fef7c597f1d426e:08b8b2b733424243760fe426a4b54908632110a66c2f6591eabd3345e3e4eb98fa6e264bf09efe12ee50f8f54e9f77b1e355f6c50544e23fb1433ddf73be84d879de7c0046dc4996d9e773f4bc9efe5738829adb26c81b37c93a1b270b20329d658675fc6ea534e0810a4432826bf58c941efb65d57a338bbd2e26640f89ffbc1a858efcb8550ee3a5e1998bd177e93a7363c344fe6b199ee5d02e82d522c4feba15452f80288a821a579116ec6dad2b3b310da903401aa62100ab5d1a36553e06203b33890cc9b832f79ef80560ccb9a39ce767967ed628c6ad573cb116dbefefd75499da96bd68a8a97b928a8bbc103b6621fcde2beca1231d206be6cd9ec7aff6f6c94fcd7204ed3455c68c83f4a41da4af2b74ef5c53f1d8ac70bdcb7ed185ce81bd84359d44254d95629e9855a94a7c1958d1f8ada5d0532ed8a5aa3fb2d17ba70eb6248e594e1a2297acbbb39d502f1a8c6eb6f1ce22b3de1a1f40cc24554119a831a9aad6079cad88425de6bde1a9187ebb6092cf67bf2b13fd65f27088d78b7e883c8759d2c4f5c65adb7553878ad575f9fad878e80a0c9ba63bcbcc2732e69485bbc9c90bfbd62481d9089beccf80cfe2df16a2cf65bd92dd597b0707e0917af48bbb75fed413d238f5555a7a569d80c3414a8d0859dc65a46128bab27af87a71314f318c782b23ebfe808b82b0ce26401d2e22f04d83d1255dc51addd3b75a2b1ae0784504df543af8969be3ea7082ff7fc9888c144da2af58429ec96031dbcad3dad9af0dcbaaaf268cb8fcffead94f3c7ca495e056a9b47acdb751fb73e666c6c655ade8297297d07ad1ba5e43f1bca32301651339e22904cc8c42f58c30c04aafdb038dda0847dd988dcda6f3bfd15c4b4c4525004aa06eeff8ca61783aacec57fb3d1f92b0fe2fd1a85f6724517b65e614ad6808d6f6ee34dff7310fdc82aebfd904b01e1dc54b2927094b2db68d6f903b68401adebf5a7e08d78ff4ef5d63653a65040cf9bfd4aca7984a74d37145986780fc0b16ac451649de6188a7dbdf191f64b5fc5e2ab47b57f7f7276cd419c17a3ca8e1b939ae49e488acba6b965610b5480109c8b17b80e1b7b750dfc7598d5d5011fd2dcc5600a32ef5b52a1ecc820e308aa342721aac0943bf6686b64b2579376504ccc493d97e6aed3fb0f9cd71a43dd497f01f17c0e2cb3797aa2a2f256656168e6c496afc5fb93246f6b1116398a346f1a641f3b041e989f7914f90cc2c7fff357876e506b50d334ba77c225bc307ba537152f3f1610e4eafe595f6d9d90d11faa933a15ef1369546868a7f3a45a96768d40fd9d03412c091c6315cf4fde7cb68606937380db2eaaa707b4c4185c32eddcdd306705e4dc1ffc872eeee475a64dfac86aba41c0618983f8741c5ef68d3a101e8a3b8cac60c905c15fc910840b94c00a0b9d0:0aab4c900501b3e24d7cdf4663326a3a87df5e4843b2cbdb67cbf6e460fec350aa5371b1508f9f4528ecea23c436d94b5e8fcd4f681e30a6ac00a9704a188a0308b8b2b733424243760fe426a4b54908632110a66c2f6591eabd3345e3e4eb98fa6e264bf09efe12ee50f8f54e9f77b1e355f6c50544e23fb1433ddf73be84d879de7c0046dc4996d9e773f4bc9efe5738829adb26c81b37c93a1b270b20329d658675fc6ea534e0810a4432826bf58c941efb65d57a338bbd2e26640f89ffbc1a858efcb8550ee3a5e1998bd177e93a7363c344fe6b199ee5d02e82d522c4feba15452f80288a821a579116ec6dad2b3b310da903401aa62100ab5d1a36553e06203b33890cc9b832f79ef80560ccb9a39ce767967ed628c6ad573cb116dbefefd75499da96bd68a8a97b928a8bbc103b6621fcde2beca1231d206be6cd9ec7aff6f6c94fcd7204ed3455c68c83f4a41da4af2b74ef5c53f1d8ac70bdcb7ed185ce81bd84359d44254d95629e9855a94a7c1958d1f8ada5d0532ed8a5aa3fb2d17ba70eb6248e594e1a2297acbbb39d502f1a8c6eb6f1ce22b3de1a1f40cc24554119a831a9aad6079cad88425de6bde1a9187ebb6092cf67bf2b13fd65f27088d78b7e883c8759d2c4f5c65adb7553878ad575f9fad878e80a0c9ba63bcbcc2732e69485bbc9c90bfbd62481d9089beccf80cfe2df16a2cf65bd92dd597b0707e0917af48bbb75fed413d238f5555a7a569d80c3414a8d0859dc65a46128bab27af87a71314f318c782b23ebfe808b82b0ce26401d2e22f04d83d1255dc51addd3b75a2b1ae0784504df543af8969be3ea7082ff7fc9888c144da2af58429ec96031dbcad3dad9af0dcbaaaf268cb8fcffead94f3c7ca495e056a9b47acdb751fb73e666c6c655ade8297297d07ad1ba5e43f1bca32301651339e22904cc8c42f58c30c04aafdb038dda0847dd988dcda6f3bfd15c4b4c4525004aa06eeff8ca61783aacec57fb3d1f92b0fe2fd1a85f6724517b65e614ad6808d6f6ee34dff7310fdc82aebfd904b01e1dc54b2927094b2db68d6f903b68401adebf5a7e08d78ff4ef5d63653a65040cf9bfd4aca7984a74d37145986780fc0b16ac451649de6188a7dbdf191f64b5fc5e2ab47b57f7f7276cd419c17a3ca8e1b939ae49e488acba6b965610b5480109c8b17b80e1b7b750dfc7598d5d5011fd2dcc5600a32ef5b52a1ecc820e308aa342721aac0943bf6686b64b2579376504ccc493d97e6aed3fb0f9cd71a43dd497f01f17c0e2cb3797aa2a2f256656168e6c496afc5fb93246f6b1116398a346f1a641f3b041e989f7914f90cc2c7fff357876e506b50d334ba77c225bc307ba537152f3f1610e4eafe595f6d9d90d11faa933a15ef1369546868a7f3a45a96768d40fd9d03412c091c6315cf4fde7cb68606937380db2eaaa707b4c4185c32eddcdd306705e4dc1ffc872eeee475a64dfac86aba41c0618983f8741c5ef68d3a101e8a3b8cac60c905c15fc910840b94c00a0b9d0: 4 | -------------------------------------------------------------------------------- /solana-bridges/ethereum-client/src/types.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::program_error::ProgramError; 2 | 3 | use rlp; 4 | 5 | #[repr(u32)] 6 | #[derive(Debug, Clone, PartialEq, Eq)] 7 | #[cfg_attr(not(test), derive(Copy))] 8 | pub enum CustomError { 9 | IncompleteInstruction, 10 | InvalidInstructionTag, 11 | 12 | #[cfg(not(test))] 13 | DecodeBlockFailed, 14 | #[cfg(not(test))] 15 | DecodeHeaderFailed, 16 | #[cfg(not(test))] 17 | DecodeDifficultyAndHeaderFailed, 18 | #[cfg(not(test))] 19 | DecodePowElementFailed, 20 | #[cfg(not(test))] 21 | DecodeInclusionInstructionFailed, 22 | #[cfg(not(test))] 23 | DecodeChallengeInstructionFailed, 24 | 25 | #[cfg(test)] 26 | DecodeBlockFailed(rlp::DecoderError), 27 | #[cfg(test)] 28 | DecodeHeaderFailed(rlp::DecoderError), 29 | #[cfg(test)] 30 | DecodeDifficultyAndHeaderFailed(rlp::DecoderError), 31 | #[cfg(test)] 32 | DecodePowElementFailed(rlp::DecoderError), 33 | #[cfg(test)] 34 | DecodeInclusionInstructionFailed(rlp::DecoderError), 35 | #[cfg(test)] 36 | DecodeChallengeInstructionFailed(rlp::DecoderError), 37 | 38 | #[allow(non_camel_case_types)] 39 | VerifyHeaderFailed_NonConsecutiveHeight, 40 | #[allow(non_camel_case_types)] 41 | VerifyHeaderFailed_NonMonotonicTimestamp, 42 | #[allow(non_camel_case_types)] 43 | VerifyHeaderFailed_InvalidParentHash, 44 | #[allow(non_camel_case_types)] 45 | VerifyHeaderFailed_TooMuchExtraData, 46 | #[allow(non_camel_case_types)] 47 | VerifyHeaderFailed_InvalidProofOfWork, 48 | 49 | BlockNotFound, 50 | UnpackExtraDataFailed, 51 | InvalidAccountOwner, 52 | DeserializeStorageFailed, 53 | AlreadyInitialized, 54 | WritableHistoryDuringProofCheck, 55 | 56 | #[allow(non_camel_case_types)] 57 | InvalidProof_BadBlockHash, 58 | #[allow(non_camel_case_types)] 59 | InvalidProof_TooEasy, 60 | #[allow(non_camel_case_types)] 61 | InvalidProof_BadMerkle, 62 | 63 | #[allow(non_camel_case_types)] 64 | InvalidChallenge_BadBlockHash, 65 | #[allow(non_camel_case_types)] 66 | InvalidChallenge_InvalidIndex, 67 | #[allow(non_camel_case_types)] 68 | InvalidChallenge_BadMerkleProof, 69 | #[allow(non_camel_case_types)] 70 | InvalidChallenge_BadMerkleRoot, 71 | #[allow(non_camel_case_types)] 72 | InvalidChallenge_SameElement, 73 | 74 | /// This contract has been successfully challenged. It won't do anything 75 | /// anymore. 76 | ContractIsDead, 77 | EthashElementsForWrongBlock, 78 | EthashElementRewriting, 79 | 80 | } 81 | 82 | pub enum DecodeFrom { 83 | Block, 84 | Header, 85 | DifficultyAndHeader, 86 | Inclusion, 87 | Challenge, 88 | PowElement, 89 | } 90 | 91 | impl CustomError { 92 | #[cfg(not(test))] 93 | pub fn from_rlp(t: DecodeFrom, _: rlp::DecoderError) -> Self { 94 | use CustomError::*; 95 | use DecodeFrom::*; 96 | match t { 97 | Block => DecodeBlockFailed, 98 | Header => DecodeHeaderFailed, 99 | DifficultyAndHeader => DecodeDifficultyAndHeaderFailed, 100 | PowElement => DecodePowElementFailed, 101 | Inclusion => DecodeInclusionInstructionFailed, 102 | Challenge => DecodeChallengeInstructionFailed, 103 | } 104 | } 105 | #[cfg(test)] 106 | pub fn from_rlp(t: DecodeFrom, e: rlp::DecoderError) -> Self { 107 | use CustomError::*; 108 | use DecodeFrom::*; 109 | match t { 110 | Block => DecodeBlockFailed(e), 111 | Header => DecodeHeaderFailed(e), 112 | DifficultyAndHeader => DecodeDifficultyAndHeaderFailed(e), 113 | PowElement => DecodePowElementFailed(e), 114 | Inclusion => DecodeInclusionInstructionFailed(e), 115 | Challenge => DecodeChallengeInstructionFailed(e), 116 | } 117 | } 118 | 119 | #[cfg(not(test))] 120 | pub fn to_program_error(self) -> ProgramError { 121 | ProgramError::Custom(self as u32) 122 | } 123 | #[cfg(test)] 124 | pub fn to_program_error(self) -> ProgramError { 125 | use CustomError::*; 126 | ProgramError::Custom(match self { 127 | IncompleteInstruction => 0, 128 | InvalidInstructionTag => 1, 129 | 130 | DecodeBlockFailed(_) => 2, 131 | DecodeHeaderFailed(_) => 3, 132 | DecodeDifficultyAndHeaderFailed(_) => 4, 133 | DecodePowElementFailed(_) => 5, 134 | DecodeInclusionInstructionFailed(_) => 6, 135 | DecodeChallengeInstructionFailed(_) => 7, 136 | 137 | VerifyHeaderFailed_NonConsecutiveHeight => 8, 138 | VerifyHeaderFailed_NonMonotonicTimestamp => 9, 139 | VerifyHeaderFailed_InvalidParentHash => 10, 140 | VerifyHeaderFailed_TooMuchExtraData => 11, 141 | VerifyHeaderFailed_InvalidProofOfWork => 12, 142 | 143 | BlockNotFound => 13, 144 | UnpackExtraDataFailed => 14, 145 | InvalidAccountOwner => 14, 146 | DeserializeStorageFailed => 15, 147 | AlreadyInitialized => 16, 148 | WritableHistoryDuringProofCheck => 17, 149 | 150 | InvalidProof_BadBlockHash => 18, 151 | InvalidProof_TooEasy => 19, 152 | InvalidProof_BadMerkle => 20, 153 | 154 | InvalidChallenge_BadBlockHash => 21, 155 | InvalidChallenge_InvalidIndex => 22, 156 | InvalidChallenge_BadMerkleProof => 23, 157 | InvalidChallenge_BadMerkleRoot => 24, 158 | InvalidChallenge_SameElement => 25, 159 | 160 | ContractIsDead => 26, 161 | EthashElementsForWrongBlock => 27, 162 | EthashElementRewriting => 28, 163 | }) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /solana-bridges/ethereum-client/src/tests/ethash_proof.rs: -------------------------------------------------------------------------------- 1 | // Code taken from https://github.com/near/rainbow-bridge 2 | 3 | //use crate::{DoubleNodeWithMerkleProof, EthClient}; 4 | use arrayref::mut_array_refs; 5 | use ethereum_types::{H128, H256, H512}; 6 | use hex::FromHex; 7 | use serde::{Deserialize, Deserializer}; 8 | use std::path::Path; 9 | 10 | #[derive(Debug)] 11 | struct Hex(pub Vec); 12 | 13 | impl<'de> Deserialize<'de> for Hex { 14 | fn deserialize(deserializer: D) -> Result>::Error> 15 | where 16 | D: Deserializer<'de>, 17 | { 18 | let mut s: String = Deserialize::deserialize(deserializer)?; 19 | if s.starts_with("0x") { 20 | s = s[2..].to_string(); 21 | } 22 | if s.len() % 2 == 1 { 23 | s.insert_str(0, "0"); 24 | } 25 | Ok(Hex(Vec::from_hex(&s).map_err(|err| { 26 | serde::de::Error::custom(err.to_string()) 27 | })?)) 28 | } 29 | } 30 | 31 | #[derive(Debug, Deserialize)] 32 | struct RootsCollectionRaw { 33 | pub dag_merkle_roots: Vec, // H128 34 | } 35 | 36 | #[derive(Debug, Deserialize)] 37 | pub struct RootsCollection { 38 | pub dag_merkle_roots: Vec, 39 | } 40 | 41 | trait HashExt { 42 | fn from_slice_extend(s: &[u8]) -> Self; 43 | } 44 | 45 | // Also handles converting the endianness 46 | macro_rules! impl_from_slice_extend { 47 | ( $name:ident ) => { 48 | impl HashExt for $name { 49 | fn from_slice_extend(s: &[u8]) -> $name { 50 | let mut res = $name::zero(); 51 | assert!(s.len() <= $name::len_bytes()); 52 | res.0[$name::len_bytes() - s.len()..].copy_from_slice(s); 53 | res 54 | } 55 | } 56 | }; 57 | } 58 | 59 | impl_from_slice_extend! { H128 } 60 | impl_from_slice_extend! { H256 } 61 | 62 | impl From for RootsCollection { 63 | fn from(item: RootsCollectionRaw) -> Self { 64 | Self { 65 | dag_merkle_roots: item 66 | .dag_merkle_roots 67 | .iter() 68 | .map(|e| H128::from_slice_extend(&*e.0)) 69 | .collect(), 70 | } 71 | } 72 | } 73 | 74 | #[derive(Debug, Deserialize)] 75 | struct BlockWithProofsRaw { 76 | pub proof_length: u64, 77 | pub header_rlp: Hex, 78 | pub merkle_root: Hex, // H128 79 | pub elements: Vec, // H256 80 | pub merkle_proofs: Vec, // H128 81 | } 82 | 83 | #[derive(Debug, Deserialize)] 84 | pub struct BlockWithProofs { 85 | pub header_rlp: Vec, 86 | pub merkle_root: H128, 87 | pub elements: Vec, 88 | pub merkle_proofs: Vec>, 89 | } 90 | 91 | impl From for BlockWithProofs { 92 | fn from(item: BlockWithProofsRaw) -> Self { 93 | Self { 94 | header_rlp: item.header_rlp.0, 95 | merkle_root: H128::from_slice_extend(&*item.merkle_root.0), 96 | elements: item 97 | .elements 98 | .iter() 99 | .map(|e| H256::from_slice_extend(&*e.0)) 100 | .collect(), 101 | merkle_proofs: item 102 | .merkle_proofs 103 | .chunks(item.proof_length as usize) 104 | .map(|s| s.iter().map(|e| H128::from_slice_extend(&*e.0)).collect()) 105 | .collect(), 106 | } 107 | } 108 | } 109 | 110 | fn combine_dag_h256_to_h512<'a>(elements: &'a [H256]) -> impl Iterator + 'a { 111 | elements 112 | .iter() 113 | .zip(elements.iter().skip(1)) 114 | .enumerate() 115 | .filter(|(i, _)| i % 2 == 0) 116 | .map(|(_, (a, b))| { 117 | let mut buffer = H512::zero(); 118 | { 119 | let (a_r, b_r) = mut_array_refs!(&mut buffer.0, 32, 32); 120 | *a_r = a.0; 121 | *b_r = b.0; 122 | a_r.reverse(); 123 | b_r.reverse(); 124 | } 125 | buffer 126 | }) 127 | } 128 | 129 | impl BlockWithProofs { 130 | pub fn elements_512<'a>(&'a self) -> impl Iterator + 'a { 131 | combine_dag_h256_to_h512(&*self.elements) 132 | } 133 | 134 | //pub fn to_double_node_with_merkle_proof_vec(&self) -> Vec { 135 | // let h512s = Self::combine_dag_h256_to_h512(self.elements.clone()); 136 | // h512s 137 | // .iter() 138 | // .zip(h512s.iter().skip(1)) 139 | // .enumerate() 140 | // .filter(|(i, _)| i % 2 == 0) 141 | // .map(|(i, (a, b))| DoubleNodeWithMerkleProof { 142 | // dag_nodes: vec![*a, *b], 143 | // proof: self.merkle_proofs 144 | // [i / 2 * self.proof_length as usize..(i / 2 + 1) * self.proof_length as usize] 145 | // .to_vec(), 146 | // }) 147 | // .collect() 148 | //} 149 | } 150 | 151 | //// Wish to avoid this code and use web3+rlp libraries directly 152 | //fn rlp_append(header: &Block, stream: &mut RlpStream) { 153 | // stream.begin_list(15); 154 | // stream.append(&header.parent_hash); 155 | // stream.append(&header.uncles_hash); 156 | // stream.append(&header.author); 157 | // stream.append(&header.state_root); 158 | // stream.append(&header.transactions_root); 159 | // stream.append(&header.receipts_root); 160 | // stream.append(&header.logs_bloom); 161 | // stream.append(&header.difficulty); 162 | // stream.append(&header.number.unwrap()); 163 | // stream.append(&header.gas_limit); 164 | // stream.append(&header.gas_used); 165 | // stream.append(&header.timestamp); 166 | // stream.append(&header.extra_data.0); 167 | // stream.append(&header.mix_hash.unwrap()); 168 | // stream.append(&header.nonce.unwrap()); 169 | //} 170 | 171 | pub fn read_block(filename: &Path) -> BlockWithProofs { 172 | read_block_raw(filename).into() 173 | } 174 | 175 | fn read_block_raw(filename: &Path) -> BlockWithProofsRaw { 176 | serde_json::from_reader(std::fs::File::open(filename).unwrap()).unwrap() 177 | } 178 | -------------------------------------------------------------------------------- /solana-bridges/ethereum-client/src/instruction.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | eth::*, 3 | types::*, 4 | pow_proof::*, 5 | }; 6 | use arrayref::array_ref; 7 | 8 | use rlp::{self, Rlp}; 9 | use std::mem::size_of; 10 | 11 | use ethereum_types::{H128, H256, H512, U256}; 12 | 13 | use rlp_derive::{RlpDecodable as RlpDecodableDerive, RlpEncodable as RlpEncodableDerive}; 14 | 15 | use solana_sdk::program_error::ProgramError; 16 | 17 | #[derive(Debug, Eq, PartialEq, Clone, RlpEncodableDerive, RlpDecodableDerive)] 18 | pub struct Initialize { 19 | pub total_difficulty: Box, 20 | pub header: Box, 21 | } 22 | 23 | 24 | #[derive(Debug, Eq, PartialEq, Clone)] 25 | pub struct ProvidePowElement { 26 | /// Height of block for which the elements are used 27 | pub height: u64, 28 | /// offset of a chunk of 8 contiguous elements, in [0, 16) since 16 * 8 is 128. 29 | pub chunk_offset: u8, 30 | pub elements: [H512; Self::ETHASH_ELEMENTS_PER_INSTRUCTION as usize], 31 | } 32 | 33 | impl ProvidePowElement { 34 | pub const ETHASH_ELEMENTS_PER_INSTRUCTION: u8 = 8; 35 | 36 | pub fn new (height: u64, chunk_offset: u8) -> Self { 37 | Self { 38 | height, 39 | chunk_offset, 40 | elements: [H512::zero(); Self::ETHASH_ELEMENTS_PER_INSTRUCTION as usize], 41 | } 42 | } 43 | } 44 | 45 | #[derive(Debug, Eq, PartialEq, Clone, RlpEncodableDerive, RlpDecodableDerive)] 46 | pub struct ProveInclusion { 47 | pub height: u64, 48 | pub block_hash: Box, 49 | pub key: Vec, 50 | pub expected_value: Vec, 51 | pub proof: Vec, 52 | pub min_difficulty: Box, 53 | } 54 | 55 | #[derive(Debug, Eq, PartialEq, Clone, RlpEncodableDerive, RlpDecodableDerive)] 56 | pub struct Challenge { 57 | pub height: u64, 58 | pub block_hash: Box, 59 | /// in access order 60 | pub element_index: u8, 61 | pub merkle_spine: Vec, 62 | pub element_pair: Box, 63 | } 64 | 65 | // TODO don't reallocate for these, and instead lazily parse the instruction. 66 | // That will get the instruction count down while continuing to keep the stack from growing too much 67 | #[derive(Debug)] 68 | pub enum Instruction { 69 | Noop, 70 | Initialize(Box), 71 | NewBlock(Box), 72 | ProvidePowElement(Box), 73 | ProveInclusion(Box), 74 | Challenge(Box), 75 | } 76 | 77 | impl Instruction { 78 | pub fn pack(&self) -> Vec { 79 | let mut buf = Vec::with_capacity(size_of::()); 80 | 81 | match *self { 82 | Self::Noop => { 83 | buf.push(0); 84 | } 85 | Self::Initialize(ref block) => { 86 | buf.push(1); 87 | buf.extend_from_slice(&rlp::encode(block)); 88 | } 89 | Self::NewBlock(ref block) => { 90 | buf.push(2); 91 | buf.extend_from_slice(&rlp::encode(block)); 92 | } 93 | Self::ProvidePowElement(ref block) => { 94 | buf.push(3); 95 | buf.extend_from_slice(&block.height.to_le_bytes()); 96 | buf.push(block.chunk_offset); 97 | for e in &block.elements { 98 | buf.extend_from_slice(&e.to_fixed_bytes()); 99 | } 100 | } 101 | Self::ProveInclusion(ref pi) => { 102 | buf.push(4); 103 | buf.extend_from_slice(&rlp::encode(pi)); 104 | } 105 | Self::Challenge(ref c) => { 106 | buf.push(5); 107 | buf.extend_from_slice(&rlp::encode(c)); 108 | } 109 | } 110 | return buf; 111 | } 112 | 113 | pub fn unpack(input: &[u8]) -> Result { 114 | let mut rest = Parser(input); 115 | let tag = rest.pop()?; 116 | let rlp = Rlp::new(rest.peek()); 117 | return match tag { 118 | 0 => Ok(Self::Noop), 119 | 1 => rlp 120 | .as_val() 121 | .map_err(|e| CustomError::from_rlp(DecodeFrom::DifficultyAndHeader, e)) 122 | .map(Self::Initialize), 123 | 2 => rlp 124 | .as_val() 125 | .map_err(|e| CustomError::from_rlp(DecodeFrom::Header, e)) 126 | .map(Self::NewBlock), 127 | 3 => { 128 | let height_bytes = rest.pop_many(8)?; 129 | let chunk_offset = rest.pop()?; 130 | let mut ppe = ProvidePowElement::new( 131 | u64::from_le_bytes(*array_ref!(height_bytes, 0, 8)), 132 | chunk_offset 133 | ); 134 | for i in 0..ProvidePowElement::ETHASH_ELEMENTS_PER_INSTRUCTION { 135 | let bytes: &[u8; 64] = array_ref!(rest.peek(), 64 * (i as usize), 64); 136 | ppe.elements[i as usize] = H512::from_slice(bytes); 137 | } 138 | Ok(Self::ProvidePowElement(Box::new(ppe))) 139 | }, 140 | 4 => rlp 141 | .as_val() 142 | .map_err(|e| CustomError::from_rlp(DecodeFrom::Inclusion, e)) 143 | .map(Self::ProveInclusion), 144 | 5 => rlp 145 | .as_val() 146 | .map_err(|e| CustomError::from_rlp(DecodeFrom::Challenge, e)) 147 | .map(Self::Challenge), 148 | _ => Err(CustomError::InvalidInstructionTag), 149 | } 150 | .map_err(CustomError::to_program_error); 151 | } 152 | } 153 | 154 | #[derive(Clone, Copy)] 155 | struct Parser<'a>(&'a [u8]); 156 | 157 | impl<'a> Parser<'a> { 158 | fn pop(&mut self) -> Result { 159 | let (&v, new) = self.0 160 | .split_first() 161 | .ok_or(CustomError::IncompleteInstruction.to_program_error())?; 162 | self.0 = new; 163 | Ok(v) 164 | } 165 | fn pop_many(&mut self, n: usize) -> Result<&'a [u8], ProgramError> { 166 | if n > self.0.len() { 167 | return Err(CustomError::IncompleteInstruction.to_program_error()); 168 | } 169 | let (v, new) = self.0 170 | .split_at(n); 171 | self.0 = new; 172 | Ok(v) 173 | } 174 | fn peek(self) -> &'a [u8] { 175 | self.0 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /solana-bridges/ethereum-client/src/ledger_ring_buffer.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | 3 | use solana_program::info; 4 | use solana_sdk::program_error::ProgramError; 5 | 6 | use ethereum_types::U256; 7 | 8 | use crate::{ 9 | eth::BlockHeader, 10 | pow_proof::AccessedElements, 11 | }; 12 | 13 | pub const BLOCKS_OFFSET: usize = mem::size_of::() + mem::size_of::() + 8; // TODO better 14 | pub const MIN_BUF_SIZE: usize = BLOCKS_OFFSET + mem::size_of::(); 15 | 16 | pub const STORAGE_ALIGN: usize = std::mem::align_of::(); 17 | 18 | #[derive(Debug)] 19 | pub struct RingItem { 20 | pub total_difficulty: U256, 21 | pub header: BlockHeader, 22 | pub elements: AccessedElements, 23 | } 24 | 25 | /// Which elements do we *not* have, specified as an (inverted) bitvector of which elements 26 | /// chunks we've received. 27 | /// 00..00: ready for next block 28 | /// otherwise: ready for next chunk for current block 29 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 30 | pub struct ElementChunkSet(pub u16); 31 | 32 | impl ElementChunkSet { 33 | pub const READY_FOR_BLOCK: Self = ElementChunkSet(0); 34 | pub const NEED_ALL_ELEMS: Self = ElementChunkSet(!0); 35 | 36 | pub fn set_has_chunk(&mut self, i: u8) { 37 | self.0 &= !(1 << i); 38 | } 39 | pub fn get_has_chunk(&mut self, i: u8) -> bool { 40 | return (self.0 & (1 << i)) != 0 41 | } 42 | } 43 | 44 | #[derive(Debug)] 45 | #[repr(C)] 46 | pub struct StorageT { 47 | pub height: u64, 48 | pub offset: usize, 49 | pub full: bool, 50 | pub ethash_elements: ElementChunkSet, 51 | pub dead: bool, 52 | pub headers: X, 53 | } 54 | 55 | pub type Storage = StorageT<[RingItem]>; 56 | 57 | // Something sized that can be unsized, useful for some compile time math 58 | pub type StorageScrach = StorageT<[RingItem; 5]>; 59 | 60 | fn guard_sufficient_storage(account: &[u8]) -> Result<(), ProgramError> { 61 | if MIN_BUF_SIZE > account.len() { 62 | info!("Account data length too small for holding state"); 63 | return Err(ProgramError::AccountDataTooSmall); 64 | } 65 | Ok(()) 66 | } 67 | 68 | #[inline] 69 | pub fn interp(raw_data: &[u8]) -> Result<&Storage, ProgramError> { 70 | guard_sufficient_storage(raw_data)?; 71 | let raw_len = raw_data.len(); 72 | let block_len = raw_data[BLOCKS_OFFSET..].len() / mem::size_of::(); 73 | let hacked_data = &raw_data[..block_len]; 74 | // FIXME use proper DST stuff once it exists 75 | let res: &Storage = unsafe { std::mem::transmute(hacked_data) }; 76 | // because no stride != size 77 | debug_assert!( 78 | std::mem::size_of_val(res) <= (raw_len + STORAGE_ALIGN - 1 / STORAGE_ALIGN) * STORAGE_ALIGN 79 | ); 80 | debug_assert_eq!(res.headers.len(), block_len); 81 | Ok(res) 82 | } 83 | 84 | #[inline] 85 | pub fn interp_mut(raw_data: &mut [u8]) -> Result<&mut Storage, ProgramError> { 86 | guard_sufficient_storage(raw_data)?; 87 | let raw_len = raw_data.len(); 88 | let block_len = raw_data[BLOCKS_OFFSET..].len() / mem::size_of::(); 89 | let hacked_data = &mut raw_data[..block_len]; 90 | // FIXME use proper DST stuff once it exists 91 | let res: &mut Storage = unsafe { std::mem::transmute(hacked_data) }; 92 | // because no stride != size 93 | debug_assert!( 94 | std::mem::size_of_val(res) <= (raw_len + STORAGE_ALIGN - 1 / STORAGE_ALIGN) * STORAGE_ALIGN 95 | ); 96 | debug_assert_eq!(res.headers.len(), block_len); 97 | Ok(res) 98 | } 99 | 100 | pub fn min_height(data: &Storage) -> u64 { 101 | let len = data.headers.len(); 102 | match *data { 103 | Storage { 104 | full: false, 105 | offset, 106 | .. 107 | } => data.height - offset as u64 + 1, 108 | Storage { full: true, .. } => data.height - len as u64 + 1, 109 | } 110 | } 111 | 112 | pub fn lowest_offset(data: &Storage) -> usize { 113 | match *data { 114 | Storage { full: false, .. } => 0, 115 | Storage { 116 | full: true, offset, .. 117 | } => offset, 118 | } 119 | } 120 | 121 | pub fn read_block<'a>(data: &'a Storage, idx: usize) -> Result, ProgramError> { 122 | let len = data.headers.len(); 123 | match *data { 124 | Storage { 125 | full: false, 126 | offset, 127 | .. 128 | } if idx < offset => (), 129 | Storage { full: true, .. } if idx < len => (), 130 | _ => return Ok(None), 131 | }; 132 | assert!(data.height != 0); 133 | let ref header = data.headers[idx]; 134 | Ok(Some(header)) 135 | } 136 | 137 | pub fn read_block_mut<'a>( 138 | data: &'a mut Storage, 139 | idx: usize, 140 | ) -> Result, ProgramError> { 141 | let len = data.headers.len(); 142 | match *data { 143 | Storage { 144 | full: false, 145 | offset, 146 | .. 147 | } if idx < offset => (), 148 | Storage { full: true, .. } if idx < len => (), 149 | _ => return Ok(None), 150 | }; 151 | assert!(data.height != 0); 152 | let ref mut header = data.headers[idx]; 153 | Ok(Some(header)) 154 | } 155 | 156 | pub fn read_prev_block<'a>(data: &'a Storage) -> Result, ProgramError> { 157 | let len = data.headers.len(); 158 | read_block(data, (data.offset + (len - 1)) % len) 159 | } 160 | 161 | pub fn read_prev_block_mut<'a>( 162 | data: &'a mut Storage, 163 | ) -> Result, ProgramError> { 164 | let len = data.headers.len(); 165 | read_block_mut(data, (data.offset + (len - 1)) % len) 166 | } 167 | 168 | pub fn write_new_block_unvalidated( 169 | data: &mut Storage, 170 | header: &BlockHeader, 171 | old_total_difficulty_opt: Option<&U256>, 172 | ) -> Result<(), ProgramError> { 173 | let old_offset = data.offset; 174 | 175 | let total_difficulty = match old_total_difficulty_opt { 176 | Some(&d) => d, 177 | None => match read_prev_block(data)? { 178 | None => U256::zero(), 179 | Some(prev_item) => prev_item.total_difficulty + header.difficulty, 180 | }, 181 | }; 182 | 183 | { 184 | let ref mut x = data.headers[old_offset]; 185 | x.header = header.clone(); 186 | x.total_difficulty = total_difficulty; 187 | } 188 | 189 | data.height = header.number; 190 | data.offset = (old_offset + 1) % data.headers.len(); 191 | data.full |= data.offset <= old_offset; 192 | 193 | return Ok(()); 194 | } 195 | -------------------------------------------------------------------------------- /solana-bridges/ethereum-client/src/prove.rs: -------------------------------------------------------------------------------- 1 | use crate::eth::*; 2 | use ethereum_types::{self, H256}; 3 | use rlp::{DecoderError, Rlp}; 4 | use std::cmp::Ordering; 5 | 6 | fn extract_nibbles(a: &[u8]) -> Vec { 7 | a.iter().flat_map(|b| vec![b >> 4, b & 0x0F]).collect() 8 | } 9 | 10 | fn concat_nibbles(a: &[u8]) -> Vec { 11 | a.iter() 12 | .enumerate() 13 | .filter(|(i, _)| i % 2 == 0) 14 | .zip(a.iter().enumerate().filter(|(i, _)| i % 2 == 1)) 15 | .map(|((_, x), (_, y))| (x << 4) | y) 16 | .collect() 17 | } 18 | 19 | pub fn verify_trie_proof<'a, I>( 20 | expected_root: ethereum_types::H256, 21 | key: &[u8], 22 | proof: I, 23 | expected_value: &[u8], 24 | ) -> Result 25 | where 26 | I: ExactSizeIterator>, 27 | { 28 | let mut actual_key = vec![]; 29 | for &el in key { 30 | if actual_key.len() + 1 == proof.len() { 31 | actual_key.push(el); 32 | } else { 33 | actual_key.push(el / 16); 34 | actual_key.push(el % 16); 35 | } 36 | } 37 | 38 | _verify_trie_proof(expected_root, &*actual_key, proof, 0, expected_value) 39 | } 40 | 41 | pub fn _verify_trie_proof<'a, I>( 42 | expected_root: H256, 43 | key: &[u8], 44 | mut proof: I, 45 | key_index: usize, 46 | expected_value: &[u8], 47 | ) -> Result 48 | where 49 | I: Iterator>, 50 | { 51 | let node = proof.next().ok_or(DecoderError::RlpIsTooShort)??; 52 | 53 | let dec = Rlp::new(node); 54 | 55 | if key_index == 0 { 56 | // trie root is always a hash 57 | if keccak256(node) != expected_root { 58 | return Ok(false); 59 | } 60 | } else if node.len() < 32 { 61 | // if rlp < 32 bytes, then it is not hashed 62 | if dec.as_raw() != &expected_root.0 { 63 | return Ok(false); 64 | } 65 | } else { 66 | if keccak256(node) != expected_root { 67 | return Ok(false); 68 | } 69 | } 70 | 71 | match dec.iter().count() { 72 | 17 => { 73 | // branch node 74 | match Ord::cmp(&key_index, &key.len()) { 75 | Ordering::Equal => { 76 | if dec.at(dec.iter().count() - 1)?.data()? == expected_value { 77 | // value stored in the branch 78 | return Ok(true); 79 | } 80 | } 81 | Ordering::Less => { 82 | let new_expected_root = dec.at(key[key_index] as usize)?.data()?; 83 | if new_expected_root.len() != 0 { 84 | return _verify_trie_proof( 85 | H256::from_slice(new_expected_root), 86 | key, 87 | proof, 88 | key_index + 1, 89 | expected_value, 90 | ); 91 | } 92 | } 93 | Ordering::Greater => { 94 | panic!("This should not be reached if the proof has the correct format") 95 | } 96 | } 97 | } 98 | 2 => { 99 | // leaf or extension node 100 | // get prefix and optional nibble from the first byte 101 | let nibbles = extract_nibbles(dec.at(0)?.data()?); 102 | let (prefix, nibble) = (nibbles[0], nibbles[1]); 103 | 104 | match prefix { 105 | 2 => { 106 | // even leaf node 107 | let key_end = &nibbles[2..]; 108 | if concat_nibbles(key_end) == &key[key_index..] 109 | && expected_value == dec.at(1)?.data()? 110 | { 111 | return Ok(true); 112 | } 113 | } 114 | 3 => { 115 | // odd leaf node 116 | let key_end = &nibbles[2..]; 117 | if nibble == key[key_index] 118 | && concat_nibbles(key_end) == &key[key_index + 1..] 119 | && expected_value == dec.at(1)?.data()? 120 | { 121 | return Ok(true); 122 | } 123 | } 124 | 0 => { 125 | // even extension node 126 | let shared_nibbles = &nibbles[2..]; 127 | let extension_length = shared_nibbles.len(); 128 | if concat_nibbles(shared_nibbles) 129 | == &key[key_index..key_index + extension_length] 130 | { 131 | let new_expected_root = dec.at(1)?.data()?; 132 | return _verify_trie_proof( 133 | H256::from_slice(new_expected_root), 134 | key, 135 | proof, 136 | key_index + extension_length, 137 | expected_value, 138 | ); 139 | } 140 | } 141 | 1 => { 142 | // odd extension node 143 | let shared_nibbles = &nibbles[2..]; 144 | let extension_length = 1 + shared_nibbles.len(); 145 | if nibble == key[key_index] 146 | && concat_nibbles(shared_nibbles) 147 | == &key[key_index + 1..key_index + extension_length] 148 | { 149 | let new_expected_root = dec.at(1)?.data()?; 150 | return _verify_trie_proof( 151 | H256::from_slice(new_expected_root), 152 | key, 153 | proof, 154 | key_index + extension_length, 155 | expected_value, 156 | ); 157 | } 158 | } 159 | _ => { 160 | return Err(DecoderError::Custom( 161 | "This should not be reached if the proof has the correct format", 162 | )) 163 | } 164 | } 165 | } 166 | _ => { 167 | return Err(DecoderError::Custom( 168 | "This should not be reached if the proof has the correct format", 169 | )) 170 | } 171 | } 172 | 173 | Ok(expected_value.len() == 0) 174 | } 175 | -------------------------------------------------------------------------------- /solana-bridges/ethereum-client/src/tests/blocks.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use hex_literal::hex; 4 | 5 | pub const HEADER_400000: &[u8] = HEADER_4000XX[0]; 6 | 7 | pub const HEADER_400001: &[u8] = HEADER_4000XX[1]; 8 | 9 | pub const HEADER_4000XX: [&[u8]; 2] = [ 10 | &hex!("f90213a01e77d8f1267348b516ebc4f4da1e2aa59f85f0cbd853949500ffac8bfc38ba14a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942a65aca4d5fc5b5c859090a6c34d164135398226a00b5e4386680f43c224c5c037efc0b645c8e1c3f6b30da0eec07272b4e6f8cd89a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000086057a418a7c3e83061a80832fefd880845622efdc96d583010202844765746885676f312e35856c696e7578a03fbea7af642a4e20cd93a945a1f5e23bd72fc5261153e09102cf718980aeff38886af23caae95692ef"), 11 | &hex!("f90215a05d15649e25d8f3e2c0374946078539d200710afc977cdfc6a977bd23f20fa8e8a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d493479452bc44d5378309ee2abf1539bf71de1b7d7be3b5a09aeed0f1a990a5578fbe75d4404f3011ff8b4c108cb8c5a634e499d153d28488a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000086057af0d2ad9183061a81832fefd880845622efe498d783010202844765746887676f312e342e32856c696e7578a0729654a37843e931a3680a27360115ae0d2f902110e1def46975f651f2e7becb8849ef7c60937788e9"), 12 | ]; 13 | 14 | pub const HEADER_8996776: &[u8] = &hex!("f90215a0f28520c0b577aa94d27bfd84ac15b9a1bd0c97815ca086935fbb6f6fe69681c9a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794ea674fdde714fd979de3edf0f56aa9716b898ec8a0c1277d7c2d1dddedf1b9a49f304bef09f65b531e94cbad2629a7fa88a22690eea08c65778bbc912fd96e116b869a5ffc9b671e473a9872d8edb68cd1b613200d16a0a82e3b139ea78960b0bd858667e067ab9b161a9287aebf5927afd3346fe91ab2b901000c0b52d1276a3048372233e31022d9941b94a599117b481f800079c0921954086c480b076f95da0ef0a011839293035452bb26d30f885a014028c88478283a10c1a5c0b2b131e1896d36105a248068e026366d94948a000c0b7a335c22c03dd85656d90a0e14500cf531431223812a330c007a352608d53029658174090052127d002f2dda01600b962c9421853103940c5199f4436132446f73018eb07468c06a002881a4042080348083d090be5101296720195195083110a942849ac4282718f2520223cab1a2080eb21047a415669e40165187e3109449c4368ada546022a21064781945a9ed804068001815a812984310088012000174b43f5e28f9e0bd87092aa28cbc4930838947a88398833e83983217845ddb678f94505059452d65746865726d696e652d6575312d38a0a1b6535bc565ed913565f8c471ec88ed73f8d59c61009c148913c791a4e3e168887ac6c6600610c8fb"); 15 | 16 | pub const TEST_HEADER_0: &[u8] = &hex!("f9021aa0f779e50b45bc27e4ed236840e5dbcf7afab50beaf553be56bf76da977e10cc73a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d493479452bc44d5378309ee2abf1539bf71de1b7d7be3b5a014c996b6934d7991643669e145b8355c63aa02cbde63d390fcf4e6181d5eea45a079b7e79dc739c31662fe6f25f65bf5a5d14299c7a7aa42c3f75b9fb05474f54ca0e28dc05418692cb7baab7e7f85c1dedb8791c275b797ea3b1ffcaec5ef2aa271b9010000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000010000000000000000000000000000000000000000000000000000000408000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000010000000000000000000000000000000000000000000000000000000400000000000100000000000000000000000000080000000000000000000000000000000000000000000100002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000903234373439353837313930323034343383890fe68395ba8e82d0d9845dd84a079150505945206e616e6f706f6f6c2e6f7267a0a35425f443452cf94ba4b698b00fd7b3ff4fc671dea3d5cc2dcbedbc3766f45e88af7fec6031063a17"); 17 | 18 | pub const TEST_BLOCK_1_TX: &[u8] = &hex!("f904eaf90213a0c89928efed5db6530c482c236da3aaeaba6435a2450a975e9b9f1f5ff6941723a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000001a0f0bf02aac82e0961d87a128569740012d6e2ec99a395157ba97709a9de950fe2a04e4964659ef22d9ecee734c5f7b8bcd00680b6329206da84ae388c383f905cb0a0777f1c1c378807634128348e4f0eeca6a0e7f516ea411690ca04266323f671a4b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008302004002833007cf830186a0845f7b5d9399d883010914846765746888676f312e31352e31856c696e7578a0c4bb1584988635f3c191eb599e2c05f450488df962904171a5547ead9131e3f9881450280dc437cf3cf902d0f902cd8001830186a08001b9027c3630383036303430353233343830313536313030313035373630303038306664356235303631303131653830363130303230363030303339363030306633666536303830363034303532333438303135363030663537363030303830666435623530363030343336313036303238353736303030333536306530316338303633633630356637366331343630326435373562363030303830666435623630333336306162353635623630343035313830383036303230303138323831303338323532383338313831353138313532363032303031393135303830353139303630323030313930383038333833363030303562383338313130313536303731353738303832303135313831383430313532363032303831303139303530363035383536356235303530353035303930353039303831303139303630316631363830313536303964353738303832303338303531363030313833363032303033363130313030306130333139313638313532363032303031393135303562353039323530353035303630343035313830393130333930663335623630363036303430353138303630343030313630343035323830363030643831353236303230303137663438363536633663366632633230353736663732366336343231303030303030303030303030303030303030303030303030303030303030303030303030303038313532353039303530393035366665613236343639373036363733353832323132323063346466366139393637666230336633323038653966383534623236643635626338343665323134393963646363333135303639313431653530623036623165363437333666366336333433303030363038303033338325ad31a06be9f7bacbbc298818438802d6c202df6084649643afce090e017f1cb37c3618a031fc123f349bdb40ccf39a159a31810d0cc6cff00a920a75c4d97cad8c36c938c0"); 19 | 20 | pub const TEST_BLOCK_0_TX: &[u8] = &hex!("f90215f90210a0d08f55a1789e660d82802ae3970130beb9736fc1b36c2e15f4589467dbdea06ca01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000001a0c4f67a7baaa163869ad9461c00cca706e317ca4e4ff4e22e4843ae2af0960003a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000830200c00483301fd280845f7b762399d883010914846765746888676f312e31352e31856c696e7578a059249dd6f99033815acf89b66da4b44e5e93d588e85e4549009152f4a513601b884983ec06833e1e43c0c0"); 21 | -------------------------------------------------------------------------------- /solana-bridges/ethereum-client/src/processor.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "program")] 2 | 3 | use ethereum_types::U256; 4 | 5 | use rlp::Rlp; 6 | 7 | use solana_program::info; 8 | use solana_sdk::{ 9 | account_info::{next_account_info, AccountInfo}, 10 | entrypoint_deprecated::ProgramResult, 11 | program_error::ProgramError, 12 | pubkey::Pubkey, 13 | }; 14 | 15 | use crate::{ 16 | eth::*, 17 | instruction::*, 18 | ledger_ring_buffer::*, 19 | pow_proof::*, 20 | prove::*, 21 | types::*, 22 | }; 23 | 24 | pub fn process_instruction<'a>( 25 | program_id: &Pubkey, 26 | accounts: &[AccountInfo<'a>], 27 | instruction_data: &[u8], 28 | ) -> ProgramResult { 29 | info!("Ethereum light client entrypoint"); 30 | 31 | let accounts_iter = &mut accounts.iter(); 32 | let account = next_account_info(accounts_iter)?; 33 | 34 | if account.owner != program_id { 35 | info!("Account does not have the correct program id"); 36 | return Err(ProgramError::IncorrectProgramId); 37 | } 38 | 39 | { 40 | let raw_data = account.try_borrow_data()?; 41 | let data = interp(&*raw_data)?; 42 | if data.dead { 43 | return Err(CustomError::ContractIsDead.to_program_error()); 44 | } 45 | } 46 | 47 | 48 | let instr = Instruction::unpack(instruction_data)?; 49 | //println!("{:#?}", instr); 50 | 51 | Ok(match instr { 52 | Instruction::Noop => {} 53 | Instruction::Initialize(item) => { 54 | if !account.is_signer { 55 | info!("Account does not have the correct program id"); 56 | return Err(ProgramError::MissingRequiredSignature); 57 | } 58 | 59 | let mut raw_data = account.try_borrow_mut_data()?; 60 | let ref mut data = *interp_mut(&mut *raw_data)?; 61 | 62 | match data { 63 | Storage { 64 | height: 0, 65 | offset: 0, 66 | full: false, 67 | .. 68 | } => (), 69 | _ => return Err(CustomError::AlreadyInitialized.to_program_error()), 70 | }; 71 | verify_block(&item.header, None).map_err(CustomError::to_program_error)?; 72 | 73 | write_new_block(data, &item.header, Some(&item.total_difficulty))?; 74 | } 75 | Instruction::NewBlock(header) => { 76 | let mut raw_data = account.try_borrow_mut_data()?; 77 | let ref mut data = *interp_mut(&mut *raw_data)?; 78 | 79 | let parent = 80 | read_prev_block(data)?.ok_or(CustomError::BlockNotFound.to_program_error())?; 81 | verify_block(&header, Some(&parent.header)).map_err(CustomError::to_program_error)?; 82 | 83 | write_new_block(data, &header, None)?; 84 | } 85 | Instruction::ProvidePowElement(ppe) => { 86 | let mut raw_data = account.try_borrow_mut_data()?; 87 | let ref mut data = *interp_mut(&mut *raw_data)?; 88 | //println!("{} {:?}", ppe.chunk_offset, data.ethash_elements); 89 | if ppe.height != data.height { 90 | return Err(CustomError::EthashElementsForWrongBlock.to_program_error()) 91 | } 92 | let mut bit_vec = data.ethash_elements; 93 | let block = read_prev_block_mut(data)? 94 | .ok_or(CustomError::BlockNotFound.to_program_error())?; 95 | for i in 0..ProvidePowElement::ETHASH_ELEMENTS_PER_INSTRUCTION { 96 | let offset = ppe.chunk_offset * ProvidePowElement::ETHASH_ELEMENTS_PER_INSTRUCTION; 97 | let new_value = ppe.elements[i as usize]; 98 | 99 | match block.elements[offset + i].value { 100 | _ if bit_vec.get_has_chunk(ppe.chunk_offset) => block.elements[offset + i].value = new_value, 101 | h if h == new_value => (), 102 | _ => return Err(CustomError::EthashElementRewriting.to_program_error()), 103 | } 104 | } 105 | bit_vec.set_has_chunk(ppe.chunk_offset); 106 | if bit_vec != ElementChunkSet::READY_FOR_BLOCK { 107 | // keep waiting for elements 108 | } else { 109 | // We have all the elements now, verify PoW 110 | let pow_valid = verify_pow_indexes(block); 111 | if !pow_valid { 112 | return Err(CustomError::VerifyHeaderFailed_InvalidProofOfWork 113 | .to_program_error()); 114 | } 115 | } 116 | data.ethash_elements = bit_vec; 117 | } 118 | 119 | Instruction::ProveInclusion(pi) => { 120 | if account.is_writable { 121 | return Err(CustomError::WritableHistoryDuringProofCheck.to_program_error()); 122 | } 123 | let raw_data = account.try_borrow_data()?; 124 | let data = interp(&*raw_data)?; 125 | 126 | let block = find_block(&data, pi.height)?; 127 | if &hash_header(&block.header, false) != &*pi.block_hash { 128 | return Err(CustomError::InvalidProof_BadBlockHash.to_program_error()); 129 | } 130 | 131 | if &block.total_difficulty < &*pi.min_difficulty { 132 | return Err(CustomError::InvalidProof_TooEasy.to_program_error()); 133 | } 134 | let expected_root = block.header.receipts_root; // pi.block_hash 135 | let rlp = Rlp::new(&*pi.proof); 136 | let proof = rlp.iter().map(|rlp| rlp.data()); 137 | verify_trie_proof(expected_root, &*pi.key, proof, &*pi.expected_value) 138 | .map_err(|_| CustomError::InvalidProof_BadMerkle.to_program_error())?; 139 | } 140 | Instruction::Challenge(challenge) => { 141 | let mut raw_data = account.try_borrow_mut_data()?; 142 | let data = interp_mut(&mut *raw_data)?; 143 | 144 | let block = find_block(&data, challenge.height)?; 145 | 146 | if &hash_header(&block.header, false) != &*challenge.block_hash { 147 | return Err(CustomError::InvalidChallenge_BadBlockHash.to_program_error()); 148 | } 149 | 150 | if challenge.element_index >= 64 { 151 | panic!("element pair index must be between 0 and 64") 152 | } 153 | 154 | let challenged_0 = &block.elements[challenge.element_index * 2]; 155 | let challenged_1 = &block.elements[challenge.element_index * 2 + 1]; 156 | 157 | // Make sure addresses are in the form (n, n + 1) (failure would 158 | // indicate contract bug not bad input.) 159 | if challenged_0.address + 1 != challenged_1.address { 160 | panic!("non-consecutive addresses") 161 | } 162 | 163 | let found = Box::new(ElementPair { 164 | e0: challenged_0.value, 165 | e1: challenged_1.value, 166 | }); 167 | 168 | if challenge.element_pair == found { 169 | return Err(CustomError::InvalidChallenge_SameElement.to_program_error()); 170 | } 171 | 172 | let wanted_merkle_root = get_wanted_merkle_root(challenge.height); 173 | 174 | let got_merkle_root = apply_pow_element_merkle_proof( 175 | &challenge.element_pair, 176 | &*challenge.merkle_spine, 177 | challenged_0.address, 178 | ); 179 | 180 | if got_merkle_root != wanted_merkle_root { 181 | return Err(CustomError::InvalidChallenge_BadMerkleRoot.to_program_error()); 182 | } 183 | 184 | let dst_account = next_account_info(accounts_iter)?; 185 | 186 | give_bounty_to_challenger(account, dst_account)?; 187 | 188 | data.dead = true; 189 | } 190 | }) 191 | } 192 | 193 | pub fn find_block<'a>(data: &'a Storage, height: u64) -> Result<&'a RingItem, ProgramError> { 194 | let min_h = min_height(data); 195 | if min_h > height { 196 | //panic!("too old {} {}", min_h, height) 197 | return Err(CustomError::BlockNotFound.to_program_error()); 198 | } 199 | let mut max_h = data.height; 200 | if data.ethash_elements != ElementChunkSet::READY_FOR_BLOCK { 201 | // last block doesn't have all it's elements 202 | max_h -= 1; 203 | } 204 | if max_h < height { 205 | //panic!("too new {} {}", max_h, height); 206 | return Err(CustomError::BlockNotFound.to_program_error()); 207 | } 208 | let offset = lowest_offset(data) + (height - min_h) as usize % data.headers.len(); 209 | 210 | // TODO: Check that we've actually run the PoW for this one 211 | 212 | read_block(data, offset)?.ok_or(CustomError::BlockNotFound.to_program_error()) 213 | } 214 | 215 | pub fn give_bounty_to_challenger(src_account: &AccountInfo, dst_account: &AccountInfo) -> ProgramResult { 216 | **dst_account.lamports.borrow_mut() += src_account.lamports(); 217 | **src_account.lamports.borrow_mut() = 0; 218 | Ok(()) 219 | } 220 | 221 | pub fn write_new_block( 222 | data: &mut Storage, 223 | header: &BlockHeader, 224 | old_total_difficulty_opt: Option<&U256>, 225 | ) -> Result<(), ProgramError> { 226 | if data.ethash_elements != ElementChunkSet::READY_FOR_BLOCK { 227 | panic!("expected PoW element for previous block, but we're trying to write a new block") 228 | } 229 | write_new_block_unvalidated(data, header, old_total_difficulty_opt)?; 230 | data.ethash_elements = ElementChunkSet::NEED_ALL_ELEMS; 231 | Ok(()) 232 | } 233 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | let 2 | nixpkgs = import ./dep/nixpkgs { overlays = [overlay]; }; 3 | 4 | nix-thunk = import ./dep/nix-thunk { pkgs = nixpkgs; }; 5 | 6 | # Per thunk notes: 7 | # 8 | # gitignore.nix: Not on nixpkgs: https://github.com/hercules-ci/gitignore.nix/issues/6 9 | # 10 | # which: Hackage release (0.1.0.0) does not support GHC 8.8 11 | sources = nix-thunk.mapSubdirectories nix-thunk.thunkSource ./dep; 12 | 13 | gitignoreSource = (import sources."gitignore.nix" {}).gitignoreSource; 14 | 15 | overlay = self: super: { 16 | haskellPackages = with nixpkgs.haskell.lib; 17 | super.haskellPackages.override (old: { 18 | overrides = self: super: with nixpkgs.haskell.lib; { 19 | solana-bridges = overrideCabal (self.callCabal2nix "solana-bridges" (gitignoreSource ./solana-bridges) {}) (drv: { 20 | executableSystemDepends = (drv.executableSystemDepends or []) ++ [solana solana-client-tool ethashproof] ++ (with nixpkgs; [ go-ethereum solc ]); 21 | }); 22 | http-client-tls = doJailbreak (super.http-client-tls); 23 | web3 = doJailbreak (dontCheck (self.callCabal2nix "web3" sources.hs-web3 {})); 24 | which = self.callCabal2nix "which" sources.which {}; 25 | }; 26 | }); 27 | }; 28 | 29 | solana = with nixpkgs; rustPlatform.buildRustPackage rec { 30 | pname = "solana"; 31 | version = "v1.3.9"; 32 | 33 | # TODO: upstream 34 | src = fetchFromGitHub { 35 | owner = "obsidiansystems"; 36 | repo = pname; 37 | rev = "db2f8ec4fc7b9ccbfdc68ace67d767dbac9330dd"; # branch: debug-elf 38 | sha256 = "0ffih3armr6fdys40dzdc913rkpaxrgyfiw7030kp0nqbarhr0d4"; 39 | }; 40 | 41 | cargoSha256 = "1hdphhl6acj48z11ciznisb826yk8njv79ri46yzznybx6bqybrh"; 42 | verifyCargoDeps = true; 43 | 44 | LIBCLANG_PATH="${llvmPackages.libclang}/lib"; 45 | nativeBuildInputs = [ pkgconfig clang llvm ]; 46 | buildInputs = [ libudev openssl ]; 47 | strictDeps = true; 48 | 49 | # TODO: Either allow the nix build to increase file descriptor limit or patch out failing test 50 | # 51 | ## [2020-09-09T18:23:04.195996780Z ERROR solana_ledger::blockstore] Unable to increase the maximum open file descriptor limit to 500000 52 | ## test test_bench_tps_local_cluster_solana ... FAILED 53 | checkPhase = null; 54 | }; 55 | 56 | solana-rust-bpf = with nixpkgs; stdenv.mkDerivation { 57 | name = "solana-rust-bpf"; 58 | src = fetchTarball { 59 | name = "solana-rust-bpf-linux"; 60 | url = "https://github.com/solana-labs/rust-bpf-builder/releases/download/v0.2.3/solana-rust-bpf-linux.tar.bz2"; 61 | sha256 = "0cbwrjwbvd2dyq4w1gnh8d7yyzywqx2k8f32h03z53fmcwldcj1g"; 62 | }; 63 | nativeBuildInputs = [ autoPatchelfHook openssl stdenv.cc.cc.lib ]; 64 | installPhase = '' 65 | cp -R $src $out 66 | ''; 67 | }; 68 | 69 | solana-llvm = with nixpkgs; stdenv.mkDerivation { 70 | name = "solana-llvm"; 71 | src = fetchTarball { 72 | url = "https://github.com/solana-labs/llvm-builder/releases/download/v0.0.15/solana-llvm-linux.tar.bz2"; 73 | sha256 = "09bfj3jg97d2xh9c036xynff0fpg648vhg4sva0sabi6rpzp2c8r"; 74 | }; 75 | nativeBuildInputs = [ autoPatchelfHook stdenv.cc.cc.lib ]; 76 | installPhase = '' 77 | cp -R $src $out 78 | ''; 79 | }; 80 | 81 | solc = nixpkgs.solc.overrideAttrs (old: { 82 | # https://github.com/NixOS/nixpkgs/pull/97730 83 | checkPhase = null; 84 | }); 85 | 86 | xargo = with nixpkgs; rustPlatform.buildRustPackage rec { 87 | pname = "xargo"; 88 | 89 | version = "v0.3.22"; 90 | 91 | src = fetchFromGitHub { 92 | owner = "japaric"; 93 | repo = pname; 94 | rev = "b7cec9d3dc3720f0b7964f4b6e3a1878f94e4c07"; 95 | sha256 = "0m1dg7vwmmlpqp20p219gsm7zbnnii6lik6hc2vvfsdmnygf271l"; 96 | }; 97 | 98 | cargoSha256 = "0jn9flcw5vqvqqm16vxzywqcz47mgbhdh73l6a5f5nxr4m00yy9i"; 99 | verifyCargoDeps = true; 100 | 101 | # TODO: allow tests to run in debug in nixpkgs 102 | # error[E0554]: `#![feature]` may not be used on the stable release channel 103 | # --> tests/smoke.rs:3:1 104 | # buildType = "debug"; 105 | checkPhase = null; 106 | strictDeps = true; 107 | buildInputs = [ makeWrapper ]; 108 | 109 | postInstall = '' 110 | wrapProgram $out/bin/xargo \ 111 | --set-default RUST_BACKTRACE FULL \ 112 | ''; 113 | }; 114 | 115 | rust-bpf-sysroot = with nixpkgs; fetchFromGitHub { 116 | owner = "solana-labs"; 117 | repo = "rust-bpf-sysroot"; 118 | rev = "b4dc90e3ee8a88f197876bc76149add1de7fec25"; # branch v0.12 119 | sha256 = "1jiw61bdxb10s2xnf9lcw8aqra35vq2a95kk01kz72kqm63rijy8"; 120 | fetchSubmodules = true; 121 | }; 122 | 123 | # TODO build these properly 124 | spl = with nixpkgs; { 125 | token = fetchurl { 126 | url = "https://github.com/solana-labs/solana-program-library/releases/download/token-v2.0.3/spl_token.so"; 127 | sha256 = "0qnkyapd033nbnqsm1hcyrr47pb6kpk9dz88i6j2wqbwhgbqxvp5"; 128 | }; 129 | memo = fetchurl { 130 | url = "https://github.com/solana-labs/solana-program-library/releases/download/memo-v1.0.0/spl_memo.so"; 131 | sha256 = "0fy664ciriinnk0x6kvsa2wr48prnnrcvlg8g06jpc62kkapn2cv"; 132 | }; 133 | }; 134 | 135 | shell = nixpkgs.haskellPackages.shellFor { 136 | withHoogle = false; # https://github.com/NixOS/nixpkgs/issues/82245 137 | packages = p: with p; [ solana-bridges ]; 138 | nativeBuildInputs = [ solana-rust-bpf solc ] ++ (with nixpkgs; 139 | [ cabal-install ghcid hlint 140 | go-ethereum solana 141 | xargo cargo-deps cargo-watch rustfmt clippy 142 | shellcheck ninja cmake 143 | jq 144 | solana-client-tool 145 | ]); 146 | 147 | RUST_BACKTRACE="1"; 148 | XARGO_RUST_SRC="${rust-bpf-sysroot}/src"; 149 | RUST_COMPILER_RT_ROOT="${rust-bpf-sysroot}/src/compiler-rt"; 150 | 151 | SPL_TOKEN=spl.token; 152 | SPL_MEMO=spl.memo; 153 | 154 | SOLANA_LLVM_CC="${solana-llvm}/bin/clang"; # CC gets overwritten 155 | SOLANA_LLVM_AR="${solana-llvm}/bin/llvm-ar"; # AR gets overwritten 156 | 157 | CARGO_TARGET_DIR="target-bpf"; 158 | 159 | # Get bpf.ld from npm? 160 | RUSTFLAGS=" 161 | -C lto=no \ 162 | -C opt-level=2 \ 163 | -C link-arg=-z -C link-arg=notext \ 164 | -C link-arg=-T${rust-bpf-sysroot}/bpf.ld \ 165 | -C link-arg=--Bdynamic \ 166 | -C link-arg=-shared \ 167 | -C link-arg=--entry=entrypoint \ 168 | -C link-arg=-no-threads \ 169 | -C linker=${solana-llvm}/bin/ld.lld"; 170 | }; 171 | 172 | shells = { 173 | ethereum-client-bpf = shell; 174 | 175 | ethereum-client-x86 = nixpkgs.mkShell { 176 | nativeBuildInputs = with nixpkgs.buildPackages; [ rustc cargo cargo-deps cargo-watch clippy rustfmt ]; 177 | }; 178 | 179 | solana-client-evm = with nixpkgs; mkShell { 180 | buildInputs = [ inotify-tools go-ethereum solc ]; 181 | }; 182 | 183 | scripts = with nixpkgs; mkShell { 184 | buildInputs = [ solana solana-client-tool ]; 185 | }; 186 | }; 187 | 188 | ethereum-client-src = gitignoreSource ./solana-bridges/ethereum-client; 189 | 190 | # Cargo hash must be updated when Cargo.lock file changes. 191 | ethereum-client-dep-sha256 = "00xyzzdnm4wkp65bqq04v6arg0zrq1nzxc79xd0yp8449kw2gijv"; 192 | ethereum-client-dep-srcs = nixpkgs.rustPlatform.fetchCargoTarball { 193 | name = "ethereum-client"; 194 | src = ethereum-client-src; 195 | sourceRoot = null; 196 | sha256 = ethereum-client-dep-sha256; 197 | }; 198 | 199 | # TODO: https://github.com/NixOS/nixpkgs/pull/95542/files 200 | mk-ethereum-client = cargoBuildFlags: nixpkgs.rustPlatform.buildRustPackage { 201 | name = "ethereum-client"; 202 | src = ethereum-client-src; 203 | #cargoVendorDir = ethereum-client-dep-srcs; 204 | #nativeBuildInputs = [ pkgs.openssl pkgs.pkgconfig ]; 205 | #buildInputs = [ rustPackages.rust-std ]; 206 | verifyCargoDeps = true; 207 | 208 | inherit cargoBuildFlags; 209 | 210 | cargoSha256 = ethereum-client-dep-sha256; 211 | }; 212 | 213 | solana-client-tool = (import ./solana-client-tool {pkgs = nixpkgs;}).package; 214 | 215 | ethereum-client-no-prog = mk-ethereum-client [ ]; 216 | 217 | ethereum-client-prog = mk-ethereum-client [ "--features" "program" ]; 218 | 219 | withSPLEnv = binName: nixpkgs.runCommand binName { 220 | nativeBuildInputs = [ nixpkgs.makeWrapper ]; 221 | } '' 222 | mkdir -p $out/bin 223 | makeWrapper "${nixpkgs.haskellPackages.solana-bridges}/bin/${binName}" "$out/bin/${binName}" \ 224 | --set-default SPL_TOKEN "${spl.token}" \ 225 | --set-default SPL_MEMO "${spl.memo}" 226 | ''; 227 | 228 | generate-solana-genesis = withSPLEnv "generate-solana-genesis"; 229 | 230 | run-solana-testnet = withSPLEnv "run-solana-testnet"; 231 | 232 | ethashproof = with nixpkgs; buildGoModule rec { 233 | name = "ethashproof"; 234 | runVend = true; 235 | 236 | src = fetchFromGitHub { 237 | owner = "tranvictor"; 238 | repo = name; 239 | rev = "82a2b716eac4965709898a3dae791b4bace0999a"; 240 | sha256 = "0xahaiv9i289lp76c0zb68qbz8xk3r2r0grl85zhbk2iykmg6jby"; 241 | }; 242 | vendorSha256 = "0chs20pgcxg2wf7y3ppsqfzihhwgaqlrb58f9j56gfpz0va5ysm4"; 243 | }; 244 | 245 | in { 246 | inherit nixpkgs shell shells solc solana solana-rust-bpf solana-llvm spl 247 | ethereum-client-prog 248 | ethereum-client-no-prog 249 | ethereum-client-dep-srcs 250 | generate-solana-genesis 251 | solana-client-tool 252 | run-solana-testnet 253 | ethashproof 254 | ; 255 | inherit (nixpkgs.haskellPackages) solana-bridges; 256 | } 257 | -------------------------------------------------------------------------------- /solana-bridges/ethereum-client/src/eth.rs: -------------------------------------------------------------------------------- 1 | use ethereum_types::{Bloom, H160, H256, H512, H64, U256}; 2 | use rlp::{Decodable, DecoderError, Encodable, Rlp, RlpStream}; 3 | use rlp_derive::{RlpDecodable as RlpDecodableDerive, RlpEncodable as RlpEncodableDerive}; 4 | use std::{result::Result, vec::Vec}; 5 | 6 | use tiny_keccak::{Hasher, Keccak}; 7 | 8 | use crate::types::*; 9 | 10 | pub const EXTRA_DATA_MAX_LEN: usize = 32; 11 | 12 | pub const EPOCH_LENGTH: u64 = 30000; 13 | 14 | #[derive(Debug, Clone, Copy)] 15 | pub struct ExtraData { 16 | len: u8, 17 | bytes: [u8; EXTRA_DATA_MAX_LEN], 18 | } 19 | 20 | #[derive(Debug, Eq, PartialEq, Clone)] 21 | pub struct BlockHeader { 22 | pub parent_hash: H256, 23 | pub uncles_hash: H256, 24 | pub author: H160, 25 | pub state_root: H256, 26 | pub transactions_root: H256, 27 | pub receipts_root: H256, 28 | pub log_bloom: Bloom, 29 | pub difficulty: U256, 30 | pub number: u64, 31 | pub gas_limit: U256, 32 | pub gas_used: U256, 33 | pub timestamp: u64, 34 | pub extra_data: ExtraData, 35 | pub mix_hash: H256, 36 | pub nonce: H64, 37 | } 38 | 39 | #[derive(Debug, Eq, PartialEq, Clone, RlpEncodableDerive, RlpDecodableDerive)] 40 | pub struct Receipt { 41 | pub status: bool, 42 | pub gas_used: U256, 43 | pub log_bloom: Bloom, 44 | pub logs: Vec, 45 | } 46 | 47 | #[derive(Debug, Eq, PartialEq, Clone)] 48 | pub struct LogEntry { 49 | pub address: H160, 50 | pub topics: Vec, 51 | pub data: Vec, 52 | } 53 | 54 | impl rlp::Decodable for LogEntry { 55 | fn decode(rlp: &rlp::Rlp) -> Result { 56 | let result = LogEntry { 57 | address: rlp.val_at(0)?, 58 | topics: rlp.list_at(1)?, 59 | data: rlp.val_at(2)?, 60 | }; 61 | Ok(result) 62 | } 63 | } 64 | 65 | impl rlp::Encodable for LogEntry { 66 | fn rlp_append(&self, stream: &mut rlp::RlpStream) { 67 | stream.begin_list(3usize); 68 | stream.append(&self.address); 69 | stream.append_list::(&self.topics); 70 | stream.append(&self.data); 71 | } 72 | } 73 | 74 | //TODO: determine maximum widths to support per field 75 | type Scalar = U256; 76 | 77 | pub struct TransactionData { 78 | pub bytes: Vec, 79 | } 80 | 81 | pub enum TransactionAction { 82 | Call(H160), //TODO: transfer? 83 | Create, 84 | } 85 | 86 | impl Encodable for TransactionAction { 87 | fn rlp_append(&self, stream: &mut RlpStream) { 88 | match self { 89 | &TransactionAction::Call(address) => stream.append(&address), 90 | &TransactionAction::Create => stream.begin_list(0), 91 | }; 92 | } 93 | } 94 | 95 | impl Decodable for TransactionAction { 96 | fn decode(rlp: &Rlp) -> Result { 97 | Ok(if rlp.is_empty() { 98 | TransactionAction::Create 99 | } else { 100 | TransactionAction::Call(rlp.as_val()?) 101 | }) 102 | } 103 | } 104 | 105 | pub struct Transaction { 106 | pub nonce: Scalar, 107 | pub gas_price: Scalar, 108 | pub gas_limit: Scalar, 109 | pub to: TransactionAction, 110 | pub value: Scalar, 111 | pub data: TransactionData, 112 | pub v: U256, 113 | pub r: U256, 114 | pub s: U256, 115 | } 116 | 117 | impl Encodable for Transaction { 118 | fn rlp_append(&self, stream: &mut RlpStream) { 119 | stream.begin_list(9); 120 | stream.append(&self.nonce); 121 | stream.append(&self.gas_price); 122 | stream.append(&self.gas_limit); 123 | stream.append(&self.to); 124 | stream.append(&self.value); 125 | stream.append(&self.data.bytes); 126 | stream.append(&self.v); 127 | stream.append(&self.r); 128 | stream.append(&self.s); 129 | } 130 | } 131 | 132 | impl Decodable for Transaction { 133 | fn decode(serialized: &Rlp) -> Result { 134 | let res = Transaction { 135 | nonce: serialized.val_at(0)?, 136 | gas_price: serialized.val_at(1)?, 137 | gas_limit: serialized.val_at(2)?, 138 | to: serialized.val_at(3)?, 139 | value: serialized.val_at(4)?, 140 | data: TransactionData { 141 | bytes: serialized.val_at(5)?, 142 | }, 143 | v: serialized.val_at(6)?, 144 | r: serialized.val_at(7)?, 145 | s: serialized.val_at(8)?, 146 | }; 147 | return Ok(res); 148 | } 149 | } 150 | 151 | pub struct Block { 152 | pub header: BlockHeader, 153 | pub transactions: Vec, 154 | } 155 | 156 | impl Decodable for Block { 157 | fn decode(serialized: &Rlp) -> Result { 158 | let res = Block { 159 | header: serialized.val_at(0)?, 160 | transactions: serialized.list_at(1)?, 161 | }; 162 | return Ok(res); 163 | } 164 | } 165 | 166 | impl Encodable for Block { 167 | fn rlp_append(&self, stream: &mut RlpStream) { 168 | stream.begin_list(2); 169 | stream.append(&self.header); 170 | stream.append_list(&self.transactions); 171 | } 172 | } 173 | 174 | pub fn hash_header(header: &BlockHeader, truncated: bool) -> H256 { 175 | let mut stream = RlpStream::new(); 176 | header.stream_rlp(&mut stream, truncated); 177 | return keccak256(stream.out().as_slice()); 178 | } 179 | 180 | pub fn keccak256(bytes: &[u8]) -> H256 { 181 | let mut keccak256 = Keccak::v256(); 182 | let mut out = [0u8; 32]; 183 | keccak256.update(bytes); 184 | keccak256.finalize(&mut out); 185 | H256::from(out) 186 | } 187 | 188 | pub fn verify_block(header: &BlockHeader, parent: Option<&BlockHeader>) -> Result<(), CustomError> { 189 | use CustomError::*; 190 | 191 | if let Some(p) = parent { 192 | if header.number != p.number + 1 { 193 | return Err(VerifyHeaderFailed_NonConsecutiveHeight); 194 | } 195 | if header.timestamp <= p.timestamp { 196 | return Err(VerifyHeaderFailed_NonMonotonicTimestamp); 197 | } 198 | if header.parent_hash != hash_header(p, false) { 199 | return Err(VerifyHeaderFailed_InvalidParentHash); 200 | } 201 | }; 202 | 203 | if header.extra_data.bytes.len() > 32 { 204 | return Err(VerifyHeaderFailed_TooMuchExtraData); 205 | } 206 | 207 | Ok(()) 208 | } 209 | 210 | pub fn height_to_epoch(h: u64) -> u64 { 211 | h / EPOCH_LENGTH 212 | } 213 | 214 | pub fn verify_pow(header: &BlockHeader, lookup: F) -> bool 215 | where 216 | F: FnMut(u32) -> H512, 217 | { 218 | use ethash::*; 219 | let epoch = height_to_epoch(header.number) as usize; 220 | let full_size = get_full_size(epoch); 221 | 222 | let (_mix_hash, result) = 223 | hashimoto(hash_header(&header, true), header.nonce, full_size, lookup); 224 | let target = cross_boundary(header.difficulty); 225 | 226 | return U256::from_big_endian(result.as_fixed_bytes()) <= target; 227 | } 228 | 229 | impl BlockHeader { 230 | const NUM_FIELDS: usize = 15; 231 | 232 | fn stream_rlp(&self, stream: &mut RlpStream, truncated: bool) { 233 | stream.begin_list(Self::NUM_FIELDS - if truncated { 2 } else { 0 }); 234 | 235 | stream.append(&self.parent_hash); 236 | stream.append(&self.uncles_hash); 237 | stream.append(&self.author); 238 | stream.append(&self.state_root); 239 | stream.append(&self.transactions_root); 240 | stream.append(&self.receipts_root); 241 | stream.append(&self.log_bloom); 242 | stream.append(&self.difficulty); 243 | stream.append(&self.number); 244 | stream.append(&self.gas_limit); 245 | stream.append(&self.gas_used); 246 | stream.append(&self.timestamp); 247 | stream.append(&self.extra_data); 248 | 249 | if !truncated { 250 | stream.append(&self.mix_hash); 251 | stream.append(&self.nonce); 252 | } 253 | } 254 | } 255 | 256 | impl Encodable for BlockHeader { 257 | fn rlp_append(&self, stream: &mut RlpStream) { 258 | self.stream_rlp(stream, false); 259 | } 260 | } 261 | 262 | impl Decodable for BlockHeader { 263 | fn decode(serialized: &Rlp) -> Result { 264 | let block_header = BlockHeader { 265 | parent_hash: serialized.val_at(0)?, 266 | uncles_hash: serialized.val_at(1)?, 267 | author: serialized.val_at(2)?, 268 | state_root: serialized.val_at(3)?, 269 | transactions_root: serialized.val_at(4)?, 270 | receipts_root: serialized.val_at(5)?, 271 | log_bloom: serialized.val_at(6)?, 272 | difficulty: serialized.val_at(7)?, 273 | number: serialized.val_at(8)?, 274 | gas_limit: serialized.val_at(9)?, 275 | gas_used: serialized.val_at(10)?, 276 | timestamp: serialized.val_at(11)?, 277 | extra_data: serialized.val_at(12)?, 278 | mix_hash: serialized.val_at(13)?, 279 | nonce: serialized.val_at(14)?, 280 | }; 281 | 282 | return Ok(block_header); 283 | } 284 | } 285 | 286 | impl ExtraData { 287 | pub fn as_slice(&self) -> &[u8] { 288 | &self.bytes[0..self.len as _] 289 | } 290 | pub fn as_mut(&mut self) -> &mut [u8] { 291 | &mut self.bytes[0..self.len as _] 292 | } 293 | pub fn from_slice(data: &[u8]) -> Self { 294 | assert!(data.len() <= EXTRA_DATA_MAX_LEN); 295 | let mut ret = Self { 296 | len: data.len() as _, 297 | bytes: unsafe { ::std::mem::uninitialized() }, 298 | }; 299 | ret.as_mut().copy_from_slice(data); 300 | ret 301 | } 302 | } 303 | 304 | impl Encodable for ExtraData { 305 | fn rlp_append(&self, stream: &mut RlpStream) { 306 | self.as_slice().rlp_append(stream); 307 | } 308 | } 309 | 310 | impl Decodable for ExtraData { 311 | fn decode(serialized: &Rlp) -> Result { 312 | let v = Vec::::decode(serialized)?; 313 | Ok(Self::from_slice(&*v)) 314 | } 315 | } 316 | 317 | impl PartialEq for ExtraData { 318 | fn eq(&self, other: &Self) -> bool { 319 | self.as_slice() == other.as_slice() 320 | } 321 | } 322 | 323 | impl Eq for ExtraData {} 324 | -------------------------------------------------------------------------------- /solana-bridges/src/Solana/RPC.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 2 | {-# LANGUAGE LambdaCase #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE ScopedTypeVariables #-} 5 | {-# LANGUAGE InstanceSigs #-} 6 | {-# LANGUAGE TypeApplications #-} 7 | 8 | module Solana.RPC where 9 | 10 | import Solana.Types 11 | import Data.Foldable 12 | import Control.Lens (view, ix, _Left) 13 | import Control.Concurrent.MVar 14 | import Data.IORef 15 | import Data.Bifunctor 16 | import Control.Monad.Reader 17 | import Data.Aeson 18 | import qualified Data.ByteString as BS 19 | import qualified Network.WebSockets as WebSockets 20 | import qualified Data.Text as T 21 | import qualified Data.Text.Encoding as T 22 | import qualified Network.HTTP.Client as HTTPClient 23 | import qualified Network.HTTP.Types.Status as HTTP 24 | import Network.Socket(withSocketsDo) 25 | import Network.HTTP.Client.TLS (newTlsManager) 26 | import qualified Data.IntMap.Strict as IntMap 27 | import Data.IntMap (IntMap) 28 | import Control.Concurrent (forkIO, killThread) 29 | import Control.Monad.Catch (finally) 30 | import Data.Void 31 | import Data.Word 32 | 33 | 34 | getEpochSchedule :: SolanaRpcM IO SolanaEpochSchedule 35 | getEpochSchedule = rpcWebRequest @SolanaEpochSchedule "getEpochSchedule" 36 | 37 | getEpochInfo :: SolanaRpcM IO SolanaEpochInfo 38 | getEpochInfo = rpcWebRequest "getEpochInfo" 39 | 40 | getConfirmedBlock :: Word64 -> SolanaRpcM IO (Either Value (Maybe SolanaCommittedBlock)) 41 | getConfirmedBlock slot = rpcWebRequest'' @_ @(Maybe SolanaCommittedBlock) "getConfirmedBlock" $ Just [slot] 42 | 43 | getConfirmedBlocks :: Word64 -> Word64 -> SolanaRpcM IO [Word64] 44 | getConfirmedBlocks startSlot endSlot = rpcWebRequest' "getConfirmedBlocks" $ Just (startSlot, endSlot) 45 | 46 | -- TODO: the real RPC doesn't work as advertised 47 | getConfirmedBlocksWithLimit :: Word64 -> Word64 -> SolanaRpcM IO [Word64] 48 | -- getConfirmedBlocksWithLimit startSlot endSlot = rpcWebRequest' "getConfirmedBlocksWithLimit" $ Just (startSlot, endSlot) 49 | getConfirmedBlocksWithLimit startSlot limit = getConfirmedBlocks startSlot (startSlot + limit) 50 | 51 | getLeaderSchedule :: Word64 -> SolanaRpcM IO SolanaLeaderSchedule 52 | getLeaderSchedule slot = rpcWebRequest' @[Word64] @SolanaLeaderSchedule "getLeaderSchedule" $ Just [slot] 53 | 54 | 55 | rootSubscribe :: (Either String Word64 -> IO ()) -> SolanaRpcM IO () 56 | rootSubscribe = sendRPCSubscription @Void @Word64 "rootSubscribe" (Nothing :: Maybe Void) 57 | 58 | slotSubscribe :: (Either String SolanaSlotNotification -> IO ()) -> SolanaRpcM IO () 59 | slotSubscribe = sendRPCSubscription @Void @SolanaSlotNotification "slotSubscribe" Nothing 60 | 61 | 62 | withSolanaWebSocket :: SolanaRpcConfig -> SolanaRpcM IO a -> IO a 63 | withSolanaWebSocket cfg (SolanaRpcM go) = do 64 | 65 | req0 <- (HTTPClient.parseRequest "http://127.0.0.1:8899") 66 | httpMgr <- newTlsManager 67 | 68 | requestId <- newIORef 0 69 | requestHandlers :: IORef (IntMap (Value -> IO ())) <- newIORef IntMap.empty 70 | notifyHandlers :: IORef (IntMap (Either [Value] (Value -> IO ()))) <- newIORef IntMap.empty 71 | rpcCriticalSection <- newMVar () 72 | 73 | withSocketsDo $ WebSockets.runClient "127.0.0.1" 8900 "/" $ \conn -> do 74 | 75 | threadId <- forkIO $ forever $ do 76 | msgOrNotify <- either error pure . decodeMsg =<< WebSockets.receiveDataMessage conn 77 | case msgOrNotify of 78 | Left msg -> join $ atomicModifyIORef' notifyHandlers $ \oldState -> 79 | case IntMap.lookup (_solanaRpcNotificationParams_subscription $ _solanaRpcNotification_params msg) oldState of 80 | Nothing -> (IntMap.insert (_solanaRpcNotificationParams_subscription $ _solanaRpcNotification_params msg) (Left $ [_solanaRpcNotificationParams_result $ _solanaRpcNotification_params msg]) oldState, pure ()) 81 | Just (Right handler) -> (oldState, handler $ _solanaRpcNotificationParams_result $ _solanaRpcNotification_params msg) 82 | Just (Left stillPending) -> (IntMap.insert (_solanaRpcNotificationParams_subscription $ _solanaRpcNotification_params msg) (Left $ stillPending <> [_solanaRpcNotificationParams_result $ _solanaRpcNotification_params msg]) oldState, pure ()) 83 | Right msg -> join $ atomicModifyIORef' requestHandlers $ \oldState -> 84 | case IntMap.lookup (_solanaRpcResult_id msg) oldState of 85 | Nothing -> (,) oldState $ pure () 86 | Just handler -> (,) (IntMap.delete (_solanaRpcResult_id msg) oldState) $ 87 | handler (_solanaRpcResult_result msg) 88 | 89 | 90 | 91 | flip finally (killThread threadId) $ runReaderT go $ SolanaRpcContext 92 | { _solanaRpcContext_baseRpcRequest = req0 93 | { HTTPClient.host = _solanaRpcConfig_host cfg 94 | } 95 | , _solanaRpcContext_httpMgr = httpMgr 96 | , _solanaRpcContext_wsConn = conn 97 | , _solanaRpcContext_requestHandlers = requestHandlers 98 | , _solanaRpcContext_notifyHandlers = notifyHandlers 99 | , _solanaRpcContext_rpcCriticalSection = rpcCriticalSection 100 | , _solanaRpcContext_requestId = requestId 101 | } 102 | 103 | unliftSolanaRpcM :: SolanaRpcM IO (SolanaRpcM IO a -> IO a) 104 | unliftSolanaRpcM = SolanaRpcM $ do 105 | ctx <- ask 106 | pure $ ($ctx) . runReaderT . runSolanaRpcM 107 | 108 | -- The low level internals follow: 109 | 110 | decodeMsg :: WebSockets.DataMessage -> Either String (Either SolanaRpcNotification SolanaRpcResult) 111 | decodeMsg = \case 112 | WebSockets.Text x _ -> go x 113 | WebSockets.Binary x -> go x 114 | where 115 | go x = first (<> show x) $ case decode' x of 116 | Just rpcresult -> Right (Right rpcresult) 117 | Nothing -> Left <$> eitherDecode' x 118 | 119 | 120 | rpcWebRequest'' :: forall a b. (Show a, ToJSON a, FromJSON b) => T.Text -> Maybe a -> SolanaRpcM IO (Either Value b) 121 | rpcWebRequest'' method params = do 122 | req0 <- SolanaRpcM $ asks _solanaRpcContext_baseRpcRequest -- (HTTPClient.parseRequest "http://127.0.0.1:8899") 123 | httpMgr <- SolanaRpcM $ asks _solanaRpcContext_httpMgr 124 | let req = req0 125 | { HTTPClient.method = "POST" 126 | , HTTPClient.requestBody = HTTPClient.RequestBodyLBS $ encode $ SolanaRpcRequest 127 | { _solanaRpcRequest_jsonrpc = JsonRpcVersion 128 | , _solanaRpcRequest_id = 1 129 | , _solanaRpcRequest_method = method 130 | , _solanaRpcRequest_params = params 131 | } 132 | , HTTPClient.requestHeaders = ("Content-Type", "application/json") : HTTPClient.requestHeaders req0 133 | } 134 | 135 | resultLBS <- liftIO $ HTTPClient.httpLbs req httpMgr 136 | unless (HTTP.statusIsSuccessful $ HTTPClient.responseStatus resultLBS) $ 137 | error $ show $ HTTPClient.responseStatus resultLBS 138 | 139 | either (error . (\bad -> unlines [bad, show (method, params), show (HTTPClient.responseBody resultLBS)])) (pure . bimap _solanaRpcError_error _solanaRpcResult_result . unSolanaRpcErrorOrResult) 140 | $ eitherDecode' @(SolanaRpcErrorOrResult b) $ HTTPClient.responseBody resultLBS 141 | 142 | rpcWebRequest :: FromJSON b => T.Text -> SolanaRpcM IO b 143 | rpcWebRequest method = rpcWebRequest' method $ (Nothing :: Maybe Void) 144 | 145 | rpcWebRequest' :: (Show a, ToJSON a, FromJSON b) => T.Text -> Maybe a -> SolanaRpcM IO b 146 | rpcWebRequest' method args = rpcWebRequest'' method args >>= either (error . show) pure 147 | 148 | 149 | sendRPCSubscription :: forall a b. (ToJSON a, FromJSON b) => T.Text -> Maybe a -> (Either String b -> IO ()) -> SolanaRpcM IO () 150 | sendRPCSubscription method params handler = do 151 | rpcCriticalSection <- SolanaRpcM $ asks _solanaRpcContext_rpcCriticalSection 152 | requestId <- SolanaRpcM $ asks _solanaRpcContext_requestId 153 | requestHandlers <- SolanaRpcM $ asks _solanaRpcContext_requestHandlers 154 | conn <- SolanaRpcM $ asks _solanaRpcContext_wsConn 155 | -- let 156 | -- rpcWSRequest :: IO Value 157 | result <- liftIO $ newEmptyMVar 158 | newId <- liftIO $ atomicModifyIORef' requestId $ \x -> (x, succ x) 159 | subIdJSON <- SolanaRpcM $ lift $ withMVar rpcCriticalSection $ \() -> do 160 | join $ atomicModifyIORef' requestHandlers $ \oldState -> (,) (IntMap.insert newId (putMVar result) oldState) $ do 161 | WebSockets.sendBinaryData conn $ Data.Aeson.encode $ SolanaRpcRequest 162 | { _solanaRpcRequest_jsonrpc = JsonRpcVersion 163 | , _solanaRpcRequest_id = newId 164 | , _solanaRpcRequest_method = method 165 | , _solanaRpcRequest_params = params 166 | } 167 | readMVar result 168 | -- sendRPCRequest method params $ \subIdJSON -> do 169 | subId <- case fromJSON subIdJSON of 170 | Success x -> pure x 171 | Error x -> error $ x <> ": " <> show subIdJSON 172 | notifyHandlers <- SolanaRpcM $ asks _solanaRpcContext_notifyHandlers 173 | lift $ join $ atomicModifyIORef' notifyHandlers $ \oldState -> 174 | let 175 | handler' :: Value -> IO () 176 | handler' x = handler $ case fromJSON x of 177 | Success x' -> Right x' 178 | Error x' -> Left $ x' <> ": " <> show subIdJSON 179 | newState = IntMap.insert subId (Right handler') oldState 180 | pendingNotifications = traverse_ handler' $ view (ix subId . _Left) oldState 181 | in (newState, pendingNotifications) 182 | 183 | newtype SolanaRpcM m a = SolanaRpcM { runSolanaRpcM :: ReaderT SolanaRpcContext m a } 184 | deriving (Functor, Applicative, Monad, MonadIO, MonadTrans, MonadFail) 185 | 186 | -- TODO: tls 187 | data SolanaRpcConfig = SolanaRpcConfig 188 | { _solanaRpcConfig_host :: BS.ByteString 189 | , _solanaRpcConfig_rpcPort :: Int 190 | , _solanaRpcConfig_wsPort :: Int 191 | } 192 | 193 | instance FromJSON SolanaRpcConfig where 194 | parseJSON v = (\(a, b, c) -> SolanaRpcConfig (T.encodeUtf8 a) b c) <$> parseJSON v 195 | instance ToJSON SolanaRpcConfig where 196 | toJSON (SolanaRpcConfig a b c) = toJSON (T.decodeUtf8 a, b, c) 197 | 198 | data SolanaRpcContext = SolanaRpcContext 199 | { _solanaRpcContext_baseRpcRequest :: HTTPClient.Request 200 | , _solanaRpcContext_httpMgr :: !HTTPClient.Manager 201 | , _solanaRpcContext_wsConn :: !WebSockets.Connection 202 | , _solanaRpcContext_requestHandlers :: !(IORef (IntMap (Value -> IO ()))) 203 | , _solanaRpcContext_notifyHandlers :: !(IORef (IntMap (Either [Value] (Value -> IO ())))) 204 | , _solanaRpcContext_requestId :: !(IORef Int) 205 | , _solanaRpcContext_rpcCriticalSection :: !(MVar ()) 206 | } 207 | -------------------------------------------------------------------------------- /solana-client-tool/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const logger = new console.Console(process.stderr, process.stderr); 4 | const web3 = require("@solana/web3.js"); 5 | const fs = require("mz/fs"); 6 | const yargs = require("yargs"); 7 | 8 | async function calcFees (connection, data, storageSize) { 9 | let fees = 0; 10 | const {feeCalculator} = await connection.getRecentBlockhash(); 11 | 12 | // Calculate the cost to load the program 13 | const NUM_RETRIES = 500; // allow some number of retries 14 | fees += 15 | feeCalculator.lamportsPerSignature * 16 | (web3.BpfLoader.getMinNumSignatures(data.length) + NUM_RETRIES) + 17 | (await connection.getMinimumBalanceForRentExemption(data.length)); 18 | 19 | // Calculate the cost to fund the greeter account 20 | fees += await await connection.getMinimumBalanceForRentExemption(storageSize); 21 | 22 | // Calculate the cost of sending the transactions 23 | fees += feeCalculator.lamportsPerSignature * 100; // wag 24 | 25 | return fees; 26 | } 27 | 28 | async function readAccount(filename) { 29 | return new web3.Account(JSON.parse(await fs.readFile(filename))); 30 | } 31 | function readAccountSync(filename) { 32 | return new web3.Account(JSON.parse(fs.readFileSync(filename))); 33 | } 34 | async function readOrGenerateAccount(filename, hint) { 35 | if (!!filename) { 36 | return readAccount(filename); 37 | } else { 38 | var programAccount = new web3.Account(); 39 | logger.log(hint + " keypair:" + JSON.stringify(Array.prototype.slice.call(programAccount.secretKey))); 40 | return programAccount; 41 | } 42 | } 43 | 44 | async function openConnection(url) { 45 | const connection = new web3.Connection(url); 46 | logger.log(await connection.getVersion()); 47 | return connection; 48 | } 49 | 50 | async function doAlloc(argv) { 51 | const payerAccount = argv.payer; 52 | logger.log ("payer id:" + payerAccount.publicKey.toBase58()) 53 | 54 | const storageAccount = await readOrGenerateAccount(argv.storageAccount, "storage"); 55 | logger.log ("storage id:" + storageAccount.publicKey.toBase58()); 56 | 57 | const programId = new web3.PublicKey(argv.programId); 58 | 59 | const connection = await openConnection(argv.url); 60 | 61 | const { space } = argv; 62 | 63 | return doAllocReal(connection, payerAccount, storageAccount, programId, space); 64 | } 65 | 66 | async function doAllocReal(connection, payerAccount, storageAccount, programId, space) { 67 | const fees = await calcFees(connection, new Buffer(0), space); 68 | 69 | const progAcctTxn = new web3.Transaction() 70 | .add(web3.SystemProgram.createAccount( 71 | { fromPubkey: payerAccount.publicKey 72 | , newAccountPubkey: storageAccount.publicKey 73 | , lamports: fees 74 | , space: space 75 | , programId: programId 76 | })) 77 | ; 78 | logger.log(await progAcctTxn) 79 | logger.log("storageAccount:" + storageAccount.publicKey.toBase58()); 80 | 81 | const payerInfo = await connection.getAccountInfo(payerAccount.publicKey); 82 | 83 | logger.log( "payer balance: " + payerInfo.lamports); 84 | logger.log( "deployment fees: " + fees); 85 | 86 | if (fees > payerInfo.lamports) { 87 | logger.log("balance too low"); 88 | process.exit(1); 89 | } 90 | 91 | var v = web3.sendAndConfirmTransaction( 92 | connection, 93 | progAcctTxn, 94 | [payerAccount, storageAccount], 95 | {skipPreflight: true, commitment: 'recent'}); 96 | 97 | logger.log("alloc txn id: " + await v); 98 | 99 | const sleep = (milliseconds) => { 100 | return new Promise(resolve => setTimeout(resolve, milliseconds)) 101 | } 102 | 103 | var stgInfo = null; 104 | while (stgInfo === null) { 105 | stgInfo = await connection.getAccountInfo(storageAccount.publicKey); 106 | if (stgInfo === null) { 107 | logger.log("..."); 108 | await sleep(1000); 109 | } 110 | } 111 | logger.log(stgInfo); 112 | logger.log("owner:" + stgInfo.owner.toBase58()); 113 | 114 | return { 115 | programId: programId.toBase58(), 116 | accountId: storageAccount.publicKey.toBase58(), 117 | accountKey: Array.prototype.slice.call(storageAccount.secretKey), 118 | }; 119 | 120 | }; 121 | 122 | 123 | 124 | function doCall(fn) { 125 | return async function (argv) { 126 | logger.log("args", argv); 127 | 128 | const storageId = argv.accountKey.publicKey; 129 | const programId = argv.programId; 130 | const payerAccount = argv.payer; 131 | logger.log ("payer id:" + payerAccount.publicKey.toBase58()) 132 | 133 | const connection = await openConnection(argv.url); 134 | 135 | const {instructionData, isSigner, isWritable} = await fn(argv); 136 | 137 | const key = { pubkey:storageId ,isSigner, isWritable }; 138 | logger.log("key", key.pubkey.toBase58()); 139 | const txnI = { keys:[key] , programId, data: instructionData }; 140 | 141 | const txn = new web3.Transaction().add(new web3.TransactionInstruction(txnI)); 142 | logger.log("txn", txn); 143 | logger.log("txn", JSON.stringify(txn)); 144 | 145 | let signers0 = isSigner 146 | ? [payerAccount, argv.accountKey] 147 | : [payerAccount] 148 | ; 149 | logger.log("signers", signers0.map(x => x.publicKey.toBase58())); 150 | 151 | var v = await connection.simulateTransaction( 152 | txn, 153 | signers0, 154 | ); 155 | logger.log("simulation", JSON.stringify(v)); 156 | 157 | var txId = await web3.sendAndConfirmTransaction( 158 | connection, 159 | txn, 160 | signers0, 161 | { 162 | commitment: 'singleGossip', 163 | skipPreflight: true, 164 | }, 165 | ); 166 | return {"sig": txId}; 167 | }; 168 | } 169 | 170 | 171 | async function noop(/* argv */) { 172 | return { 173 | instructionData :Buffer.from("00", "hex"), 174 | isSigner: false, 175 | isWritable: false 176 | }; 177 | } 178 | async function initialize(argv) { 179 | const instructionData = Buffer.from("01" + argv.instruction, argv.instructionEncoding); 180 | return { 181 | instructionData, 182 | isSigner: true, 183 | isWritable: true 184 | }; 185 | } 186 | async function newBlock(argv) { 187 | const instructionData = Buffer.from("02" + argv.instruction, argv.instructionEncoding); 188 | return { 189 | instructionData, 190 | isSigner: false, 191 | isWritable: true 192 | }; 193 | } 194 | async function provideEthashElement(argv) { 195 | const instructionData = Buffer.from("03" + argv.element, argv.elementEncoding); 196 | return { 197 | instructionData, 198 | isSigner: false, 199 | isWritable: true 200 | }; 201 | } 202 | 203 | async function doInclusionProof(argv) { 204 | logger.log("args", argv); 205 | 206 | const chainLogId = argv.accountId; 207 | const programId = argv.programId; 208 | const payerAccount = argv.payer; 209 | logger.log ("payer id:" + payerAccount.publicKey.toBase58()) 210 | 211 | const connection = await openConnection(argv.url); 212 | 213 | const proofAccount = new web3.Account(); 214 | logger.log ("proof storage id:" + proofAccount.publicKey.toBase58()); 215 | 216 | const space = 9999; // ? 217 | const newAccountInfo = await doAllocReal(connection, payerAccount, proofAccount, chainLogId, space); 218 | 219 | const chainLogKey = 220 | { pubkey: chainLogId 221 | , isSigner: false 222 | , isWritable: false 223 | }; 224 | logger.log("chain log key", chainLogKey.pubkey.toBase58()); 225 | 226 | const proofKey = 227 | { pubkey: proofAccount.publicKey 228 | , isSigner: true 229 | , isWritable: true 230 | }; 231 | logger.log("proof key", proofKey.pubkey.toBase58()); 232 | 233 | async function doTx(data) { 234 | const txnI = 235 | { keys: [ chainLogKey, proofKey ] 236 | , programId 237 | , data 238 | }; 239 | 240 | const txn = new web3.Transaction().add(new web3.TransactionInstruction(txnI)); 241 | logger.log("txn", txn); 242 | logger.log("txn", JSON.stringify(txn)); 243 | 244 | let signers0 = [payerAccount, proofAccount]; 245 | logger.log("signers", signers0.map(x => x.publicKey.toBase58())); 246 | 247 | var v = await connection.simulateTransaction( 248 | txn, 249 | signers0 250 | ); 251 | logger.log("simulation", JSON.stringify(v)); 252 | 253 | var txId = await web3.sendAndConfirmTransaction(connection, 254 | txn, 255 | signers0 256 | ); 257 | return {"sig": txId}; 258 | } 259 | 260 | //doTx(Buffer.from("04", "hex")); 261 | const v = doTx(Buffer.from("05" + argv.inclusionProof, argv.inclusionProofEncoding)); 262 | //doTx(Buffer.from("05" + argv.inclusionProof, argv.inclusionProofEncoding)); 263 | //doTx(Buffer.from("05" + argv.inclusionProof, argv.inclusionProofEncoding)); 264 | //doTx(Buffer.from("06", "hex")); 265 | 266 | return {"sig": v}; 267 | } 268 | 269 | 270 | function callCmd (fn) { 271 | return function (argv) { 272 | return fn(argv) 273 | .then(result => {process.stdout.write(JSON.stringify(result)); process.exit();}) 274 | .catch(bad => {logger.error(bad); process.exit(99)}) 275 | ; 276 | }; 277 | } 278 | 279 | function commandArgs(yargv) { 280 | return (yargv.config("config", x => JSON.parse(fs.readFileSync(x))) 281 | .options( 282 | { 'program-id' : 283 | { demand: true 284 | , coerce: arg => new web3.PublicKey(arg) 285 | } 286 | , 'account-key': 287 | { demand: true 288 | , coerce: arg => new web3.Account(typeof arg === "string" ? JSON.parse(arg) : arg) 289 | } 290 | })); 291 | } 292 | 293 | yargs 294 | .demandCommand().recommendCommands() 295 | 296 | .options( 297 | { 'url': { default: "http://localhost:8899" } 298 | , 'payer' : 299 | { default: process.env.HOME + "/.config/solana/id.json" 300 | , coerce: readAccountSync 301 | } 302 | }) 303 | 304 | .command('alloc', 'Deploy program' 305 | , (yargv) => yargv.options( 306 | { 'program-id' : {demand : true } 307 | , 'space' : { demand : true, type: 'number' } 308 | , 'storage-account' : {} 309 | }) 310 | , callCmd(doAlloc)) 311 | 312 | .command('noop', 'noop' 313 | , (yargv) => commandArgs(yargv).options({ 314 | }) 315 | , callCmd(doCall(noop))) 316 | 317 | .command('initialize', 'initialize' 318 | , (yargv) => commandArgs(yargv).options( 319 | { 'instruction': {demand: true} 320 | , 'instruction-encoding': {default: 'hex'} 321 | }) 322 | , callCmd(doCall(initialize))) 323 | .command('new-block', 'new block' 324 | , (yargv) => commandArgs(yargv).options( 325 | { 'instruction': {demand: true} 326 | , 'instruction-encoding': {default: 'hex'} 327 | }) 328 | , callCmd(doCall(newBlock))) 329 | .command('provide-ethash-element', 'provide ethash element' 330 | , (yargv) => commandArgs(yargv).options( 331 | { 'element': {demand: true} 332 | , 'element-encoding': {default: 'hex'} 333 | }) 334 | , callCmd(doCall(provideEthashElement))) 335 | 336 | .command('inclusion-proof', 'inclusion proof' 337 | , (yargv) => yargv.options( 338 | { 'program-id' : 339 | { demand: true 340 | , coerce: arg => new web3.PublicKey(arg) 341 | } 342 | , 'account-id': 343 | { demand: true 344 | , coerce: arg => new web3.PublicKey(arg) 345 | } 346 | , 'inclusion-proof': {demand: true} 347 | , 'inclusino-proof-encoding': {default: 'hex'} 348 | }) 349 | , callCmd(doInclusionProof)) 350 | 351 | .help().alias('help', 'h').argv; 352 | -------------------------------------------------------------------------------- /solana-bridges/src/Solana/Utils.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE LambdaCase #-} 3 | {-# LANGUAGE NumDecimals #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE ScopedTypeVariables #-} 6 | module Solana.Utils where 7 | 8 | import Control.Lens (mapped, (&), (.~)) 9 | import Control.Monad.Except (MonadError, MonadIO, runExceptT) 10 | import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT) 11 | import Crypto.Hash (Digest, SHA256, hash) 12 | import qualified Crypto.PubKey.Ed25519 as Ed25519 13 | import Crypto.Random.Types 14 | import Data.Binary 15 | import Data.Bool (bool) 16 | import qualified Data.ByteArray as ByteArray 17 | import Data.ByteString (ByteString) 18 | import qualified Data.ByteString as BS 19 | import qualified Data.ByteString.Char8 as BSC 20 | import qualified Data.ByteString.Base64 as Base64 21 | import qualified Data.ByteString.Lazy as LBS 22 | import Data.Default 23 | import Data.Foldable 24 | import Data.List.NonEmpty (nonEmpty) 25 | import Data.Maybe 26 | import qualified Data.Sequence as Seq 27 | import qualified Data.Solidity.Prim.Address as Solidity 28 | import Data.Tree (Tree (..)) 29 | import qualified Network.Ethereum.Api.Types as Eth 30 | import Network.JsonRpc.TinyClient (JsonRpcException) 31 | import qualified Network.Web3.Provider as Eth 32 | import Test.Hspec (it, describe, hspec, shouldBe, shouldNotBe, shouldThrow) 33 | 34 | import Ethereum.Contracts 35 | import Solana.Relayer 36 | import Solana.Types 37 | 38 | testWithRunningNode :: Eth.Provider -> IO () 39 | testWithRunningNode node = do 40 | (contract, _slot) <- deploySolanaClientContract node defaultSolanaRPCConfig 41 | testInclusionProofVerification node contract 42 | 43 | testInclusionProofVerification :: Eth.Provider -> Solidity.Address -> IO () 44 | testInclusionProofVerification node contract = hspec $ describe "16-ary merkle tree" $ do 45 | let 46 | verify expected proof blockMerkle value index = do 47 | let stubAccountHash = sha256 "qwerty" 48 | stubBankHash = sha256 $ ByteArray.convert stubAccountHash <> ByteArray.convert blockMerkle 49 | res <- runExceptT (verifyTransactionInclusionProof node contract stubAccountHash blockMerkle proof stubBankHash value index) 50 | res `shouldBe` Right expected 51 | 52 | it "does not access offset when proof is empty " $ do 53 | verify True [] (sha256 $ BS.singleton 0) (BS.singleton 0) 99999 54 | 55 | it "accepts valid proofs" $ do 56 | verify True [leaves "a"] (merkleParent $ leaves "a") "aa" 0x0 57 | 58 | verify True [leaves "a", branches] (merkleParent branches) "aa" 0x00 59 | verify True [leaves "a", branches] (merkleParent branches) "ap" 0x0f 60 | verify True [leaves "b", branches] (merkleParent branches) "bc" 0x12 61 | verify True [leaves "p", branches] (merkleParent branches) "pa" 0xf0 62 | verify True [leaves "p", branches] (merkleParent branches) "pp" 0xff 63 | 64 | it "rejects invalid proofs" $ do 65 | verify False [leaves "a"] (merkleParent $ leaves "a") "aa" 0x1 66 | 67 | verify False [leaves "a", branches] (merkleParent branches) "aa" 0x01 68 | verify False [leaves "a", branches] (merkleParent branches) "ap" 0x00 69 | verify False [leaves "a", branches] (merkleParent branches) "pp" 0xff 70 | 71 | merkleParent :: [Digest SHA256] -> Digest SHA256 72 | merkleParent = sha256 . BS.concat . fmap ByteArray.convert 73 | 74 | verifyProof 75 | :: Eth.Provider 76 | -> Solidity.Address 77 | -> [[Digest SHA256]] 78 | -> Digest SHA256 79 | -> ByteString 80 | -> Word64 81 | -> IO () 82 | verifyProof node contract proof root leaf offset = do 83 | putStrLn $ "Verifying inclusion proof" 84 | putStrLn $ unlines $ fmap (" " <>) $ 85 | [ "of length: " <> show (length proof) 86 | , "for value: " <> show leaf 87 | , "at offset: " <> show offset 88 | , "of merkle tree with root: " <> show root 89 | , "on contract with address: " <> show contract 90 | ] 91 | runExceptT (verifyMerkleProof node contract proof root leaf offset) >>= \case 92 | Left err -> putStrLn $ "Error: " <> show err 93 | Right res -> putStrLn $ "Verification " <> bool "failed" "succeeded" res 94 | 95 | labels :: [Char] 96 | labels = "abcdefghijklmnop" 97 | 98 | leaves :: ByteString -> [Digest SHA256] 99 | leaves x = fmap (sha256 . (x <>) . BSC.singleton) labels 100 | 101 | branches :: [Digest SHA256] 102 | branches = fmap (merkleParent . leaves . BSC.singleton) labels 103 | 104 | mkTree :: [Char] -> Tree String 105 | mkTree ls = Node "" $ flip fmap ls $ \l -> 106 | Node [] $ flip fmap ls $ \s -> 107 | Node [l, s] [] 108 | 109 | mkSignature :: MonadRandom m => BS.ByteString -> m Ed25519.Signature 110 | mkSignature msg = do 111 | sk <- Ed25519.generateSecretKey 112 | pure $ Ed25519.sign sk (Ed25519.toPublic sk) msg 113 | 114 | getVoteCount :: (MonadError String m, MonadIO m) => Eth.Provider -> Solidity.Address -> Word64 -> m Word64 115 | getVoteCount node contract slot = fmap _contractSlot_voteCounts $ getSlot node contract slot 116 | 117 | getTransaction :: (MonadError String m, MonadIO m) => Eth.Provider -> Solidity.Address -> Word64 -> Word64 -> m (Maybe SolanaTxn) 118 | getTransaction node contract slot txIdx = runMaybeT $ do 119 | sigs <- MaybeT $ getSignatures node contract slot txIdx 120 | msg <- MaybeT $ getMessage node contract slot txIdx 121 | pure $ Data.Binary.decode $ LBS.fromStrict $ sigs <> msg 122 | 123 | 124 | -- curl getConfirmedBlock [30, "base64"] 125 | txnBase64 :: BS.ByteString 126 | txnBase64 = "AZVKAuMoJbnV4PtPG7mMZCBEanoBomhtUHls0FuOa1QgthOeVMVj6VeAUVkn++w9NW1VF2POQ4YaeOEY6T2L0gsBAAMF01L0Cxouih7U0W+rqrqFKJDpZV3XeSpuqaXgRbrAeAqgAVVnFM8Agxw18NmD9pMJiEV64Q4iw2HyJ+BVmNG3+Qan1RcZLwqvxvJl4/t3zHragsUp0L47E24tAFUgAAAABqfVFxjHdMkoVmOYaR1etoteuKObS21cc1VbIQAAAAAHYUgdNXR0u3xNdiTr072z2DVec9EQQ/wNo1OAAAAAAOXoKdOObGil/U5GuEw/MRcdyHgKqsLIJgZiCDePh+iGAQQEAQIDADUCAAAAAQAAAAAAAAAdAAAAAAAAAGjgBRtbPax5AAeZ2uXW052RZQ4g+VXwFZO6iUZSVsuGAA==" 127 | 128 | txnBinary :: BS.ByteString 129 | Right txnBinary = Base64.decode txnBase64 130 | 131 | txnParsed :: SolanaTxn 132 | txnParsed = Data.Binary.decode $ LBS.fromStrict txnBinary 133 | 134 | roundtrip :: forall b. Binary b => b -> b 135 | roundtrip = dec . enc 136 | where 137 | dec = Data.Binary.decode . LBS.fromStrict 138 | enc = LBS.toStrict . Data.Binary.encode 139 | 140 | dummyHash :: Base58ByteString 141 | dummyHash = Base58ByteString $ ByteArray.convert solanaVoteProgram 142 | 143 | submitChildBlock :: (MonadError String m, MonadIO m) => Eth.Provider -> Solidity.Address -> [SolanaTxn] -> m Eth.TxReceipt 144 | submitChildBlock node contract txs = do 145 | s <- getLastSlot node contract 146 | slot <- getSlot node contract s 147 | let block = SolanaCommittedBlock 148 | { _solanaCommittedBlock_blockhash = dummyHash 149 | , _solanaCommittedBlock_previousBlockhash = _contractSlot_blockHash slot 150 | , _solanaCommittedBlock_parentSlot = s 151 | , _solanaCommittedBlock_transactions = SolanaTxnWithMeta <$> txs 152 | , _solanaCommittedBlock_rewards = [] 153 | , _solanaCommittedBlock_blockTime = Nothing 154 | } 155 | addBlocks node contract $ pure (s+1, block, dummyHash) 156 | 157 | txnSigningInfo :: SolanaTxn -> (BS.ByteString, Seq.Seq (Ed25519.PublicKey, Ed25519.Signature)) 158 | txnSigningInfo txn = (LBS.toStrict $ Data.Binary.encode msg, Seq.zip pks sigs) 159 | where 160 | msg = _solanaTxn_message txn 161 | 162 | pks :: Seq.Seq Ed25519.PublicKey 163 | pks = unCompactArray $ _solanaTxnMessage_accountKeys msg 164 | 165 | sigs :: Seq.Seq Ed25519.Signature 166 | sigs = unCompactArray (_solanaTxn_signatures txn) 167 | 168 | sha256 :: BS.ByteString -> Digest SHA256 169 | sha256 = hash 170 | 171 | verifySigs :: SolanaTxn -> Seq.Seq Bool 172 | verifySigs txn = flip fmap ed25519 $ \(pk, sig) -> Ed25519.verify pk msg sig 173 | where 174 | (msg, ed25519) = txnSigningInfo txn 175 | 176 | testSolanaClient :: IO () 177 | testSolanaClient = do 178 | let node = def 179 | 180 | [vi] = txnParsed 181 | & _solanaTxn_message 182 | & _solanaTxnMessage_instructions 183 | & fmap _solanaTxnInstruction_data 184 | & fmap unCompactByteArray 185 | & unCompactArray 186 | & toList 187 | & fmap Data.Binary.decode 188 | SolanaVoteInstruction_Vote v = vi 189 | 190 | [votedOnSlot] = v & _solanaVote_slots & toList 191 | initializeSlot = 1 + fromIntegral votedOnSlot 192 | relayStartingSlot = 1 + initializeSlot 193 | 194 | -- VoteSwitch's Hash is of a switching proof which is currently unused since optimistic confirmation proofs are not yet implemented 195 | madeUpHash = sha256 "0x1234567890" 196 | vsi = SolanaVoteInstruction_VoteSwitch v madeUpHash 197 | 198 | notAVote = SolanaVoteInstruction_UpdateValidatorIdentity 199 | 200 | txnTamperedMessageHeader = txnParsed 201 | & solanaTxn_message . solanaTxnMessage_header . solanaTxnHeader_numRequiredSignatures .~ 111 202 | 203 | txnTamperedVoteSwitch = txnParsed 204 | & solanaTxn_message . solanaTxnMessage_instructions . mapped . solanaTxnInstruction_data .~ CompactByteArray (Data.Binary.encode vsi) 205 | 206 | txnTamperedNoVotes = txnParsed 207 | & solanaTxn_message . solanaTxnMessage_instructions . mapped . solanaTxnInstruction_data .~ CompactByteArray (Data.Binary.encode notAVote) 208 | 209 | txnTamperedInvalidProgram = txnParsed 210 | & solanaTxn_message . solanaTxnMessage_instructions . mapped . solanaTxnInstruction_programIdIndex .~ 255 211 | 212 | contract <- deploySolanaClientContractImpl node 213 | putStrLn $ "Contract deployed at address " <> show contract 214 | 215 | hspec $ describe "Solana client" $ do 216 | let 217 | dummyEpochSchedule = SolanaEpochSchedule 218 | { _solanaEpochSchedule_warmup = False 219 | , _solanaEpochSchedule_firstNormalEpoch = 0 220 | , _solanaEpochSchedule_leaderScheduleSlotOffset = 0 221 | , _solanaEpochSchedule_firstNormalSlot = 0 222 | , _solanaEpochSchedule_slotsPerEpoch = 8096 223 | } 224 | dummyBlock slot txs = SolanaCommittedBlock 225 | { _solanaCommittedBlock_blockhash = dummyHash 226 | , _solanaCommittedBlock_previousBlockhash = dummyHash 227 | , _solanaCommittedBlock_parentSlot = slot - 1 228 | , _solanaCommittedBlock_transactions = SolanaTxnWithMeta <$> txs 229 | , _solanaCommittedBlock_rewards = [] 230 | , _solanaCommittedBlock_blockTime = Nothing 231 | } 232 | 233 | it "can initialize contract" $ do 234 | res <- runExceptT $ initialize node contract initializeSlot (dummyBlock (initializeSlot - 1) []) dummyHash dummyEpochSchedule 235 | res `shouldBe` Right () 236 | 237 | let roundtrips x = roundtrip x `shouldBe` x 238 | verify expected txn = do 239 | let (msg, ed25519) = txnSigningInfo txn 240 | let verifies (pk,sig) = do 241 | Ed25519.verify pk msg sig `shouldBe` expected 242 | res <- runExceptT $ test_ed25519_verify node contract (ByteArray.convert sig) msg (Base58ByteString $ ByteArray.convert pk) 243 | res `shouldBe` Right expected 244 | for_ ed25519 verifies 245 | 246 | it "has enough balance for challenge payout" $ do 247 | res <- runExceptT $ getBalance node contract 248 | res `shouldBe` Right challengePayout 249 | 250 | it "parses instructions" $ do 251 | for_ (txnParsed & _solanaTxn_message & _solanaTxnMessage_instructions) $ \i -> do 252 | roundtrips i 253 | let buffer = LBS.toStrict $ Data.Binary.encode i 254 | res <- runExceptT $ parseInstruction node contract buffer 0 255 | res `shouldBe` Right (i, fromIntegral (BS.length buffer)) 256 | 257 | it "accepts valid sigs" $ do 258 | verify True txnParsed 259 | 260 | it "rejects invalid sigs" $ do 261 | verify False txnTamperedMessageHeader 262 | verify False txnTamperedVoteSwitch 263 | verify False txnTamperedNoVotes 264 | 265 | it "verifies vote transactions" $ do 266 | let sigs = LBS.toStrict $ Data.Binary.encode $ txnParsed & _solanaTxn_signatures 267 | msg = LBS.toStrict $ Data.Binary.encode $ txnParsed & _solanaTxn_message 268 | 269 | test expected s m i = do 270 | res <- runExceptT $ verifyVote node contract s m i 271 | res `shouldBe` Right expected 272 | 273 | test True sigs msg 0 274 | 275 | let 276 | submitChallenge slot tx instr = do 277 | res <- runExceptT $ challengeVote node contract slot tx instr 278 | fmap Eth.receiptStatus res `shouldBe` Right (Just 1) 279 | 280 | submitBlocks blocks = do 281 | res <- runExceptT $ addBlocks node contract blocks 282 | fmap Eth.receiptStatus res `shouldBe` Right (Just 1) 283 | 284 | expectTx slot txIdx expectedTx = do 285 | tx <- runExceptT $ getTransaction node contract slot txIdx 286 | tx `shouldBe` Right expectedTx 287 | 288 | expectContractAlive expectedAlive = do 289 | res <- runExceptT $ getCode node contract 290 | (bool shouldBe shouldNotBe expectedAlive) res (Right "") 291 | 292 | 293 | do 294 | let 295 | txsPerSlot :: Num a => a 296 | txsPerSlot = 10 297 | 298 | txCopies :: Word64 299 | txCopies = 200 300 | 301 | slots = txCopies `div` txsPerSlot --TODO: roundup when fractional 302 | tamperedVoteSlot = relayStartingSlot + slots 303 | tamperedNoVotesSlot = tamperedVoteSlot + 1 304 | 305 | copies = flip fmap [0..slots-1] $ \i -> 306 | (relayStartingSlot + i, replicate txsPerSlot txnParsed) 307 | 308 | voteTxs = copies <> [(tamperedVoteSlot, [txnTamperedVoteSwitch])] 309 | allTxs = voteTxs <> [(tamperedNoVotesSlot, [txnTamperedInvalidProgram, txnTamperedNoVotes])] 310 | 311 | it "can submit a chunk of slots with votes" $ do 312 | let 313 | blocks = fromMaybe (error "Blocks must be non-empty") $ nonEmpty 314 | $ flip fmap allTxs $ \(s, txs) -> (s, dummyBlock s txs, dummyHash) 315 | 316 | submitBlocks blocks 317 | 318 | for_ (_solanaVote_slots v) $ \s -> do 319 | res <- runExceptT $ getVoteCount node contract (fromIntegral s) 320 | res `shouldBe` Right (fromIntegral $ length copies * txsPerSlot + 1) 321 | 322 | expectTx relayStartingSlot 0 (Just txnParsed) 323 | expectTx relayStartingSlot (txsPerSlot - 1) (Just txnParsed) 324 | expectTx (relayStartingSlot + 1) 0 (Just txnParsed) 325 | expectTx (relayStartingSlot + 2) (txsPerSlot - 1) (Just txnParsed) 326 | expectTx tamperedNoVotesSlot 0 Nothing 327 | expectTx tamperedNoVotesSlot 1 (Just txnTamperedNoVotes) 328 | 329 | it "survives challenge of valid vote signatures" $ do 330 | submitChallenge relayStartingSlot 0 0 331 | submitChallenge relayStartingSlot 1 0 332 | submitChallenge (relayStartingSlot + 1) 0 0 333 | submitChallenge (relayStartingSlot + 1) 1 0 334 | 335 | expectTx relayStartingSlot 0 (Just txnParsed) 336 | 337 | it "survives challenge of non-relayed transactions" $ do 338 | runExceptT (challengeVote node contract tamperedNoVotesSlot 0 0) `shouldThrow` (\(_ :: JsonRpcException) -> True) 339 | expectContractAlive True 340 | 341 | it "self-destructs on challenge of invalid vote signatures" $ do 342 | submitChallenge tamperedNoVotesSlot 1 0 343 | expectContractAlive False 344 | -------------------------------------------------------------------------------- /solana-bridges/src/Ethereum/Contracts.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE DeriveGeneric #-} 3 | {-# LANGUAGE FlexibleContexts #-} 4 | {-# LANGUAGE FlexibleInstances #-} 5 | {-# LANGUAGE LambdaCase #-} 6 | {-# LANGUAGE MultiParamTypeClasses #-} 7 | {-# LANGUAGE NumDecimals #-} 8 | {-# LANGUAGE OverloadedStrings #-} 9 | {-# LANGUAGE TupleSections #-} 10 | {-# LANGUAGE TypeApplications #-} 11 | {-# LANGUAGE GADTs #-} 12 | {-# LANGUAGE RankNTypes #-} 13 | {-# LANGUAGE ScopedTypeVariables #-} 14 | {-# LANGUAGE ConstraintKinds #-} 15 | {-# LANGUAGE TypeOperators #-} 16 | 17 | {-# LANGUAGE OverloadedLists #-} 18 | {-# LANGUAGE EmptyCase #-} 19 | 20 | module Ethereum.Contracts where 21 | 22 | import Control.Lens hiding (index) 23 | import Control.Monad 24 | import Control.Monad.Except (MonadError, throwError) 25 | import Control.Monad.IO.Class 26 | import Crypto.Error (CryptoFailable(..)) 27 | import Crypto.Hash (Digest, SHA256, digestFromByteString) 28 | import Data.ByteArray.HexString (HexString) 29 | import Data.ByteArray.Sized (unSizedByteArray, unsafeSizedByteArray) 30 | import Data.ByteString (ByteString) 31 | import qualified Data.ByteString.Lazy as LBS 32 | import Data.Foldable 33 | import Data.Functor.Compose 34 | import Data.List.NonEmpty (NonEmpty) 35 | import qualified Data.List.NonEmpty as NonEmpty 36 | import Data.Map (Map) 37 | import Data.Solidity.Prim.Address (Address) 38 | import Data.Word 39 | import GHC.Exts (fromList) 40 | import Network.Web3.Provider (runWeb3') 41 | import qualified Crypto.PubKey.Ed25519 as Ed25519 42 | import qualified Data.Binary as Binary 43 | import qualified Data.ByteArray as ByteArray 44 | import qualified Data.ByteString as BS 45 | import qualified Data.Map.Strict as Map 46 | import qualified Data.Sequence as Sequence 47 | import qualified Data.Solidity.Prim.Bytes as Solidity 48 | import qualified Data.Solidity.Prim.Int as Solidity 49 | import qualified Network.Ethereum.Account as Eth 50 | import qualified Network.Ethereum.Api.Eth as Eth (getBalance, getCode) 51 | import qualified Network.Ethereum.Api.Types as Eth (TxReceipt(..), DefaultBlock(Latest)) 52 | import qualified Network.Ethereum.Unit as Eth 53 | import qualified Network.Web3.Provider as Eth 54 | 55 | import Solana.Types 56 | import qualified Ethereum.Contracts.Bindings as Contracts 57 | 58 | test_sha512 :: (MonadError String m, MonadIO m) => Eth.Provider -> Address -> BS.ByteString -> m BS.ByteString 59 | test_sha512 node ca a = bytesFromSol <$> simulate node ca "test_sha512" (Contracts.test_sha512 (bytesToSol a)) 60 | 61 | test_sha512_gas :: (MonadError String m, MonadIO m) => Eth.Provider -> Address -> BS.ByteString -> m Eth.TxReceipt 62 | test_sha512_gas node ca a = simulate node ca "test_sha512" (Eth.send $ Contracts.Test_sha512Data (bytesToSol a)) 63 | 64 | test_ed25519_verify 65 | :: (MonadError String m, MonadIO m) 66 | => Eth.Provider -> Address 67 | -> BS.ByteString -> BS.ByteString -> Base58ByteString 68 | -> m Bool 69 | test_ed25519_verify node ca sig msg pk = simulate node ca "test_ed25519_verify" (Contracts.test_ed25519_verify (bytesToSol sig) (bytesToSol msg) (unsafeBytes32ToSol pk)) 70 | 71 | test_ed25519_verify_gas 72 | :: (MonadError String m, MonadIO m) 73 | => Eth.Provider -> Address 74 | -> BS.ByteString -> BS.ByteString -> Base58ByteString 75 | -> m Eth.TxReceipt 76 | test_ed25519_verify_gas node ca sig msg pk = simulate node ca "" (Eth.send $ Contracts.Test_ed25519_verifyData (bytesToSol sig) (bytesToSol msg) (unsafeBytes32ToSol pk)) 77 | 78 | getInitialized :: (MonadError String m, MonadIO m) => Eth.Provider -> Address -> m Bool 79 | getInitialized node ca = simulate node ca "initialized" Contracts.initialized 80 | 81 | getLastSlot :: (MonadError String m, MonadIO m) => Eth.Provider -> Address -> m Word64 82 | getLastSlot node ca = word64FromSol <$> simulate node ca "lastSlot" Contracts.lastSlot 83 | 84 | getLastHash :: (MonadError String m, MonadIO m) => Eth.Provider -> Address -> m Base58ByteString 85 | getLastHash node ca = Base58ByteString . ByteArray.convert . unSizedByteArray <$> simulate node ca "lastHash" Contracts.lastHash 86 | 87 | getSignatures :: (MonadError String m, MonadIO m) => Eth.Provider -> Address -> Word64 -> Word64 -> m (Maybe ByteString) 88 | getSignatures node ca slot tx = fmap convert $ simulate node ca "getSignatures" 89 | $ Contracts.getSignatures (fromIntegral slot) (fromIntegral tx) 90 | where 91 | convert (hasTx, signatures) = if hasTx then Just (ByteArray.convert signatures) else Nothing 92 | 93 | getMessage :: (MonadError String m, MonadIO m) => Eth.Provider -> Address -> Word64 -> Word64 -> m (Maybe ByteString) 94 | getMessage node ca slot tx = fmap convert $ simulate node ca "getMessage" 95 | $ Contracts.getMessage (fromIntegral slot) (fromIntegral tx) 96 | where 97 | convert (hasTx, message) = if hasTx then Just (ByteArray.convert message) else Nothing 98 | 99 | 100 | data ContractSlot = ContractSlot 101 | { _contractSlot_hasBlock :: Bool 102 | , _contractSlot_blockHash :: Base58ByteString 103 | , _contractSlot_leaderPublicKey :: Base58ByteString 104 | , _contractSlot_voteCounts :: Word64 105 | } deriving (Eq, Ord, Show) 106 | 107 | getSlot :: (MonadError String m, MonadIO m) => Eth.Provider -> Address -> Word64 -> m ContractSlot 108 | getSlot node ca = fmap convert . simulate node ca "slots" . Contracts.getSlot_ . fromIntegral 109 | where 110 | convert (hasBlock, hash, leader, votes) = 111 | ContractSlot 112 | hasBlock 113 | (bytes32FromSol hash) 114 | (bytes32FromSol leader) 115 | (fromIntegral votes) 116 | 117 | initialize :: (MonadIO m, MonadError String m) 118 | => Eth.Provider 119 | -> Address 120 | -> Word64 121 | -> SolanaCommittedBlock 122 | -> Base58ByteString 123 | -> SolanaEpochSchedule 124 | -> m () 125 | initialize node ca slot root leader schedule = void $ submit node ca "initialize" $ Contracts.initialize 126 | (word64ToSol slot)-- uint64 slot, 127 | (unsafeBytes32ToSol $ _solanaCommittedBlock_blockhash root)-- bytes32 blockHash, 128 | (unsafeBytes32ToSol $ leader)-- bytes32 leader, 129 | (_solanaEpochSchedule_warmup schedule)-- bool scheduleWarmup, 130 | (word64ToSol $ _solanaEpochSchedule_firstNormalEpoch schedule)-- uint64 scheduleFirstNormalEpoch, 131 | (word64ToSol $ _solanaEpochSchedule_leaderScheduleSlotOffset schedule)-- uint64 scheduleLeaderScheduleSlotOffset, 132 | (word64ToSol $ _solanaEpochSchedule_firstNormalSlot schedule)-- uint64 scheduleFirstNormalSlot, 133 | (word64ToSol $ _solanaEpochSchedule_slotsPerEpoch schedule)-- uint64 scheduleSlotsPerEpoch 134 | (blockTransactions root) 135 | 136 | addBlocks 137 | :: (MonadIO m, MonadError String m) 138 | => Eth.Provider 139 | -> Address 140 | -> NonEmpty (Word64, SolanaCommittedBlock, Base58ByteString) 141 | -> m Eth.TxReceipt 142 | addBlocks node ca blocks = submit node ca "addBlocks" $ Contracts.addBlocks 143 | ( word64ToSol $ _solanaCommittedBlock_parentSlot parent 144 | , unsafeBytes32ToSol $ _solanaCommittedBlock_previousBlockhash parent 145 | ) 146 | (toList $ flip fmap blocks $ \(slot,block,leader) -> 147 | ( word64ToSol slot 148 | , unsafeBytes32ToSol $ _solanaCommittedBlock_blockhash block 149 | , unsafeBytes32ToSol leader 150 | ) 151 | ) 152 | (toList $ flip fmap blocks $ \(_, b, _) -> blockTransactions b) 153 | where 154 | (_, parent, _) = NonEmpty.head blocks 155 | 156 | mergeSchedules :: Map Word64 SolanaLeaderSchedule -> SolanaEpochSchedule -> Map Word64 Base58ByteString 157 | mergeSchedules leaderSchedule epochSchedule = flip ifoldMap (Compose leaderSchedule) $ \(epoch, leaderPk) slotIndices -> 158 | foldMap (\slotIndex -> Map.singleton (firstSlotInEpoch epochSchedule epoch + slotIndex) leaderPk) slotIndices 159 | 160 | blockTransactions :: SolanaCommittedBlock -> [(Bool, Solidity.Bytes, Solidity.Bytes)] 161 | blockTransactions b = 162 | flip fmap (fmap _solanaTxnWithMeta_transaction $ _solanaCommittedBlock_transactions b) $ \tx -> 163 | if isVoteTxn tx 164 | then 165 | ( True 166 | , ByteArray.convert $ LBS.toStrict $ Binary.encode $ tx & _solanaTxn_signatures 167 | , ByteArray.convert $ LBS.toStrict $ Binary.encode $ tx & _solanaTxn_message 168 | ) 169 | else 170 | ( False 171 | , "" 172 | , "" 173 | ) 174 | 175 | 176 | challengeVote 177 | :: (MonadIO m, MonadError String m) 178 | => Eth.Provider 179 | -> Address 180 | -> Word64 181 | -> Word64 182 | -> Word64 183 | -> m Eth.TxReceipt 184 | challengeVote node ca slot tx instruction = submit node ca "challengeVote" 185 | $ Contracts.challengeVote (fromIntegral slot) (fromIntegral tx) (fromIntegral instruction) 186 | 187 | verifyVote_gas 188 | :: (MonadError String m, MonadIO m) 189 | => Eth.Provider 190 | -> Address 191 | -> ByteString 192 | -> ByteString 193 | -> Word64 194 | -> m Eth.TxReceipt 195 | verifyVote_gas node ca sigs msg idx = simulate node ca "verifyVote" 196 | $ Eth.send $ Contracts.VerifyVoteData (ByteArray.convert sigs) (ByteArray.convert msg) (fromIntegral idx) 197 | 198 | verifyVote 199 | :: (MonadError String m, MonadIO m) 200 | => Eth.Provider 201 | -> Address 202 | -> ByteString 203 | -> ByteString 204 | -> Word64 205 | -> m Bool 206 | verifyVote node ca sigs msg idx = simulate node ca "verifyVote" 207 | $ Contracts.verifyVote (ByteArray.convert sigs) (ByteArray.convert msg) (fromIntegral idx) 208 | 209 | -- TODO: misbehaving bindings - parsing is fixed in https://github.com/obsidiansystems/hs-web3/tree/tupple-array but encoding seems broken 210 | parseSolanaMessage 211 | :: (MonadError String m, MonadIO m) 212 | => Eth.Provider 213 | -> Address 214 | -> ByteString 215 | -> m SolanaTxnMessage 216 | parseSolanaMessage node ca msg = fmap convert $ simulate node ca "parseSolanaMessage_" $ Contracts.parseSolanaMessage_ (ByteArray.convert msg) 217 | where 218 | -- bytes = CompactByteArray . LBS.fromStrict . ByteArray.convert 219 | convert (requiredSignatures, readOnlySignatures, readOnlyUnsigned, addresses, recentBlockHash) --, _instructions) 220 | = SolanaTxnMessage 221 | (SolanaTxnHeader (fromIntegral requiredSignatures) (fromIntegral readOnlySignatures) (fromIntegral readOnlyUnsigned)) 222 | (LengthPrefixedArray . Sequence.fromList . fmap unsafeBytes32ToPublicKey $ addresses) 223 | (bytes32ToSha256 recentBlockHash) 224 | (LengthPrefixedArray $ Sequence.fromList []) 225 | {- 226 | (LengthPrefixedArray $ Sequence.fromList $ flip fmap instructions $ 227 | \(programId, accounts, data') -> SolanaTxnInstruction 228 | (fromIntegral programId) 229 | (bytes accounts) 230 | (bytes data')) 231 | -} 232 | 233 | parseInstruction 234 | :: (MonadError String m, MonadIO m) 235 | => Eth.Provider 236 | -> Address 237 | -> ByteString 238 | -> Word64 239 | -> m (SolanaTxnInstruction, Word64) 240 | parseInstruction node ca buffer offset = fmap convert $ simulate node ca "parseInstruction_" 241 | $ Contracts.parseInstruction_ (ByteArray.convert buffer) (fromIntegral offset) 242 | where 243 | bytes = CompactByteArray . LBS.fromStrict . ByteArray.convert 244 | convert (programId, accounts, data', consumed) = 245 | (SolanaTxnInstruction (fromIntegral programId) (bytes accounts) (bytes data') 246 | , fromIntegral consumed) 247 | 248 | getSeenBlocks :: (MonadError String m, MonadIO m) => Eth.Provider -> Address -> m Word64 249 | getSeenBlocks node ca = word64FromSol <$> simulate node ca "seenBlocks" Contracts.seenBlocks 250 | 251 | verifyTransactionInclusionProof 252 | :: (MonadError String m, MonadIO m) 253 | => Eth.Provider 254 | -> Address 255 | -> Digest SHA256 256 | -> Digest SHA256 257 | -> [[Digest SHA256]] 258 | -> Digest SHA256 259 | -> ByteString 260 | -> Word64 261 | -> m Bool 262 | verifyTransactionInclusionProof node ca accountsHash blockMerkle subProof bankHashMerkleRoot value transactionIndex = 263 | simulate node ca "verifyTransactionInclusionProof" $ Contracts.verifyTransactionInclusionProof 264 | (sha256ToBytes32 accountsHash) 265 | (sha256ToBytes32 blockMerkle) 266 | (fmap (fromList . fmap (unsafeSizedByteArray . ByteArray.convert . sha256ToBytes32)) subProof) 267 | (sha256ToBytes32 bankHashMerkleRoot) 268 | (ByteArray.convert value) 269 | (word64ToSol transactionIndex) 270 | 271 | verifyMerkleProof :: (MonadError String m, MonadIO m) => Eth.Provider -> Address -> [[Digest SHA256]] -> Digest SHA256 -> ByteString -> Word64 -> m Bool 272 | verifyMerkleProof node ca proof root value index = simulate node ca "verifyMerkleProof" $ 273 | Contracts.verifyMerkleProof 274 | (fmap (fromList . fmap (unsafeSizedByteArray . ByteArray.convert . sha256ToBytes32)) proof) 275 | (sha256ToBytes32 root) 276 | (ByteArray.convert value) 277 | (word64ToSol index) 278 | 279 | -- implementation details 280 | 281 | sha256ToBytes32 :: Digest SHA256 -> Solidity.BytesN 32 282 | sha256ToBytes32 = unsafeSizedByteArray . ByteArray.convert 283 | 284 | bytes32ToSha256 :: Solidity.BytesN 32 -> Digest SHA256 285 | bytes32ToSha256 = maybe (error "bytes32ToSha256: digestFromByteString failed") id . digestFromByteString . unSizedByteArray 286 | 287 | word64ToSol :: Word64 -> Solidity.UIntN 64 288 | word64ToSol = fromInteger . toInteger 289 | 290 | word64FromSol :: Solidity.UIntN 64 -> Word64 291 | word64FromSol = fromInteger . toInteger 292 | 293 | unsafeBytes32ToSol :: Base58ByteString -> Solidity.BytesN 32 294 | unsafeBytes32ToSol = unsafeSizedByteArray . ByteArray.convert . unBase58ByteString 295 | 296 | bytes32FromSol :: Solidity.BytesN 32 -> Base58ByteString 297 | bytes32FromSol = Base58ByteString . ByteArray.convert . unSizedByteArray 298 | 299 | bytesToSol :: BS.ByteString -> Solidity.Bytes 300 | bytesToSol = ByteArray.convert 301 | 302 | bytesFromSol :: Solidity.Bytes -> BS.ByteString 303 | bytesFromSol = ByteArray.convert 304 | 305 | unsafeBytes32ToPublicKey :: Solidity.BytesN 32 -> Ed25519.PublicKey 306 | unsafeBytes32ToPublicKey bytes32 = case Ed25519.publicKey (ByteArray.convert bytes32 :: ByteString) of 307 | CryptoPassed good -> good 308 | CryptoFailed bad -> error $ "unsafeBytes32ToPublicKey: publicKey failed: " <> show bad 309 | 310 | 311 | invokeContract :: Address 312 | -> Eth.DefaultAccount Eth.Web3 a 313 | -> Eth.Web3 a 314 | invokeContract a = Eth.withAccount () 315 | . Eth.withParam (Eth.to .~ a) 316 | . Eth.withParam (Eth.gasPrice .~ (1 :: Eth.Wei)) 317 | 318 | simulate :: (MonadIO m, MonadError String m, Show a) => Eth.Provider -> Address -> String -> Eth.DefaultAccount Eth.Web3 a -> m a 319 | simulate node ca name x = do 320 | let qname = "'" <> name <> "'" 321 | runWeb3' node (invokeContract ca x) >>= \case 322 | Left err -> throwError $ "Failed " <> qname <> ": " <> show err 323 | Right r -> pure r 324 | 325 | submit :: (MonadError String m, MonadIO m) => Eth.Provider -> Address -> String -> Eth.DefaultAccount Eth.Web3 Eth.TxReceipt -> m Eth.TxReceipt 326 | submit node ca name x = do 327 | let qname = "'" <> name <> "'" 328 | runWeb3' node (invokeContract ca x) >>= \case 329 | Left err -> throwError $ "Failed " <> qname <> ": " <> show err 330 | Right r -> do 331 | when (Just 1 /= Eth.receiptStatus r) $ throwError "Contract execution reported failure" 332 | pure r 333 | 334 | getCode :: (MonadError String m, MonadIO m) => Eth.Provider -> Address -> m HexString 335 | getCode node ca = do 336 | runWeb3' node (Eth.getCode ca Eth.Latest) >>= \case 337 | Left err -> throwError $ "Failed getCode@" <> show ca <> ": " <> show err 338 | Right r -> pure r 339 | 340 | getBalance :: (MonadError String m, MonadIO m) => Eth.Provider -> Address -> m Integer 341 | getBalance node ca = do 342 | runWeb3' node (Eth.getBalance ca Eth.Latest) >>= \case 343 | Left err -> throwError $ "Failed getBalance@" <> show ca <> ": " <> show err 344 | Right r -> pure (fromIntegral r) 345 | -------------------------------------------------------------------------------- /solana-bridges/src/Solana/Types.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE FlexibleInstances #-} 3 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 4 | {-# LANGUAGE LambdaCase #-} 5 | {-# LANGUAGE DeriveTraversable #-} 6 | {-# LANGUAGE ScopedTypeVariables #-} 7 | {-# LANGUAGE TypeApplications #-} 8 | {-# LANGUAGE DeriveGeneric #-} 9 | {-# LANGUAGE TemplateHaskell #-} 10 | 11 | {-# OPTIONS_GHC -Wno-orphans #-} 12 | 13 | module Solana.Types where 14 | 15 | import Control.Applicative 16 | import Control.Lens (makeLenses, (<&>)) 17 | import Control.Monad ((<=<)) 18 | import Crypto.Error (CryptoFailable(..)) 19 | import Crypto.Hash (Digest, HashAlgorithm, hashDigestSize, digestFromByteString) 20 | import Crypto.Hash.Algorithms (SHA256) 21 | import Data.Aeson 22 | import Data.Aeson.TH 23 | import Data.Aeson.Types (toJSONKeyText) 24 | import Data.Binary 25 | import Data.Binary.Get 26 | import Data.Binary.Put 27 | import Data.Bits 28 | import Data.Foldable 29 | import Data.Map (Map) 30 | import Data.Sequence (Seq) 31 | import Data.Text (Text) 32 | import GHC.Generics 33 | import qualified Crypto.PubKey.Ed25519 as Ed25519 34 | import qualified Data.ByteArray as ByteArray 35 | import qualified Data.ByteString as BS 36 | import qualified Data.ByteString.Base58 as Base58 37 | import qualified Data.ByteString.Lazy as LBS 38 | import qualified Data.Map.Strict as Map 39 | import qualified Data.Sequence as Seq 40 | import qualified Data.Text as T 41 | import qualified Data.Text.Encoding as T 42 | 43 | data JsonRpcVersion = JsonRpcVersion 44 | instance ToJSON JsonRpcVersion where toJSON _ = toJSON ("2.0" :: T.Text) 45 | instance FromJSON JsonRpcVersion where parseJSON _ = pure JsonRpcVersion 46 | 47 | type SolanaRpcRequest = SolanaRpcRequestF Value 48 | data SolanaRpcRequestF a = SolanaRpcRequest 49 | { _solanaRpcRequest_jsonrpc :: !JsonRpcVersion 50 | , _solanaRpcRequest_id :: !Int 51 | , _solanaRpcRequest_method :: !T.Text 52 | , _solanaRpcRequest_params :: !(Maybe a) 53 | } 54 | 55 | type SolanaRpcResult = SolanaRpcResultF Value 56 | data SolanaRpcResultF a = SolanaRpcResult 57 | { _solanaRpcResult_jsonrpc :: !JsonRpcVersion 58 | , _solanaRpcResult_id :: !Int 59 | , _solanaRpcResult_result :: !a 60 | } 61 | 62 | data SolanaRpcError = SolanaRpcError 63 | { _solanaRpcError_jsonrpc :: !JsonRpcVersion 64 | , _solanaRpcError_id :: !Int 65 | , _solanaRpcError_error :: !Value 66 | } 67 | 68 | type SolanaRpcNotification = SolanaRpcNotificationF Value 69 | data SolanaRpcNotificationF a = SolanaRpcNotification 70 | { _solanaRpcNotification_jsonrpc :: !JsonRpcVersion 71 | , _solanaRpcNotification_method :: !T.Text 72 | , _solanaRpcNotification_params :: !(SolanaRpcNotificationParamsF a) 73 | } 74 | 75 | type SolanaRpcNotificationParams = SolanaRpcNotificationParamsF Value 76 | data SolanaRpcNotificationParamsF a = SolanaRpcNotificationParams 77 | { _solanaRpcNotificationParams_result :: !a 78 | , _solanaRpcNotificationParams_subscription :: !Int 79 | } 80 | 81 | newtype Base58ByteString = Base58ByteString { unBase58ByteString :: BS.ByteString } 82 | deriving (Eq, Ord, Show) 83 | 84 | data SolanaEpochInfo = SolanaEpochInfo 85 | { _solanaEpochInfo_slotsInEpoch :: !Word64 86 | , _solanaEpochInfo_slotIndex :: !Word64 87 | , _solanaEpochInfo_absoluteSlot :: !Word64 88 | , _solanaEpochInfo_blockHeight :: !Word64 89 | , _solanaEpochInfo_epoch :: !Word64 90 | } deriving Show 91 | 92 | data SolanaEpochSchedule = SolanaEpochSchedule 93 | { _solanaEpochSchedule_warmup :: !Bool 94 | , _solanaEpochSchedule_firstNormalEpoch :: !Word64 95 | , _solanaEpochSchedule_leaderScheduleSlotOffset :: !Word64 96 | , _solanaEpochSchedule_firstNormalSlot :: !Word64 97 | , _solanaEpochSchedule_slotsPerEpoch :: !Word64 98 | } deriving Show 99 | 100 | 101 | --- Wellknown addresses 102 | wellKnownAddress :: BS.ByteString -> Ed25519.PublicKey 103 | wellKnownAddress x = case Base58.decodeBase58 Base58.bitcoinAlphabet x of 104 | Nothing -> error "b58" 105 | Just y -> case Ed25519.publicKey y of 106 | CryptoPassed good -> good 107 | CryptoFailed bad -> error $ show bad 108 | 109 | wellKnownPrograms :: [BS.ByteString] 110 | wellKnownPrograms = [ "11111111111111111111111111111111" 111 | , "BPFLoader1111111111111111111111111111111111" 112 | , "Config1111111111111111111111111111111111111" 113 | , "Stake11111111111111111111111111111111111111" 114 | , "KeccakSecp256k11111111111111111111111111111" 115 | , "Vote111111111111111111111111111111111111111" 116 | ] 117 | 118 | solanaVoteProgram :: Ed25519.PublicKey 119 | solanaVoteProgram = wellKnownAddress "Vote111111111111111111111111111111111111111" 120 | 121 | findWellKnownProgram :: Ed25519.PublicKey -> Maybe BS.ByteString 122 | findWellKnownProgram pk = find (\p -> wellKnownAddress p == pk) wellKnownPrograms 123 | 124 | newtype CompactWord16 = CompactWord16 Word16 125 | deriving (Real, Integral, Num, Show, Eq, Ord, Enum) 126 | 127 | instance Binary CompactWord16 where 128 | put (CompactWord16 x) 129 | | x < 1 `shiftL` 7 = putWord8 (fromIntegral x ) 130 | | x < 1 `shiftL` 14 = putWord8 (fromIntegral x .|. 0x80) <> putWord8 (fromIntegral (x `shiftR` 7) ) 131 | | otherwise = putWord8 (fromIntegral x .|. 0x80) <> putWord8 (fromIntegral (x `shiftR` 7) .|. 0x80) <> putWord8 (fromIntegral (x `shiftR` 14)) 132 | 133 | get = getWord8 >>= \x0 -> CompactWord16 <$> if x0 < 1 `shiftL` 7 134 | then pure $ fromIntegral x0 135 | else getWord8 >>= \x1 -> if x1 < 1 `shiftL` 7 136 | then pure $ fromIntegral (x0 .&. 0x7f) .|. shiftL (fromIntegral x1) 7 137 | else getWord8 >>= \x2 -> if x2 < 1 `shiftL` 2 138 | then pure $ fromIntegral (x0 .&. 0x7f) .|. shiftL (fromIntegral (x1 .&. 0x7f)) 7 .|. shiftL (fromIntegral x2) 14 139 | else fail "too big" 140 | 141 | newtype LengthPrefixedArray sz a = LengthPrefixedArray { unCompactArray :: Seq a} 142 | deriving (Eq, Ord, Show, Functor, Foldable, Traversable, Generic) 143 | 144 | instance (Integral sz, Num sz, Binary sz, Binary a) => Binary (LengthPrefixedArray sz a) where 145 | put (LengthPrefixedArray xs) 146 | | length xs /= fromIntegral (fromIntegral (length xs) :: sz) = error "bad LengthPrefixedArray length" 147 | | otherwise = put (fromIntegral (length xs) :: sz) <> traverse_ put xs 148 | 149 | get = do 150 | numXs :: sz <- get 151 | LengthPrefixedArray <$> Seq.replicateM (fromIntegral numXs) get 152 | 153 | type CompactArray = LengthPrefixedArray CompactWord16 154 | 155 | newtype CompactByteArray = CompactByteArray { unCompactByteArray :: LBS.ByteString } 156 | deriving (Eq, Ord, Show) 157 | 158 | instance Binary CompactByteArray where 159 | put (CompactByteArray xs) 160 | | LBS.length xs /= fromIntegral (fromIntegral (LBS.length xs) :: Word16) = error "bad CompactByteArray length" 161 | | otherwise = put (CompactWord16 $ fromIntegral $ LBS.length xs) <> putByteString (LBS.toStrict xs) 162 | 163 | get = do 164 | CompactWord16 numXs <- get 165 | CompactByteArray <$> getLazyByteString (fromIntegral numXs) 166 | 167 | 168 | instance ToJSON a => ToJSON (LengthPrefixedArray sz a) where 169 | toJSON = toJSON . unCompactArray 170 | instance FromJSON a => FromJSON (LengthPrefixedArray sz a) where 171 | parseJSON = fmap LengthPrefixedArray . parseJSON 172 | 173 | instance Binary Ed25519.Signature where 174 | put = putByteString . ByteArray.convert 175 | get = mkEd25519Signature =<< getByteString (Ed25519.signatureSize) 176 | instance Binary Ed25519.PublicKey where 177 | put = putByteString . ByteArray.convert 178 | get = mkEd25519PublicKey =<< getByteString (Ed25519.publicKeySize) 179 | instance HashAlgorithm a => Binary (Digest a) where 180 | put = putByteString . ByteArray.convert 181 | get = mkDigest =<< getByteString (hashDigestSize (undefined :: a)) 182 | 183 | instance ToJSON Ed25519.Signature where 184 | toJSON = toJSON . Base58ByteString . ByteArray.convert 185 | instance ToJSON Ed25519.PublicKey where 186 | toJSON = toJSON . Base58ByteString . ByteArray.convert 187 | instance ToJSON (Digest SHA256) where 188 | toJSON = toJSON . Base58ByteString . ByteArray.convert 189 | 190 | instance FromJSON Ed25519.Signature where 191 | parseJSON = mkEd25519Signature . unBase58ByteString <=< parseJSON 192 | instance FromJSON Ed25519.PublicKey where 193 | parseJSON = mkEd25519PublicKey . unBase58ByteString <=< parseJSON 194 | instance FromJSON (Digest SHA256) where 195 | parseJSON = mkDigest . unBase58ByteString <=< parseJSON 196 | 197 | 198 | mkEd25519PublicKey :: MonadFail m => BS.ByteString -> m Ed25519.PublicKey 199 | mkEd25519PublicKey bs = case Ed25519.publicKey bs of 200 | CryptoPassed good -> pure good 201 | CryptoFailed bad -> fail $ show bad 202 | 203 | mkEd25519Signature :: MonadFail m => BS.ByteString -> m Ed25519.Signature 204 | mkEd25519Signature bs = case Ed25519.signature bs of 205 | CryptoPassed good -> pure good 206 | CryptoFailed bad -> fail $ show bad 207 | 208 | mkDigest :: (MonadFail m, HashAlgorithm ha) => BS.ByteString -> m (Digest ha) 209 | mkDigest bs = case digestFromByteString bs of 210 | Just good -> pure good 211 | Nothing -> fail "bad Digest size" 212 | 213 | -- https://docs.solana.com/transaction#transaction-format 214 | data SolanaTxn = SolanaTxn 215 | { _solanaTxn_signatures :: CompactArray Ed25519.Signature 216 | , _solanaTxn_message :: SolanaTxnMessage 217 | } deriving (Eq, Generic, Show) 218 | instance Binary SolanaTxn 219 | 220 | data SolanaTxnMessage = SolanaTxnMessage 221 | { _solanaTxnMessage_header :: SolanaTxnHeader 222 | , _solanaTxnMessage_accountKeys :: CompactArray Ed25519.PublicKey 223 | , _solanaTxnMessage_recentBlockhash :: Digest SHA256 224 | , _solanaTxnMessage_instructions :: CompactArray SolanaTxnInstruction 225 | } deriving (Eq, Generic, Show) 226 | instance Binary SolanaTxnMessage 227 | 228 | data SolanaTxnHeader = SolanaTxnHeader 229 | { _solanaTxnHeader_numRequiredSignatures :: Word8 230 | , _solanaTxnHeader_numReadonlySignedAccounts :: Word8 231 | , _solanaTxnHeader_numReadonlyUnsignedAccounts :: Word8 232 | } deriving (Eq, Ord, Generic, Show) 233 | instance Binary SolanaTxnHeader 234 | 235 | -- https://github.com/solana-labs/solana/blob/master/sdk/program/src/instruction.rs#L227 236 | data SolanaTxnInstruction = SolanaTxnInstruction 237 | { _solanaTxnInstruction_programIdIndex :: Word8 238 | , _solanaTxnInstruction_accounts :: CompactByteArray 239 | , _solanaTxnInstruction_data :: CompactByteArray 240 | } deriving (Eq, Generic, Show) 241 | instance Binary SolanaTxnInstruction 242 | 243 | instance ToJSON SolanaTxnInstruction where 244 | toJSON tx = object 245 | [ "programIdIndex" .= _solanaTxnInstruction_programIdIndex tx 246 | , "accounts" .= LBS.unpack (unCompactByteArray $ _solanaTxnInstruction_accounts tx) 247 | , "data" .= base58ByteStringToText (Base58ByteString $ LBS.toStrict $ unCompactByteArray $ _solanaTxnInstruction_data tx) 248 | ] 249 | 250 | instance FromJSON SolanaTxnInstruction where 251 | parseJSON = withObject "SolanaTxnInstruction" $ \v -> SolanaTxnInstruction 252 | <$> v .: "programIdIndex" 253 | <*> (v .: "accounts" <&> (CompactByteArray . LBS.pack)) 254 | <*> ((v .: "data") >>= parseData) 255 | where 256 | parseData txt = case parseBase58ByteString txt of 257 | Nothing -> fail "invalid base58" 258 | Just bs -> pure $ CompactByteArray $ LBS.fromStrict $ unBase58ByteString bs 259 | 260 | isVoteTxn :: SolanaTxn -> Bool 261 | isVoteTxn txn = any isVoteInstr (_solanaTxnMessage_instructions $ _solanaTxn_message txn) 262 | where 263 | isVoteInstr :: SolanaTxnInstruction -> Bool 264 | isVoteInstr instr 265 | = Just solanaVoteProgram == Seq.lookup (fromIntegral $ _solanaTxnInstruction_programIdIndex instr) (unCompactArray $ _solanaTxnMessage_accountKeys $ _solanaTxn_message txn) 266 | 267 | 268 | data VoteAuthorize 269 | = VoteAuthorize_Voter 270 | | VoteAuthorize_Withdrawer 271 | deriving (Generic, Eq, Show) 272 | instance Binary VoteAuthorize 273 | 274 | newtype Word64LE = Word64LE { unWord64LE :: Word64 } 275 | deriving (Integral, Num, Eq, Ord, Real, Enum, Bounded, Show) 276 | 277 | instance Binary Word64LE where 278 | get = Word64LE <$> getWord64le 279 | put = putWord64le . unWord64LE 280 | 281 | 282 | data SolanaVote = SolanaVote 283 | { _solanaVote_slots :: LengthPrefixedArray Word64LE Word64LE -- ^ A stack of votes starting with the oldest vote 284 | , _solanaVote_hash :: Digest SHA256-- ^ signature of the bank's state at the last slot 285 | , _solanaVote_timestamp :: Maybe Word64LE -- ^ processing timestamp of last slot 286 | } deriving (Generic, Eq, Show) 287 | instance Binary SolanaVote 288 | 289 | data SolanaVoteInitialize = SolanaVoteInitialize 290 | { _solanaVoteInitialize_nodePubkey :: Ed25519.PublicKey 291 | , _solanaVoteInitialize_authorizedVoter :: Ed25519.PublicKey 292 | , _solanaVoteInitialize_authorizedWithdrawer :: Ed25519.PublicKey 293 | , _solanaVoteInitialize_commission :: Word8 294 | } deriving (Generic, Eq, Show) 295 | instance Binary SolanaVoteInitialize 296 | 297 | data SolanaVoteInstruction 298 | = SolanaVoteInstruction_InitializeAccount SolanaVoteInitialize 299 | | SolanaVoteInstruction_Authorize Ed25519.PublicKey VoteAuthorize 300 | | SolanaVoteInstruction_Vote SolanaVote 301 | | SolanaVoteInstruction_Withdraw Word64LE 302 | | SolanaVoteInstruction_UpdateValidatorIdentity 303 | | SolanaVoteInstruction_UpdateCommission Word8 304 | | SolanaVoteInstruction_VoteSwitch SolanaVote (Digest SHA256) 305 | deriving (Eq, Show) 306 | 307 | instance Binary SolanaVoteInstruction where 308 | get = getWord32le >>= \case 309 | 0 -> SolanaVoteInstruction_InitializeAccount <$> get 310 | 1 -> SolanaVoteInstruction_Authorize <$> get <*> get 311 | 2 -> SolanaVoteInstruction_Vote <$> get 312 | 3 -> SolanaVoteInstruction_Withdraw <$> get 313 | 4 -> pure SolanaVoteInstruction_UpdateValidatorIdentity 314 | 5 -> SolanaVoteInstruction_UpdateCommission <$> get 315 | 6 -> SolanaVoteInstruction_VoteSwitch <$> get <*> get 316 | bad -> fail $ "bad tag for SolanaVoteInstruction: " <> show bad 317 | put = \case 318 | SolanaVoteInstruction_InitializeAccount x -> putWord32le 0 <> put x 319 | SolanaVoteInstruction_Authorize x y -> putWord32le 1 <> put x <> put y 320 | SolanaVoteInstruction_Vote x -> putWord32le 2 <> put x 321 | SolanaVoteInstruction_Withdraw x -> putWord32le 3 <> put x 322 | SolanaVoteInstruction_UpdateValidatorIdentity -> putWord32le 4 323 | SolanaVoteInstruction_UpdateCommission x -> putWord32le 5 <> put x 324 | SolanaVoteInstruction_VoteSwitch x y -> putWord32le 6 <> put x <> put y 325 | 326 | 327 | firstSlotInEpoch :: SolanaEpochSchedule -> Word64 -> Word64 328 | firstSlotInEpoch schedule = 329 | let 330 | warmup0 = (\x -> div (_solanaEpochSchedule_slotsPerEpoch schedule) $ 2 ^ x) 331 | <$> reverse [1.._solanaEpochSchedule_firstNormalEpoch schedule] 332 | warmup = Map.fromList $ zip [0..] $ scanl (+) 0 warmup0 333 | in \epoch -> case Map.lookup epoch warmup of 334 | Nothing -> _solanaEpochSchedule_firstNormalSlot schedule + (satsub epoch $ _solanaEpochSchedule_firstNormalEpoch schedule) * _solanaEpochSchedule_slotsPerEpoch schedule 335 | Just slot -> slot 336 | 337 | 338 | epochFromSlot :: SolanaEpochSchedule -> Word64 -> SolanaEpochInfo 339 | epochFromSlot schedule = 340 | let 341 | warmup0 = (\x -> div (_solanaEpochSchedule_slotsPerEpoch schedule) $ 2 ^ x) 342 | <$> reverse [1.._solanaEpochSchedule_firstNormalEpoch schedule] 343 | warmup = zip [0..] $ zip warmup0 $ drop 1 $ scanl (+) 0 warmup0 344 | in \absoluteSlot -> 345 | let 346 | (epoch, (slotsInEpoch, firstSlotInEpoch0)) = if absoluteSlot >= _solanaEpochSchedule_firstNormalSlot schedule 347 | then 348 | (_solanaEpochSchedule_firstNormalEpoch schedule + (absoluteSlot - _solanaEpochSchedule_firstNormalSlot schedule) `div` _solanaEpochSchedule_slotsPerEpoch schedule 349 | , ( _solanaEpochSchedule_slotsPerEpoch schedule 350 | , _solanaEpochSchedule_firstNormalSlot schedule + (epoch - _solanaEpochSchedule_firstNormalEpoch schedule) * _solanaEpochSchedule_slotsPerEpoch schedule 351 | ) 352 | ) 353 | else 354 | case reverse $ filter (\(_, (_, firstSlotInEpoch')) -> firstSlotInEpoch' <= absoluteSlot) warmup of 355 | [] -> error "Network too recent: epoch info is not available yet" 356 | (x:_) -> x 357 | in SolanaEpochInfo 358 | { _solanaEpochInfo_slotsInEpoch = slotsInEpoch 359 | , _solanaEpochInfo_slotIndex = absoluteSlot - firstSlotInEpoch0 360 | , _solanaEpochInfo_absoluteSlot = absoluteSlot 361 | , _solanaEpochInfo_blockHeight = 0 -- or maybe this should be undefined? 362 | , _solanaEpochInfo_epoch = epoch 363 | } 364 | 365 | 366 | parseBase58ByteString :: Text -> Maybe Base58ByteString 367 | parseBase58ByteString pk58Text = Base58ByteString <$> Base58.decodeBase58 Base58.bitcoinAlphabet (T.encodeUtf8 pk58Text) 368 | base58ByteStringToText :: Base58ByteString -> Text 369 | base58ByteStringToText = T.decodeLatin1 . Base58.encodeBase58 Base58.bitcoinAlphabet . unBase58ByteString 370 | 371 | instance FromJSON Base58ByteString where 372 | parseJSON = withText "Base58ByteString" $ maybe (fail "invalid base58") pure . parseBase58ByteString 373 | instance ToJSON Base58ByteString where 374 | toJSON = toJSON . base58ByteStringToText 375 | 376 | instance ToJSONKey Base58ByteString where 377 | toJSONKey = toJSONKeyText base58ByteStringToText 378 | instance FromJSONKey Base58ByteString where 379 | fromJSONKey = FromJSONKeyTextParser (maybe (fail "invalid base58") pure . parseBase58ByteString) 380 | 381 | data SolanaCommitment 382 | = SolanaCommitment_Max 383 | | SolanaCommitment_Root 384 | | SolanaCommitment_SingleGossip 385 | | SolanaCommitment_Recent 386 | deriving (Eq, Ord, Show, Enum) 387 | 388 | instance ToJSON SolanaCommitment where 389 | toJSON x = object ["commitment" Data.Aeson..= case x of 390 | SolanaCommitment_Max -> "max" :: Text 391 | SolanaCommitment_Root -> "root" 392 | SolanaCommitment_SingleGossip -> "singleGossip" 393 | SolanaCommitment_Recent -> "recent" 394 | ] 395 | 396 | data SolanaCommittedBlock = SolanaCommittedBlock 397 | { _solanaCommittedBlock_blockhash :: !Base58ByteString 398 | , _solanaCommittedBlock_previousBlockhash :: !Base58ByteString 399 | , _solanaCommittedBlock_parentSlot :: !Word64 400 | , _solanaCommittedBlock_transactions :: ![SolanaTxnWithMeta] 401 | , _solanaCommittedBlock_rewards :: ![Value] 402 | , _solanaCommittedBlock_blockTime :: !(Maybe Word64) 403 | } deriving Show 404 | 405 | data SolanaTxnWithMeta = SolanaTxnWithMeta 406 | { _solanaTxnWithMeta_transaction :: !SolanaTxn 407 | } deriving (Eq, Show, Generic) 408 | 409 | type SolanaLeaderSchedule = Map Base58ByteString [Word64] 410 | 411 | newtype SolanaRpcErrorOrResult a = SolanaRpcErrorOrResult { unSolanaRpcErrorOrResult :: Either SolanaRpcError (SolanaRpcResultF a) } 412 | 413 | instance FromJSON a => FromJSON (SolanaRpcErrorOrResult a) where 414 | parseJSON x = (SolanaRpcErrorOrResult . Left <$> parseJSON x) 415 | <|> (SolanaRpcErrorOrResult . Right <$> parseJSON x) 416 | 417 | 418 | data SolanaSlotNotification = SolanaSlotNotification 419 | { _solanaSlotNotification_parent :: !Word64 420 | , _solanaSlotNotification_root :: !Word64 421 | , _solanaSlotNotification_slot :: !Word64 422 | } deriving Show 423 | 424 | data SolanaBlockCommitment = SolanaBlockCommitment 425 | { _solanaBlockCommitment_totalStake :: !Word64 426 | , _solanaBlockCommitment_commitment :: ![Word64] 427 | } deriving Show 428 | 429 | satsub :: Word64 -> Word64 -> Word64 430 | satsub x y 431 | | x <= y = 0 432 | | otherwise = x - y 433 | 434 | 435 | do 436 | let x = (defaultOptions { fieldLabelModifier = dropWhile ('_' ==) . dropWhile ('_' /=) . dropWhile ('_' ==) }) 437 | concat <$> traverse (deriveJSON x) 438 | [ ''SolanaRpcResultF 439 | , ''SolanaRpcError 440 | , ''SolanaRpcRequestF 441 | , ''SolanaRpcNotificationF 442 | , ''SolanaRpcNotificationParamsF 443 | , ''SolanaEpochInfo 444 | , ''SolanaEpochSchedule 445 | , ''SolanaSlotNotification 446 | -- , ''SolanaVote 447 | , ''SolanaBlockCommitment 448 | , ''SolanaCommittedBlock 449 | , ''SolanaTxnWithMeta 450 | , ''SolanaTxn 451 | , ''SolanaTxnMessage 452 | , ''SolanaTxnHeader 453 | ] 454 | 455 | concat <$> traverse (makeLenses) 456 | [ ''SolanaTxn 457 | , ''SolanaTxnMessage 458 | , ''SolanaTxnHeader 459 | , ''SolanaTxnInstruction 460 | ] 461 | -------------------------------------------------------------------------------- /solana-client-tool/node-env.nix: -------------------------------------------------------------------------------- 1 | # This file originates from node2nix 2 | 3 | {stdenv, nodejs, python2, utillinux, libtool, runCommand, writeTextFile}: 4 | 5 | let 6 | python = if nodejs ? python then nodejs.python else python2; 7 | 8 | # Create a tar wrapper that filters all the 'Ignoring unknown extended header keyword' noise 9 | tarWrapper = runCommand "tarWrapper" {} '' 10 | mkdir -p $out/bin 11 | 12 | cat > $out/bin/tar <> $out/nix-support/hydra-build-products 37 | ''; 38 | }; 39 | 40 | includeDependencies = {dependencies}: 41 | stdenv.lib.optionalString (dependencies != []) 42 | (stdenv.lib.concatMapStrings (dependency: 43 | '' 44 | # Bundle the dependencies of the package 45 | mkdir -p node_modules 46 | cd node_modules 47 | 48 | # Only include dependencies if they don't exist. They may also be bundled in the package. 49 | if [ ! -e "${dependency.name}" ] 50 | then 51 | ${composePackage dependency} 52 | fi 53 | 54 | cd .. 55 | '' 56 | ) dependencies); 57 | 58 | # Recursively composes the dependencies of a package 59 | composePackage = { name, packageName, src, dependencies ? [], ... }@args: 60 | builtins.addErrorContext "while evaluating node package '${packageName}'" '' 61 | DIR=$(pwd) 62 | cd $TMPDIR 63 | 64 | unpackFile ${src} 65 | 66 | # Make the base dir in which the target dependency resides first 67 | mkdir -p "$(dirname "$DIR/${packageName}")" 68 | 69 | if [ -f "${src}" ] 70 | then 71 | # Figure out what directory has been unpacked 72 | packageDir="$(find . -maxdepth 1 -type d | tail -1)" 73 | 74 | # Restore write permissions to make building work 75 | find "$packageDir" -type d -exec chmod u+x {} \; 76 | chmod -R u+w "$packageDir" 77 | 78 | # Move the extracted tarball into the output folder 79 | mv "$packageDir" "$DIR/${packageName}" 80 | elif [ -d "${src}" ] 81 | then 82 | # Get a stripped name (without hash) of the source directory. 83 | # On old nixpkgs it's already set internally. 84 | if [ -z "$strippedName" ] 85 | then 86 | strippedName="$(stripHash ${src})" 87 | fi 88 | 89 | # Restore write permissions to make building work 90 | chmod -R u+w "$strippedName" 91 | 92 | # Move the extracted directory into the output folder 93 | mv "$strippedName" "$DIR/${packageName}" 94 | fi 95 | 96 | # Unset the stripped name to not confuse the next unpack step 97 | unset strippedName 98 | 99 | # Include the dependencies of the package 100 | cd "$DIR/${packageName}" 101 | ${includeDependencies { inherit dependencies; }} 102 | cd .. 103 | ${stdenv.lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."} 104 | ''; 105 | 106 | pinpointDependencies = {dependencies, production}: 107 | let 108 | pinpointDependenciesFromPackageJSON = writeTextFile { 109 | name = "pinpointDependencies.js"; 110 | text = '' 111 | var fs = require('fs'); 112 | var path = require('path'); 113 | 114 | function resolveDependencyVersion(location, name) { 115 | if(location == process.env['NIX_STORE']) { 116 | return null; 117 | } else { 118 | var dependencyPackageJSON = path.join(location, "node_modules", name, "package.json"); 119 | 120 | if(fs.existsSync(dependencyPackageJSON)) { 121 | var dependencyPackageObj = JSON.parse(fs.readFileSync(dependencyPackageJSON)); 122 | 123 | if(dependencyPackageObj.name == name) { 124 | return dependencyPackageObj.version; 125 | } 126 | } else { 127 | return resolveDependencyVersion(path.resolve(location, ".."), name); 128 | } 129 | } 130 | } 131 | 132 | function replaceDependencies(dependencies) { 133 | if(typeof dependencies == "object" && dependencies !== null) { 134 | for(var dependency in dependencies) { 135 | var resolvedVersion = resolveDependencyVersion(process.cwd(), dependency); 136 | 137 | if(resolvedVersion === null) { 138 | process.stderr.write("WARNING: cannot pinpoint dependency: "+dependency+", context: "+process.cwd()+"\n"); 139 | } else { 140 | dependencies[dependency] = resolvedVersion; 141 | } 142 | } 143 | } 144 | } 145 | 146 | /* Read the package.json configuration */ 147 | var packageObj = JSON.parse(fs.readFileSync('./package.json')); 148 | 149 | /* Pinpoint all dependencies */ 150 | replaceDependencies(packageObj.dependencies); 151 | if(process.argv[2] == "development") { 152 | replaceDependencies(packageObj.devDependencies); 153 | } 154 | replaceDependencies(packageObj.optionalDependencies); 155 | 156 | /* Write the fixed package.json file */ 157 | fs.writeFileSync("package.json", JSON.stringify(packageObj, null, 2)); 158 | ''; 159 | }; 160 | in 161 | '' 162 | node ${pinpointDependenciesFromPackageJSON} ${if production then "production" else "development"} 163 | 164 | ${stdenv.lib.optionalString (dependencies != []) 165 | '' 166 | if [ -d node_modules ] 167 | then 168 | cd node_modules 169 | ${stdenv.lib.concatMapStrings (dependency: pinpointDependenciesOfPackage dependency) dependencies} 170 | cd .. 171 | fi 172 | ''} 173 | ''; 174 | 175 | # Recursively traverses all dependencies of a package and pinpoints all 176 | # dependencies in the package.json file to the versions that are actually 177 | # being used. 178 | 179 | pinpointDependenciesOfPackage = { packageName, dependencies ? [], production ? true, ... }@args: 180 | '' 181 | if [ -d "${packageName}" ] 182 | then 183 | cd "${packageName}" 184 | ${pinpointDependencies { inherit dependencies production; }} 185 | cd .. 186 | ${stdenv.lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."} 187 | fi 188 | ''; 189 | 190 | # Extract the Node.js source code which is used to compile packages with 191 | # native bindings 192 | nodeSources = runCommand "node-sources" {} '' 193 | tar --no-same-owner --no-same-permissions -xf ${nodejs.src} 194 | mv node-* $out 195 | ''; 196 | 197 | # Script that adds _integrity fields to all package.json files to prevent NPM from consulting the cache (that is empty) 198 | addIntegrityFieldsScript = writeTextFile { 199 | name = "addintegrityfields.js"; 200 | text = '' 201 | var fs = require('fs'); 202 | var path = require('path'); 203 | 204 | function augmentDependencies(baseDir, dependencies) { 205 | for(var dependencyName in dependencies) { 206 | var dependency = dependencies[dependencyName]; 207 | 208 | // Open package.json and augment metadata fields 209 | var packageJSONDir = path.join(baseDir, "node_modules", dependencyName); 210 | var packageJSONPath = path.join(packageJSONDir, "package.json"); 211 | 212 | if(fs.existsSync(packageJSONPath)) { // Only augment packages that exist. Sometimes we may have production installs in which development dependencies can be ignored 213 | console.log("Adding metadata fields to: "+packageJSONPath); 214 | var packageObj = JSON.parse(fs.readFileSync(packageJSONPath)); 215 | 216 | if(dependency.integrity) { 217 | packageObj["_integrity"] = dependency.integrity; 218 | } else { 219 | packageObj["_integrity"] = "sha1-000000000000000000000000000="; // When no _integrity string has been provided (e.g. by Git dependencies), add a dummy one. It does not seem to harm and it bypasses downloads. 220 | } 221 | 222 | if(dependency.resolved) { 223 | packageObj["_resolved"] = dependency.resolved; // Adopt the resolved property if one has been provided 224 | } else { 225 | packageObj["_resolved"] = dependency.version; // Set the resolved version to the version identifier. This prevents NPM from cloning Git repositories. 226 | } 227 | 228 | if(dependency.from !== undefined) { // Adopt from property if one has been provided 229 | packageObj["_from"] = dependency.from; 230 | } 231 | 232 | fs.writeFileSync(packageJSONPath, JSON.stringify(packageObj, null, 2)); 233 | } 234 | 235 | // Augment transitive dependencies 236 | if(dependency.dependencies !== undefined) { 237 | augmentDependencies(packageJSONDir, dependency.dependencies); 238 | } 239 | } 240 | } 241 | 242 | if(fs.existsSync("./package-lock.json")) { 243 | var packageLock = JSON.parse(fs.readFileSync("./package-lock.json")); 244 | 245 | if(packageLock.lockfileVersion !== 1) { 246 | process.stderr.write("Sorry, I only understand lock file version 1!\n"); 247 | process.exit(1); 248 | } 249 | 250 | if(packageLock.dependencies !== undefined) { 251 | augmentDependencies(".", packageLock.dependencies); 252 | } 253 | } 254 | ''; 255 | }; 256 | 257 | # Reconstructs a package-lock file from the node_modules/ folder structure and package.json files with dummy sha1 hashes 258 | reconstructPackageLock = writeTextFile { 259 | name = "addintegrityfields.js"; 260 | text = '' 261 | var fs = require('fs'); 262 | var path = require('path'); 263 | 264 | var packageObj = JSON.parse(fs.readFileSync("package.json")); 265 | 266 | var lockObj = { 267 | name: packageObj.name, 268 | version: packageObj.version, 269 | lockfileVersion: 1, 270 | requires: true, 271 | dependencies: {} 272 | }; 273 | 274 | function augmentPackageJSON(filePath, dependencies) { 275 | var packageJSON = path.join(filePath, "package.json"); 276 | if(fs.existsSync(packageJSON)) { 277 | var packageObj = JSON.parse(fs.readFileSync(packageJSON)); 278 | dependencies[packageObj.name] = { 279 | version: packageObj.version, 280 | integrity: "sha1-000000000000000000000000000=", 281 | dependencies: {} 282 | }; 283 | processDependencies(path.join(filePath, "node_modules"), dependencies[packageObj.name].dependencies); 284 | } 285 | } 286 | 287 | function processDependencies(dir, dependencies) { 288 | if(fs.existsSync(dir)) { 289 | var files = fs.readdirSync(dir); 290 | 291 | files.forEach(function(entry) { 292 | var filePath = path.join(dir, entry); 293 | var stats = fs.statSync(filePath); 294 | 295 | if(stats.isDirectory()) { 296 | if(entry.substr(0, 1) == "@") { 297 | // When we encounter a namespace folder, augment all packages belonging to the scope 298 | var pkgFiles = fs.readdirSync(filePath); 299 | 300 | pkgFiles.forEach(function(entry) { 301 | if(stats.isDirectory()) { 302 | var pkgFilePath = path.join(filePath, entry); 303 | augmentPackageJSON(pkgFilePath, dependencies); 304 | } 305 | }); 306 | } else { 307 | augmentPackageJSON(filePath, dependencies); 308 | } 309 | } 310 | }); 311 | } 312 | } 313 | 314 | processDependencies("node_modules", lockObj.dependencies); 315 | 316 | fs.writeFileSync("package-lock.json", JSON.stringify(lockObj, null, 2)); 317 | ''; 318 | }; 319 | 320 | prepareAndInvokeNPM = {packageName, bypassCache, reconstructLock, npmFlags, production}: 321 | let 322 | forceOfflineFlag = if bypassCache then "--offline" else "--registry http://www.example.com"; 323 | in 324 | '' 325 | # Pinpoint the versions of all dependencies to the ones that are actually being used 326 | echo "pinpointing versions of dependencies..." 327 | source $pinpointDependenciesScriptPath 328 | 329 | # Patch the shebangs of the bundled modules to prevent them from 330 | # calling executables outside the Nix store as much as possible 331 | patchShebangs . 332 | 333 | # Deploy the Node.js package by running npm install. Since the 334 | # dependencies have been provided already by ourselves, it should not 335 | # attempt to install them again, which is good, because we want to make 336 | # it Nix's responsibility. If it needs to install any dependencies 337 | # anyway (e.g. because the dependency parameters are 338 | # incomplete/incorrect), it fails. 339 | # 340 | # The other responsibilities of NPM are kept -- version checks, build 341 | # steps, postprocessing etc. 342 | 343 | export HOME=$TMPDIR 344 | cd "${packageName}" 345 | runHook preRebuild 346 | 347 | ${stdenv.lib.optionalString bypassCache '' 348 | ${stdenv.lib.optionalString reconstructLock '' 349 | if [ -f package-lock.json ] 350 | then 351 | echo "WARNING: Reconstruct lock option enabled, but a lock file already exists!" 352 | echo "This will most likely result in version mismatches! We will remove the lock file and regenerate it!" 353 | rm package-lock.json 354 | else 355 | echo "No package-lock.json file found, reconstructing..." 356 | fi 357 | 358 | node ${reconstructPackageLock} 359 | ''} 360 | 361 | node ${addIntegrityFieldsScript} 362 | ''} 363 | 364 | npm ${forceOfflineFlag} --nodedir=${nodeSources} ${npmFlags} ${stdenv.lib.optionalString production "--production"} rebuild 365 | 366 | if [ "''${dontNpmInstall-}" != "1" ] 367 | then 368 | # NPM tries to download packages even when they already exist if npm-shrinkwrap is used. 369 | rm -f npm-shrinkwrap.json 370 | 371 | npm ${forceOfflineFlag} --nodedir=${nodeSources} ${npmFlags} ${stdenv.lib.optionalString production "--production"} install 372 | fi 373 | ''; 374 | 375 | # Builds and composes an NPM package including all its dependencies 376 | buildNodePackage = 377 | { name 378 | , packageName 379 | , version 380 | , dependencies ? [] 381 | , buildInputs ? [] 382 | , production ? true 383 | , npmFlags ? "" 384 | , dontNpmInstall ? false 385 | , bypassCache ? false 386 | , reconstructLock ? false 387 | , preRebuild ? "" 388 | , dontStrip ? true 389 | , unpackPhase ? "true" 390 | , buildPhase ? "true" 391 | , ... }@args: 392 | 393 | let 394 | extraArgs = removeAttrs args [ "name" "dependencies" "buildInputs" "dontStrip" "dontNpmInstall" "preRebuild" "unpackPhase" "buildPhase" ]; 395 | in 396 | stdenv.mkDerivation ({ 397 | name = "node_${name}-${version}"; 398 | buildInputs = [ tarWrapper python nodejs ] 399 | ++ stdenv.lib.optional (stdenv.isLinux) utillinux 400 | ++ stdenv.lib.optional (stdenv.isDarwin) libtool 401 | ++ buildInputs; 402 | 403 | inherit nodejs; 404 | 405 | inherit dontStrip; # Stripping may fail a build for some package deployments 406 | inherit dontNpmInstall preRebuild unpackPhase buildPhase; 407 | 408 | compositionScript = composePackage args; 409 | pinpointDependenciesScript = pinpointDependenciesOfPackage args; 410 | 411 | passAsFile = [ "compositionScript" "pinpointDependenciesScript" ]; 412 | 413 | installPhase = '' 414 | # Create and enter a root node_modules/ folder 415 | mkdir -p $out/lib/node_modules 416 | cd $out/lib/node_modules 417 | 418 | # Compose the package and all its dependencies 419 | source $compositionScriptPath 420 | 421 | ${prepareAndInvokeNPM { inherit packageName bypassCache reconstructLock npmFlags production; }} 422 | 423 | # Create symlink to the deployed executable folder, if applicable 424 | if [ -d "$out/lib/node_modules/.bin" ] 425 | then 426 | ln -s $out/lib/node_modules/.bin $out/bin 427 | fi 428 | 429 | # Create symlinks to the deployed manual page folders, if applicable 430 | if [ -d "$out/lib/node_modules/${packageName}/man" ] 431 | then 432 | mkdir -p $out/share 433 | for dir in "$out/lib/node_modules/${packageName}/man/"* 434 | do 435 | mkdir -p $out/share/man/$(basename "$dir") 436 | for page in "$dir"/* 437 | do 438 | ln -s $page $out/share/man/$(basename "$dir") 439 | done 440 | done 441 | fi 442 | 443 | # Run post install hook, if provided 444 | runHook postInstall 445 | ''; 446 | } // extraArgs); 447 | 448 | # Builds a development shell 449 | buildNodeShell = 450 | { name 451 | , packageName 452 | , version 453 | , src 454 | , dependencies ? [] 455 | , buildInputs ? [] 456 | , production ? true 457 | , npmFlags ? "" 458 | , dontNpmInstall ? false 459 | , bypassCache ? false 460 | , reconstructLock ? false 461 | , dontStrip ? true 462 | , unpackPhase ? "true" 463 | , buildPhase ? "true" 464 | , ... }@args: 465 | 466 | let 467 | extraArgs = removeAttrs args [ "name" "dependencies" "buildInputs" ]; 468 | 469 | nodeDependencies = stdenv.mkDerivation ({ 470 | name = "node-dependencies-${name}-${version}"; 471 | 472 | buildInputs = [ tarWrapper python nodejs ] 473 | ++ stdenv.lib.optional (stdenv.isLinux) utillinux 474 | ++ stdenv.lib.optional (stdenv.isDarwin) libtool 475 | ++ buildInputs; 476 | 477 | inherit dontStrip; # Stripping may fail a build for some package deployments 478 | inherit dontNpmInstall unpackPhase buildPhase; 479 | 480 | includeScript = includeDependencies { inherit dependencies; }; 481 | pinpointDependenciesScript = pinpointDependenciesOfPackage args; 482 | 483 | passAsFile = [ "includeScript" "pinpointDependenciesScript" ]; 484 | 485 | installPhase = '' 486 | mkdir -p $out/${packageName} 487 | cd $out/${packageName} 488 | 489 | source $includeScriptPath 490 | 491 | # Create fake package.json to make the npm commands work properly 492 | cp ${src}/package.json . 493 | chmod 644 package.json 494 | ${stdenv.lib.optionalString bypassCache '' 495 | if [ -f ${src}/package-lock.json ] 496 | then 497 | cp ${src}/package-lock.json . 498 | fi 499 | ''} 500 | 501 | # Go to the parent folder to make sure that all packages are pinpointed 502 | cd .. 503 | ${stdenv.lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."} 504 | 505 | ${prepareAndInvokeNPM { inherit packageName bypassCache reconstructLock npmFlags production; }} 506 | 507 | # Expose the executables that were installed 508 | cd .. 509 | ${stdenv.lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."} 510 | 511 | mv ${packageName} lib 512 | ln -s $out/lib/node_modules/.bin $out/bin 513 | ''; 514 | } // extraArgs); 515 | in 516 | stdenv.mkDerivation { 517 | name = "node-shell-${name}-${version}"; 518 | 519 | buildInputs = [ python nodejs ] ++ stdenv.lib.optional (stdenv.isLinux) utillinux ++ buildInputs; 520 | buildCommand = '' 521 | mkdir -p $out/bin 522 | cat > $out/bin/shell <