Skip to content

Commit a757f6d

Browse files
committed
chore: add n42 e2e test workflow
1 parent 7e9f733 commit a757f6d

File tree

9 files changed

+470
-3
lines changed

9 files changed

+470
-3
lines changed

.github/workflows/release-n42.yml

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,16 @@ jobs:
4747
shared-key: "rust-build-cache"
4848
cache-targets: true
4949
- name: Build release binary
50-
run: cargo build --release
50+
run: |
51+
cargo build --release
52+
cargo build --release -p mobile-sdk --example mobile-sdk-test
5153
- uses: actions/upload-artifact@v4
5254
with:
5355
name: build-artifact
54-
path: target/release/n42
56+
path: |
57+
target/release/n42
58+
target/release/examples/mobile-sdk-test
59+
tests/
5560
- name: Compress artifacts (tar.xz)
5661
run: |
5762
tar -cJf ${{ github.ref_name }}.tar.xz target/release/n42
@@ -81,4 +86,10 @@ jobs:
8186
- uses: actions/download-artifact@v4
8287
with:
8388
name: build-artifact
84-
- run: ./n42 --version
89+
- run: |
90+
mkdir -p target/release
91+
mv n42 target/release/
92+
mkdir -p target/release/examples/
93+
mv mobile-sdk-test target/release/examples/
94+
chmod +x target/release/n42 target/release/examples/mobile-sdk-test
95+
bash tests/e2e.sh

tests/e2e.sh

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/bin/bash
2+
3+
RPC_ADDR=http://127.0.0.1:8545
4+
5+
N42=./target/release/n42
6+
MOBILE_SDK_TEST=./target/release/examples/mobile-sdk-test
7+
8+
if [ ! -x $N42 ] || [ ! -x $MOBILE_SDK_TEST ]; then
9+
echo "binaries for n42 and mobile-sdk-test not found: $N42, $MOBILE_SDK_TEST"
10+
exit
11+
fi
12+
13+
RUST_LOG=debug $N42 node --chain n42-devnet \
14+
--dev.consensus-signer-private-key \
15+
0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
16+
--ws --http \
17+
--http.api "eth,net,web3,txpool,debug,trace" \
18+
--ws.api "eth,net,web3,txpool,debug,trace" \
19+
--http.addr 0.0.0.0 --ws.addr 0.0.0.0 \
20+
--dev.block-time 4s \
21+
--disable-discovery \
22+
>/dev/null 2>&1 &
23+
N42_PID=$!
24+
25+
sleep 10 # wait for n42 to initialize
26+
27+
# install the contracts(deposit & exit)
28+
cd tests/typescript/ && npm install &&
29+
PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 npx hardhat jest --network localdevnet && cd -
30+
31+
$MOBILE_SDK_TEST generate-credentials --number-of-validators 2 >v2.json
32+
$MOBILE_SDK_TEST deposit-for-validators --deposit-private-key \
33+
0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
34+
< v2.json
35+
36+
$MOBILE_SDK_TEST validate-for-validators < v2.json &
37+
MOBILE_SDK_TEST_PID=$!
38+
trap "kill $N42_PID $MOBILE_SDK_TEST_PID" EXIT
39+
40+
VALIDATOR_ADDR=`jq -r '.[0].withdrawal_address' <v2.json`
41+
42+
LIMIT=640
43+
for (( i=1; i<=LIMIT; i++ )); do
44+
BALANCE=`curl -X POST \
45+
-H "Content-Type: application/json" \
46+
-d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getBalance\",\"params\":[\"$VALIDATOR_ADDR\",\"latest\"],\"id\":1}" \
47+
$RPC_ADDR | jq -r '.result'`
48+
echo $BALANCE
49+
if (( "$BALANCE" > 300000 * 1000000000 )); then
50+
break
51+
else
52+
sleep 4
53+
fi
54+
done
55+
$MOBILE_SDK_TEST exit-for-validators < v2.json
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/**
2+
*Submitted for verification at Etherscan.io on 2020-10-14
3+
*/
4+
5+
// ┏━━━┓━┏┓━┏┓━━┏━━━┓━━┏━━━┓━━━━┏━━━┓━━━━━━━━━━━━━━━━━━━┏┓━━━━━┏━━━┓━━━━━━━━━┏┓━━━━━━━━━━━━━━┏┓━
6+
// ┃┏━━┛┏┛┗┓┃┃━━┃┏━┓┃━━┃┏━┓┃━━━━┗┓┏┓┃━━━━━━━━━━━━━━━━━━┏┛┗┓━━━━┃┏━┓┃━━━━━━━━┏┛┗┓━━━━━━━━━━━━┏┛┗┓
7+
// ┃┗━━┓┗┓┏┛┃┗━┓┗┛┏┛┃━━┃┃━┃┃━━━━━┃┃┃┃┏━━┓┏━━┓┏━━┓┏━━┓┏┓┗┓┏┛━━━━┃┃━┗┛┏━━┓┏━┓━┗┓┏┛┏━┓┏━━┓━┏━━┓┗┓┏┛
8+
// ┃┏━━┛━┃┃━┃┏┓┃┏━┛┏┛━━┃┃━┃┃━━━━━┃┃┃┃┃┏┓┃┃┏┓┃┃┏┓┃┃━━┫┣┫━┃┃━━━━━┃┃━┏┓┃┏┓┃┃┏┓┓━┃┃━┃┏┛┗━┓┃━┃┏━┛━┃┃━
9+
// ┃┗━━┓━┃┗┓┃┃┃┃┃┃┗━┓┏┓┃┗━┛┃━━━━┏┛┗┛┃┃┃━┫┃┗┛┃┃┗┛┃┣━━┃┃┃━┃┗┓━━━━┃┗━┛┃┃┗┛┃┃┃┃┃━┃┗┓┃┃━┃┗┛┗┓┃┗━┓━┃┗┓
10+
// ┗━━━┛━┗━┛┗┛┗┛┗━━━┛┗┛┗━━━┛━━━━┗━━━┛┗━━┛┃┏━┛┗━━┛┗━━┛┗┛━┗━┛━━━━┗━━━┛┗━━┛┗┛┗┛━┗━┛┗┛━┗━━━┛┗━━┛━┗━┛
11+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┃┃━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┗┛━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
13+
14+
// SPDX-License-Identifier: CC0-1.0
15+
16+
pragma solidity 0.6.11;
17+
18+
// This interface is designed to be compatible with the Vyper version.
19+
/// @notice This is the Ethereum 2.0 deposit contract interface.
20+
/// For more information see the Phase 0 specification under https://github.com/ethereum/eth2.0-specs
21+
interface IDepositContract {
22+
/// @notice A processed deposit event.
23+
event DepositEvent(
24+
bytes pubkey,
25+
bytes withdrawal_credentials,
26+
bytes amount,
27+
bytes signature,
28+
bytes index
29+
);
30+
31+
/// @notice Submit a Phase 0 DepositData object.
32+
/// @param pubkey A BLS12-381 public key.
33+
/// @param withdrawal_credentials Commitment to a public key for withdrawals.
34+
/// @param signature A BLS12-381 signature.
35+
/// @param deposit_data_root The SHA-256 hash of the SSZ-encoded DepositData object.
36+
/// Used as a protection against malformed input.
37+
function deposit(
38+
bytes calldata pubkey,
39+
bytes calldata withdrawal_credentials,
40+
bytes calldata signature,
41+
bytes32 deposit_data_root
42+
) external payable;
43+
44+
/// @notice Query the current deposit root hash.
45+
/// @return The deposit root hash.
46+
function get_deposit_root() external view returns (bytes32);
47+
48+
/// @notice Query the current deposit count.
49+
/// @return The deposit count encoded as a little endian 64-bit number.
50+
function get_deposit_count() external view returns (bytes memory);
51+
}
52+
53+
// Based on official specification in https://eips.ethereum.org/EIPS/eip-165
54+
interface ERC165 {
55+
/// @notice Query if a contract implements an interface
56+
/// @param interfaceId The interface identifier, as specified in ERC-165
57+
/// @dev Interface identification is specified in ERC-165. This function
58+
/// uses less than 30,000 gas.
59+
/// @return `true` if the contract implements `interfaceId` and
60+
/// `interfaceId` is not 0xffffffff, `false` otherwise
61+
function supportsInterface(bytes4 interfaceId) external pure returns (bool);
62+
}
63+
64+
// This is a rewrite of the Vyper Eth2.0 deposit contract in Solidity.
65+
// It tries to stay as close as possible to the original source code.
66+
/// @notice This is the Ethereum 2.0 deposit contract interface.
67+
/// For more information see the Phase 0 specification under https://github.com/ethereum/eth2.0-specs
68+
contract DepositContract is IDepositContract, ERC165 {
69+
uint constant DEPOSIT_CONTRACT_TREE_DEPTH = 32;
70+
// NOTE: this also ensures `deposit_count` will fit into 64-bits
71+
uint constant MAX_DEPOSIT_COUNT = 2**DEPOSIT_CONTRACT_TREE_DEPTH - 1;
72+
73+
bytes32[DEPOSIT_CONTRACT_TREE_DEPTH] branch;
74+
uint256 deposit_count;
75+
76+
bytes32[DEPOSIT_CONTRACT_TREE_DEPTH] zero_hashes;
77+
78+
constructor() public {
79+
// Compute hashes in empty sparse Merkle tree
80+
for (uint height = 0; height < DEPOSIT_CONTRACT_TREE_DEPTH - 1; height++)
81+
zero_hashes[height + 1] = sha256(abi.encodePacked(zero_hashes[height], zero_hashes[height]));
82+
}
83+
84+
function get_deposit_root() override external view returns (bytes32) {
85+
bytes32 node;
86+
uint size = deposit_count;
87+
for (uint height = 0; height < DEPOSIT_CONTRACT_TREE_DEPTH; height++) {
88+
if ((size & 1) == 1)
89+
node = sha256(abi.encodePacked(branch[height], node));
90+
else
91+
node = sha256(abi.encodePacked(node, zero_hashes[height]));
92+
size /= 2;
93+
}
94+
return sha256(abi.encodePacked(
95+
node,
96+
to_little_endian_64(uint64(deposit_count)),
97+
bytes24(0)
98+
));
99+
}
100+
101+
function get_deposit_count() override external view returns (bytes memory) {
102+
return to_little_endian_64(uint64(deposit_count));
103+
}
104+
105+
function deposit(
106+
bytes calldata pubkey,
107+
bytes calldata withdrawal_credentials,
108+
bytes calldata signature,
109+
bytes32 deposit_data_root
110+
) override external payable {
111+
// Extended ABI length checks since dynamic types are used.
112+
require(pubkey.length == 48, "DepositContract: invalid pubkey length");
113+
require(withdrawal_credentials.length == 32, "DepositContract: invalid withdrawal_credentials length");
114+
require(signature.length == 96, "DepositContract: invalid signature length");
115+
116+
// Check deposit amount
117+
require(msg.value >= 1 ether, "DepositContract: deposit value too low");
118+
require(msg.value % 1 gwei == 0, "DepositContract: deposit value not multiple of gwei");
119+
uint deposit_amount = msg.value / 1 gwei;
120+
require(deposit_amount <= type(uint64).max, "DepositContract: deposit value too high");
121+
122+
// Emit `DepositEvent` log
123+
bytes memory amount = to_little_endian_64(uint64(deposit_amount));
124+
emit DepositEvent(
125+
pubkey,
126+
withdrawal_credentials,
127+
amount,
128+
signature,
129+
to_little_endian_64(uint64(deposit_count))
130+
);
131+
132+
// Compute deposit data root (`DepositData` hash tree root)
133+
bytes32 pubkey_root = sha256(abi.encodePacked(pubkey, bytes16(0)));
134+
bytes32 signature_root = sha256(abi.encodePacked(
135+
sha256(abi.encodePacked(signature[:64])),
136+
sha256(abi.encodePacked(signature[64:], bytes32(0)))
137+
));
138+
bytes32 node = sha256(abi.encodePacked(
139+
sha256(abi.encodePacked(pubkey_root, withdrawal_credentials)),
140+
sha256(abi.encodePacked(amount, bytes24(0), signature_root))
141+
));
142+
143+
// Verify computed and expected deposit data roots match
144+
require(node == deposit_data_root, "DepositContract: reconstructed DepositData does not match supplied deposit_data_root");
145+
146+
// Avoid overflowing the Merkle tree (and prevent edge case in computing `branch`)
147+
require(deposit_count < MAX_DEPOSIT_COUNT, "DepositContract: merkle tree full");
148+
149+
// Add deposit data root to Merkle tree (update a single `branch` node)
150+
deposit_count += 1;
151+
uint size = deposit_count;
152+
for (uint height = 0; height < DEPOSIT_CONTRACT_TREE_DEPTH; height++) {
153+
if ((size & 1) == 1) {
154+
branch[height] = node;
155+
return;
156+
}
157+
node = sha256(abi.encodePacked(branch[height], node));
158+
size /= 2;
159+
}
160+
// As the loop should always end prematurely with the `return` statement,
161+
// this code should be unreachable. We assert `false` just to be safe.
162+
assert(false);
163+
}
164+
165+
function supportsInterface(bytes4 interfaceId) override external pure returns (bool) {
166+
return interfaceId == type(ERC165).interfaceId || interfaceId == type(IDepositContract).interfaceId;
167+
}
168+
169+
function to_little_endian_64(uint64 value) internal pure returns (bytes memory ret) {
170+
ret = new bytes(8);
171+
bytes8 bytesValue = bytes8(value);
172+
// Byteswapping during copying to bytes.
173+
ret[0] = bytesValue[7];
174+
ret[1] = bytesValue[6];
175+
ret[2] = bytesValue[5];
176+
ret[3] = bytesValue[4];
177+
ret[4] = bytesValue[3];
178+
ret[5] = bytesValue[2];
179+
ret[6] = bytesValue[1];
180+
ret[7] = bytesValue[0];
181+
}
182+
}

tests/typescript/hardhat.config.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import "hardhat-jest";
2+
import 'dotenv/config'; // Loads .env file
3+
import { HardhatUserConfig } from "hardhat/config";
4+
import "@nomicfoundation/hardhat-toolbox";
5+
6+
const config: HardhatUserConfig = {
7+
solidity: {
8+
compilers: [
9+
{
10+
version: "0.6.11",
11+
},
12+
{
13+
version: "0.8.28"
14+
}
15+
]
16+
},
17+
networks: {
18+
localdevnet: {
19+
url: "http://127.0.0.1:8545",
20+
accounts: [process.env.PRIVATE_KEY],
21+
chainId: 1143,
22+
},
23+
24+
localtestnet: {
25+
url: "http://127.0.0.1:8545",
26+
accounts: [process.env.PRIVATE_KEY],
27+
chainId: 1142,
28+
},
29+
30+
testnet1: {
31+
url: "http://5.161.252.59:8545/",
32+
accounts: [process.env.PRIVATE_KEY],
33+
chainId: 1142,
34+
},
35+
},
36+
};
37+
38+
export default config;

tests/typescript/jest.config.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Config } from 'jest';
2+
3+
const config: Config = {
4+
// Use a preset to automatically configure Jest for TypeScript
5+
preset: 'ts-jest',
6+
7+
// The test environment that will be used for testing
8+
testEnvironment: 'node',
9+
10+
// Directories to scan for test files
11+
testMatch: ['<rootDir>/test/**/*.test.ts'],
12+
13+
// The root directory that Jest should scan for tests and modules
14+
rootDir: '.',
15+
16+
globalSetup: '<rootDir>/test/jest.global-setup.ts',
17+
};
18+
19+
export default config;

tests/typescript/package.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "pos-test",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"scripts": {
6+
"test": "echo \"Error: no test specified\" && exit 1"
7+
},
8+
"keywords": [],
9+
"author": "",
10+
"license": "ISC",
11+
"description": "",
12+
"devDependencies": {
13+
"@nomicfoundation/hardhat-toolbox": "^6.1.0",
14+
"@typechain/ethers-v6": "^0.5.1",
15+
"@typechain/hardhat": "^9.1.0",
16+
"@types/jest": "^30.0.0",
17+
"@types/node": "^24.3.0",
18+
"dotenv": "^17.2.1",
19+
"hardhat": "^2.26.1",
20+
"hardhat-jest": "^1.0.8",
21+
"jest": "^29.7.0",
22+
"ts-jest": "^29.4.1",
23+
"ts-node": "^10.9.2",
24+
"typechain": "^8.3.2"
25+
},
26+
"dependencies": {
27+
"@noble/curves": "^1.9.6",
28+
"ethereumjs-tx": "^2.1.2"
29+
}
30+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
describe("deposit tests", () => {
2+
it.only("dummy", async () => {
3+
})
4+
});
5+

0 commit comments

Comments
 (0)