[Ethereum] 우분투에 node 설치

execution layer client로 geth를, consensus layer client로 Prysm을 사용한다.

geth 설치

sudo add-apt-repository -y ppa:ethereum/ethereum
sudo apt update
sudo apt install -y geth

확인

geth version

부팅 시 자동 실행

sudo vi /etc/systemd/system/geth.service

[Unit]
Description=Ethereum Geth Node
After=network.target

[Service]
Type=simple
User=dasomoli
ExecStart=/usr/bin/geth --mainnet --syncmode "snap" --datadir "/home/dasomoli/geth/data" --http --http.api web3,eth,txpool --http.addr 0.0.0.0
Restart=always
RestartSec=5
LimitNOFILE=8192

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl start geth
sudo systemctl enable geth
sudo systemctl status geth

위에서는 “snap” 방식을 사용했다. sync mode로 “full”도 가능.

도메인을 연결한다면 설정에 다음 설정을 포함해야 한다.

	--http.corsdomain "*" \
	--http.vhosts "*"

IPC로 geth console 연결

geth attach ipc:/home/dasomoli/geth/data/geth.ipc

로그 확인

journalctl -u geth -f

Prysm 설치

mkdir prysm
cd prysm/
sudo apt update
sudo apt install -y wget
wget <https://raw.githubusercontent.com/prysmaticlabs/prysm/master/prysm.sh>
chmod +x prysm.sh

부팅 시 자동 실행

sudo vi /etc/systemd/system/prysm-beacon.service

[Unit]
Description=Prysm Beacon Chain Client
After=network.target

[Service]
Type=simple
User=dasomoli
ExecStart=/home/dasomoli/prysm/prysm.sh beacon-chain \
    --execution-endpoint=http://localhost:8551 \
    --datadir=/home/dasomoli/prysm/data \
    --jwt-secret=/home/dasomoli/geth/data/geth/jwtsecret \
    --accept-terms-of-use \
    --mainnet \
    --checkpoint-sync-url=https://beaconstate.info --genesis-beacon-api-url=https://beaconstate.info
Restart=always
RestartSec=5
LimitNOFILE=8192

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl start prysm-beacon
sudo systemctl enable prysm-beacon
sudo systemctl status prysm-beacon

로그 확인

journalctl -u prysm-beacon -f

[ethers.js] HD wallet으로 주소 파생

12개의 seed wallet이 있는 경우, 다음과 같이 파생 주소를 확인할 수 있다.

$ npm install ethers@^5.0.0

const { ethers } = require('ethers');

// 12개의 시드 단어를 여기에 입력하세요
const mnemonic = "Your seed 12 words here";

// HD Node 생성
const hdNode = ethers.utils.HDNode.fromMnemonic(mnemonic);

// 파생 경로 설정
const derivationPath = "m/44'/60'/0'/0/"; // "m/44'/905956'/0'/0/" 와 같이 사용도 가능

// 파생 주소 생성 및 출력 함수
function generateAddresses(count) {
    const addresses = [];
    for (let i = 0; i < count; i++) {
        const walletNode = hdNode.derivePath(derivationPath + i);
        console.log(i, walletNode.address);
        addresses.push({
            index: i,
            address: walletNode.address,
        });
    }
    return addresses;
}

// 원하는 주소 개수를 설정하세요
const numAddresses = 85535;
const addresses = generateAddresses(numAddresses);

[golang] go-ethereum으로 이벤트 구독

	go func(client *ethclient.Client) {
		contractAddresses := []common.Address{}
		for _, pair := range Pair {
			contractAddresses = append(contractAddresses, common.HexToAddress(pair.PairAddress))
		}

		SyncEventSig := []byte("Sync(uint112,uint112)")
		hash := sha3.NewLegacyKeccak256()
		hash.Write(SyncEventSig)
		SyncEventHashBytes := hash.Sum(nil)
		signature := common.BytesToHash(SyncEventHashBytes) // 0x1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1
		topic := []common.Hash{signature}
		topics := [][]common.Hash{topic}
		query := ethereum.FilterQuery{
			Addresses: contractAddresses,
			Topics:    topics,
		}

		logs := make(chan types.Log)
		sub := event.Resubscribe(2*time.Second, func(ctx context.Context) (event.Subscription, error) {
			return client.SubscribeFilterLogs(context.Background(), query, logs)
		})
		defer sub.Unsubscribe()
		for {
			select {
			case err := <-sub.Err():
				log.Fatal("Error on select:", err)
			case vLog := <-logs:
				fmt.Println("Log block number:", vLog.BlockNumber)
				// 여기서 하고 싶은 일 하기.
			}
		}
	}(client)

[Ethereum] 트랜잭션 해시로부터 Public key 및 주소 얻기

Ethereum의 경우 버전에 따라 트랜잭션에 gasPrice를 쓸 지, maxFeePerGas/maxPriorityFeePerGas 를 쓸 지가 정해진다. https://ethereum.stackexchange.com/questions/147692/how-to-get-public-key-by-the-transaction-hash 를 참고.

주소를 얻을 때, Public key에서 앞의 한 바이트(0x04)를 빼고, keccak256을 돌리는 부분에 유의하자.

const { ethers } = require("hardhat");

// 트랜잭션 서명 값
// const r = "0xe680637b83a1dd102364503bd77979b87c92ba651132a4df8e839af69c20af95";
// const s = "0x65becfa32384747adb05f543397baaf23d1c55fa28d0c9a38924912dae6df28f";
// const v = 12266684226873;
// const chainId = 6133342113419;

// 트랜잭션 해시
const txHash = "0xc7159866fb5b94d291e8624bdc731b7a6d46533b398065de7aa7dd1af7b6aa36";
const ethermainHash = "0xef120deb6ff2e516ad44724d220c0cd73166d169a2a0b0f66dfb5613d2d6169c"
const sepoliaHash = "0xfe4ddad4cae9ea353fc91441ee5db6c70bd5468673d907926be92dd4b8a63f63"

// 실제 트랜잭션에 사용된 private key. 아무거나 넣자.
const privateKey = "0x0000000000000000000000000000000000000000000000000000000000000001";

// Web3 프로바이더 설정 (Hardhat 로컬 노드 사용 예시)
const provider = ethers.provider;

// 서명된 트랜잭션 정보 가져오기
async function getPublicKeyFromTransactionHash(provider, txHash) {
  // Fetch the transaction using the transaction hash and provier
  const tx = await provider.getTransaction(txHash);

  console.log(tx);

  // Extract the all the relevant fields from the transaction (We need all of them)
  const unsignedTx = {
    gasPrice: tx.gasPrice,
    gasLimit: tx.gasLimit,
    value: tx.value,
    nonce: tx.nonce,
    data: tx.data,
    chainId: tx.chainId,
    to: tx.to,
    type: tx.type,
    // maxFeePerGas: tx.maxFeePerGas,
    // maxPriorityFeePerGas: tx.maxPriorityFeePerGas,
  };

  // Serializing tx without the signature
  const serializedTx = ethers.utils.serializeTransaction(unsignedTx);

  // Extract the signature (v, r, s) from the transaction
  const { v, r, s } = tx;

  // Join splitted signature
  const signature = ethers.utils.joinSignature({ v, r, s });

  const recoveredPublicKey = ethers.utils.recoverPublicKey(
    ethers.utils.keccak256(serializedTx),
    signature,
  );
  console.log("recoveredPublicKey:", recoveredPublicKey);
  const keccak = ethers.utils.keccak256("0x" + recoveredPublicKey.slice(4))
  console.log("keccak256 of recoveredPublicKey:", keccak, ethers.utils.keccak256("0x" + recoveredPublicKey.slice(4)));

  // Recover the address or public key with (replace recoverAddress by recoverPublicKey) associated with the transaction
  return ethers.utils.recoverAddress(
    ethers.utils.keccak256(serializedTx),
    signature
  );
}

async function main() {
  const address = await getPublicKeyFromTransactionHash(provider, txHash);

  console.log("Address:", address);
}

main()
.then(() => process.exit(0))
.catch((error) => {
    console.error(error);
    process.exit(1);
});

[ethers.js] Contract를 특정 블록 넘버 기준으로 호출

import { ethers } from 'ethers';
import contract_ABI from './abi/contract_ABI.json';
const provider = new ethers.JsonRpcProvider("http://mainnet.dasomoli.org:8545");
const contract_address = "0x1234567890123456789012345678901234567890";
const contract = new ethers.Contract(contract_address, contract_ABI, provider);

const balanceOf = contract.balanceOf(account_address, { blockTag: 12345678 });

위처럼 호출 시 blockTag: 와 함께 blockNumber를 주면 된다.

[ethers.js] 컨트랙트 이벤트 로그 및 데이터 보기

ethers.js v5 기준

const contract = await ethers.getContractAt("DasomOLIContract", contract_address);
// DasomEvent(type arg1, ...)
const filter = contract.filters.DasomEvent();
const currentBlock = await ethers.provider.getBlockNumber();
const fromBlock = currentBlock - 86400;
const events = await ethers.provider.getLogs({
        ...filter,
        fromBlock: fromBlock,
        toBlock: currentBlock,
      });
if (events.length > 0) {
  events.forEach((event: Log) => {
      console.log(`Dasom event found in block ${event.blockNumber}:`);
      console.log(`Transaction Hash: ${event.transactionHash}`);
      console.log(`Transaction data: ${event.data}`);
      const parsedLog = contract.interface.parseLog(event);
      const args = parsedLog.args;
      const arg1 = args.arg1;
      console.log(`Log Data:`, parsedLog.args);
      console.log('--------------------------');
  });
}

[Solidity] Panic exception code

  1. 0x00: Used for generic compiler inserted panics.
  2. 0x01: If you call assert with an argument that evaluates to false.
  3. 0x11: If an arithmetic operation results in underflow or overflow outside of an unchecked { ... } block.
  4. 0x12; If you divide or modulo by zero (e.g. 5 / 0 or 23 % 0).
  5. 0x21: If you convert a value that is too big or negative into an enum type.
  6. 0x22: If you access a storage byte array that is incorrectly encoded.
  7. 0x31: If you call .pop() on an empty array.
  8. 0x32: If you access an array, bytesN or an array slice at an out-of-bounds or negative index (i.e. x[i] where i >= x.length or i < 0).
  9. 0x41: If you allocate too much memory or create an array that is too large.
  10. 0x51: If you call a zero-initialized variable of internal function type.

from https://docs.soliditylang.org/en/latest/control-structures.html#panic-via-assert-and-error-via-require

[Ethereum] 트랜잭션과 서명

트랜잭션

트랜잭션 T는 다음 data를 포함한다.

T = { nonce, gasPrice, gasLimit, to, value, data, v, r, s }
  • nonce: 보내는 EOA(External Owned Account) 의 counter.
    • 중간에 생략되면 생략된 nonce가 와서 유효하게 처리되기 전까지 이후의 nonce를 가지는 트랜잭션은 mempool에 저장되었다가 처리 후 유효하게 된다.
  • gasPrice: 발신자가 보내는 가스의 가격. 단위는 wei.
    • 0도 가능. 높을 수록 해당 트랜잭션이 빨리 처리됨.
  • gasLimit: 이 트랜잭션을 위해 구입할 가스의 최대량
    • 21000 gas. 이더 소비량 = 21000 * gasPrice
  • to: 수신 이더리움 address (20 bytes)
  • value: 수신처에 보낼 이더의 양
  • data: 가변 길이 data payload
    • to가 contract address라면, 4 bytes의 function selector와 그 이후의 function argument를 serialize한 data이다.
      • function selector = (keccak-256(function prototype))[0:4]
  • v, r, s: EOA의 ECDSA 디지털 서명의 구성 요소

서명

서명 시 사용되는 트랜잭션 T는 9개 필드로 다음과 같다. 이 중 맨 끝의 3개 { chainID, 0, 0 }은 EIP-155에 의해 추가된다. EIP-155는 Simple Replay Attack Protection으로 chainID를 포함하여 다른 네트워크 체인에서 해당 트랜잭션이 replay될 수 없도록 한다.

T = { nonce, gasPrice, gasLimit, to, value, data, chainID, 0, 0 }

Sig는 서명으로 서명 알고리즘, F sig() 로 (r, s) 두 값이 output으로 만들어진다. Transaction T와 private key k를 사용한다. RLP는 Recursive Length Prefix (RLP) encoding scheme 을 말한다.

Sig = F sig(keccak256(RLP(T)), k) = (r, s)

서명 시에는 임시 private key q, 그리고 q로부터 생성되는 임시 public key Q 를 사용한다.

q = rand() % 2**256
Q = q * K = (x, y)

여기서 r = Q의 x 좌표이다. s는 다음으로 계산된다.

s ≡ q**-1 (Keccak-256(RLP(T)) + r * k) mod p

서명 검증

r, s 그리고 sender의 public key K를 사용해서 Q를 계산한다. Q의 x 좌표와 r이 같으면 서명이 유효하다.

w = s**-1 mod p
u1 = Keccak-256(RLP(T)) * w mod p
u2 = r * w mod p
Q ≡ u1 * G + u2 * K     (mod p)

참고: https://github.com/ethereumbook/ethereumbook/blob/develop/06transactions.asciidoc

[Ethereum] 키와 주소

타원 곡선 함수, Elliptic curve cryptography 에서 다음 p를 사용

p = 2**256 - 2**32 - 2**9 - 2**8 - 2**7 - 2**6 - 2**4 - 1
y ** 2 mod p = (x ** 3 + 7) mod p

Private key -> Public key -> Address 로 얻는다.

개인키 (Private key)

k가 private key.

k = rand() % (2 ** 256), 따라서 256 bits, 64 bytes.

공개키 (Public key)

K가 public key로 (x, y) 좌표. G는 미리 정의된 값.

K = k * G = (x, y)

Public key, K를 표현할 때는 Standards for Efficient Cryptography (SEC1)Serialized EC public key prefixes 의 prefix를 사용해서 나타낸다. 이더리움은 uncompressed point만을 지원하므로 0x04 prefix로 사용한다. 따라서 0x04 뒤에 x와 y를 concatenate한다.

0x04 | x | y

주소 (Address)

keccak-256 Hash function을 사용. Public key (x, y)를 concatenate한 값(0x04가 붙지 않았다!)을 keccak-256 로 hash 값을 얻은 후 마지막 20 바이트 값을 사용한다.

(keccak-256(x | y))[-20:]