메타마스크, Web3.js 쓰니까 아주 편했었지? 이제 좀 맞자.
이더리움에서 트랜잭션의 종류는 총 3가지다. (롤업 트랜잭션 제외)
당신은 각각의 종류의 트랜잭션 데이터를 아는가?
뭔 개소리냐고?
{
"to": "0x1234567890abcdef1234567890abcdef12345678",
"from": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
"gas": "0x5208",
"gasPrice": "0x3B9ACA00",
"value": "0x0",
"nonce": "0x1",
"data": "0xa3f32d2c000000000000000000000000000000000000000000000000000000000000000a
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
0000000000000000000000000000000000000000000000000000000000000001
000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcd
0000000000000000000000000000000000000000000000000000000000000020
}
이 JSON 데이터를 적재적소에 맞게 직접 만들 수 있냐는 거다.
태어나서 처음본다고? 당연하다.
이더를 전송할 때 저런 데이터를 만들어서, 이더리움 노드에 HTTP-RPC 요청을 내가 왜해,
메타마스크에서 알아서 저런 데이터를 만들어서 특정 노드에 알아서 보낸다.
컨트랙트 생성이나 호출?
리믹스에서 딸깍으로도 가능하고
JS로 프론트나 백엔드에서 web3.js를 사용해서 휴먼답게 코드를 써서 보낼 수 있다.
이제 그런거 없다. 당신이 직접 풀노드의 HTTP-RPC 서버 URL에 POST 요청으로,
BODY에 JSON으로 transaction 데이터를 넣어줘야 한다.
요런식으로 말이다.
자, 저 데이터를 어떻게 넣어야 할까?
from, to : 보내고 받는 주소
value : 전송 eth
nonce : from의 nonce
signature : from의 개인키로 암호화한 값
여긴 ㅈㄴ 쉽다. 사실 여긴 중요한 게 아냐.
이 데이터 필드가 컨트랙트 함수 호출을 위한 필드다.
즉, to에는 컨트랙트 주소를 적고,
"이 함수에 이런 이런 파라미터를 넣어서 실행해주세요~"
즉, 함수명 + 파라미터 값
을 넣어야 할 것이다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract TestContract {
function testFunction(
uint256 number,
int256 signedNumber,
bool flag,
address user,
string memory text,
bytes memory data,
uint256[] memory numbers
) public pure returns (bool) {
return true;
}
}
위와 같은 함수가 있다. 저 함수에, 여러 파라미터를 넣어서 data를 어떻게 만들어야 하나?
data 필드는 4가지 필드로 순서대로 나눈다.
함수를 특정하기 위한 식별자다.
위의 예시를 보면, 내가 부르고 싶은 함수 선언문은
testFunction(uint256, int256, bool, address, string, bytes, uint256[])
다.
이걸 통으로 Keccak256에 넣고, 앞 4바이트를 가져온다.
그래서 0xa3f32d2c다.
위 함수는 인자가 7개다.
uint256 number, // 정적 데이터
int256 signedNumber, // 정적 데이터
bool flag, // 정적 데이터 (1바이트지만 32바이트 사용)
address user, // 정적 데이터 (20바이트지만 32바이트 패딩)
string memory text, // 동적 데이터
bytes memory data, // 동적 데이터
uint256[] memory numbers // 동적 데이터
정적 데이터는 각각, 32바이트 크기로 정렬이 된다.
저장이랑 헷갈리면 안된다. bool은 1비트지만 표현은 32바이트로 해야 된다.
또한 동적 데이터는,
string인 text를 보자. 만약 "hello"를 넣고 싶다면, data로 변환하려면
포인터 : 잠시 기다려봐
문자열 길이 : 00000....5
hello 값 : 48656c6c6f00000....
다음 bytes인 data에 "hey"가 들어간다면
포인터 : 잠시 기다려봐
길이 : 000000....3
hey 값 : 6865792100000.....
마지막 uint256[]에 3개가 들어간다면
포인터 : 잠시 기다려봐
길이 : 3
값 : 배열 요소 1, 2,3을 각각 32바이트로 처리.
즉 포인터 떼고 3개의 변수가
string : 2줄
bytes : 2줄
uint256[] : 4줄
이걸 구하고 pointer의 위치를 각각 구할 수 있다.
종합하면 data는
0xa3f32d2c // Function Selector
// 정적 데이터
000000000000000000000000000000000000000000000000000000000000000a // uint256 number = 10
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff // int256 signedNumber = -1
0000000000000000000000000000000000000000000000000000000000000001 // bool flag = true
000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcd // address user
// 포인터 (Offset)
00000000000000000000000000000000000000000000000000000000000000e0 // text (string) 위치
0000000000000000000000000000000000000000000000000000000000000120 // data (bytes) 위치
0000000000000000000000000000000000000000000000000000000000000160 // numbers (uint256[]) 위치
// 동적 데이터 (text)
0000000000000000000000000000000000000000000000000000000000000005 // 문자열 길이(5)
48656c6c6f000000000000000000000000000000000000000000000000000000 // "Hello"
// 동적 데이터 (data)
0000000000000000000000000000000000000000000000000000000000000004 // 바이트 길이(4)
6865792100000000000000000000000000000000000000000000000000000000 // "hey!"
// 동적 데이터 (numbers)
0000000000000000000000000000000000000000000000000000000000000003 // 배열 길이(3)
0000000000000000000000000000000000000000000000000000000000000001 // 배열 요소 1 (1)
0000000000000000000000000000000000000000000000000000000000000002 // 배열 요소 2 (2)
0000000000000000000000000000000000000000000000000000000000000003 // 배열 요소 3 (3)
이렇게 만들어지는 것이다.
함수를 실행했을 때, 실행되는 OPCODE의 가스비를 모조리 계산해서 좀 더 여유있게 넣어야 한다.
위 예시 함수는 pure 함수로, 스토리지에 저장도, 조회도 하지 않는다.
그렇다고 가스비가 발생 안하냐? 연산이 없냐? 그건 아니다.
약 30000 이상의 가스가 든다.
Remix, Web3.js는 estimateGas()를 사용해 노드에게 연산 시뮬레이션을 돌려
OPCODE에 따른 가스비를 계산할 수 있다.
혹은 어차피 돌려 받으니까 넣을 수도 있는 것이다.
이제 우리는 라이브러리, 지갑 도움 없이 직접 트랜잭션 JSON 데이터를 만들어서 보낼 수 있다.
이제 JS 라이브러리를 만들어 볼까?