이번 포스팅에서는 이더리움 네트워크 상에 스마트 컨트랙트(Smart Contract)를 배포하고 배포된 스마트 컨트랙트를 실행시키는 방법에 대해 다뤄보고자 한다.
< 목차 >
- 스마트 컨트랙트 컴파일
- 스마트 컨트랙트 배포
- 스마트 컨트랙트 실행
1. 스마트 컨트랙트 컴파일
스마트 컨트랙트를 배포하기 위해서는 우선 배포하고자 하는 소스코드가 존재하여야 한다. 여기서는 Solidity를 사용해 아래와 같은 스마트 컨트랙트를 작성하였다. Solidity와 관련된 문법 및 스마트 컨트랙트 작성과 관련된 내용은 추후 다루도록 하고 지금은 스마트 컨트랙트의 배포와 실행에 초점을 맞춰보고자 한다.
/* solidity로 작성된 스마트 컨트랙트 */
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15; // 버전
// HelloWorld 라는 컨트랙트 생성
contract HelloWorld {
// 솔리디티는 세미콜론; 필수
string text; // 상태변수
constructor() {
text = 'Hello World!';
}
// 함수를 작성할 때는 function 키워드 필수
// 디폴트는 public (외부에서 접근 가능)
// view 함수 : 상태변수를 그대로 출력하는 함수
function getText() public view returns(string memory) {
return text;
}
// 인스턴스에 있는 상태변수를 바꾸는 함수
function setText(string memory value) public {
text = value;
}
}
👉 참고
Solidity(솔리디티)는 기본적으로 객체 지향적인 언어이다. 다시말해, 하나의 컨트랙트는 곧 하나의 객체라고 생각하면 된다. 이해를 돕기 위해 Solidity로 작성된 위의 코드를 TypeScript로 작성해보면 다음과 같다.
class HelloWorld {
public text: string;
constructor() {
this.text = 'Hello World!';
}
getText(): string {
return this.text;
}
setText(value: string): void {
this.text = value;
}
}
const obj = new HelloWorld();
console.log(obj.getText()); // Hello World!
다시 본론으로 돌아와서 Solidity로 작성된 스마트 컨트랙트를 이더리움 네트워크 상에 배포하기 위해서는 다음의 과정들을 거쳐야 한다.
- 컴파일러(Compiler)를 통해 작성된 코드를 바이트 코드(byte code)로 변환
- 바이트 코드를 데이터로 하는 트랜잭션 객체 생성
- 트랜잭션을 발생시켜 이더리움 네트워크에 스마트 컨트랙트 배포
위와 같이 스마트 컨트랙트 작성이 완료되었다면 Solidity로 작성된 스마트 컨트랙트 즉, Solidity 코드를 컴파일 해주는 과정을 진행해줘야 한다. Node.js 환경에서 Solidity로 작성된 코드를 컴파일하기 위해 solc 라이브러리를 설치해주었다.
$ npm install solc
solc를 설치 완료하였다면, 아래의 명령어를 통해 컴파일을 진행해주면 된다.
$ npx solc --bin --abi [디렉토리/파일명]
컴파일이 완료되었다면 bin 파일과 abi 파일이 생성된 것을 확인할 수 있을 것이다. 각각의 파일이 어떠한 역할을 하는 파일인지 간략하게 짚고 넘어가도록 하자.
bin 파일은 기본적으로 실행파일을 의미한다. 실제 bin 파일의 내용을 살펴보면 byte code가 작성되어 있는 것을 확인할 수 있다. 즉, 컴파일러가 Solidity로 작성된 스마트 컨트랙트 내용을 바이트 코드(byte code)로 변환하여 bin 이라는 실행파일을 만들어 준 것이다.
abi 파일은 Application Binary Interface의 약자로 스마트 컨트랙트 안에 존재하는 함수와 매개변수들을 JSON 형식으로 나타낸 리스트이다. 우리는 abi를 사용해 컨트랙트 내의 함수를 호출하거나 컨트랙트로부터 데이터를 얻을 수 있다. 실제 스마트 컨트랙트는 바이트 코드로 변환된 후 트랙잭션의 데이터 안에 담겨 이더리움 네트워크 상에 올라간다. 즉, 트랜잭션 데이터 안에 내용을 넣을 때 소스코드를 그대로 넣는 것이 아니라 컴파일된 바이트 코드 형태로 넣어 이더리움 네트워크 상에 올리기 때문에 데이터를 어떻게 전달해야 하는지에 대한 이슈가 발생한다. abi 파일은 사용자가 이더리움 네트워크에 요청한 데이터를 보내줄 때 어떠한 객체 형태로 보내줄 것인지에 대한 결정 파일이다.
ABI 는 낮은 수준의 API 혹은 API의 컴파일된 버전이라고 생각할 수도 있다. 스마트 컨트랙트는 특정 주소의 블록체인에 바이너리 형식의 바이트 코드로 저장된다. 따라서 호출자(컨트랙트 실행자)는 바이트 코드에 대한 의미 있는 호출을 인코딩하는데 필요한 정보를 나타내는 ABI를 통해 컨트랙트의 데이터에 접근할 수 있게 된다.
정리하면, 스마트 컨트랙트의 배포란 이더리움 네트워크 상에 바이트 코드를 올리는 행위를 일컫는다. 이 때 트랜잭션 객체 안의 data 속성값으로 컴파일된 바이트 코드(bin 파일의 내용)를 넣어 이더리움 네트워크 상에 배포한다. 그리고 abi 파일 안에 있는 내용을 이용해 이더리움 네트워크 상에 올라가 있는 컴파일된 스마트 컨트랙트 코드를 호출할 수 있게 된다.
2. 스마트 컨트랙트 배포
이제 트랜잭션 객체를 만들어서 컴파일된 스마트 컨트랙트를 실제 이더리움 네트워크 상에 배포해보도록 하자.
배포를 하기에 앞서 계정을 unlock 하는 과정을 선행적으로 진행해주고자 한다. 앞서 이더리움 private 네트워크 내에서 트랜잭션을 발생시킬 때 다음과 같이 Geth JS console 창에 다음과 같이 작성했던 것을 상기시켜보자.
$ personal.sendTransaction({from: eth.coinbase, to: '0xeec18f258b11011d462c4fef0e32271695048552', value: web3.toWei(10, 'ether')}, '1234')
personal.sendTransaction( ) 메소드의 두번째 인자값으로 들어간 "1234"는 계정을 생성할 때 입력했던 passphrase이다. 일종의 계정 비밀번호라고 할 수 있는데 트랜잭션을 발생시킬 계정의 개인키를 이용해 서명을 생성하기 위해 입력하는 비밀번호라고 보면 된다.
private 네트워크를 실행시킨 노드에서 personal.newAccount() 메소드를 사용해 계정을 생성하면 다음과 같이 keystore/ 디렉토리 안에 생성된 계정 정보가 담긴 파일이 만들어진다.
해당 파일은 암호화 되어 있는 계정 정보 파일이라고 볼 수 있다. 하지만 단방향 암호화가 아니기 때문에 복호화가 가능하며 복호화를 진행했을 때 개인키를 얻을 수 있게 된다. 이 때 복호화를 진행하기 위해 필요한 값이 바로 계정을 생성할 때 입력했던 passphrase 값이다. 트랜잭션을 발생시키기 위해서는 서명이 필요하고 서명을 작성하는 행위는 개인키를 통해서만 진행할 수 있다. 이에 계정 정보 파일 안에는 개인키 내용이 포함되어 있어야 하는데 개인키를 그대로 저장할 경우 보안상 이슈가 발생할 수 있기 때문에 암호화해서 개인키를 저장한 것이라고 생각하면 된다.
하지만 위와 같이 트랜잭션을 발생시킬 때마다 passphrase 값을 인자값으로 넣어주는 것은 어찌보면 번거로운 일이다. 따라서 geth를 실행시킬 때 특정 계정을 unlock 함으로써 passphrase를 인자값으로 전달하지 않고서도 해당 계정으로 트랜잭션을 발생시킬 수 있게끔 할 수 있다.
특정 계정을 unlock 한 상태로 geth를 실행하기 위해서는 password 파일을 만들어서 해당 파일 안에 passphrase 값을 작성한 다음 geth를 실행할때 --allow-insecure-unlock --unlock --password 옵션값을 추가로 작성해 실행시켜주면 된다.
$ geth --datadir node --http --http.addr "0.0.0.0" --http.port 9000 --http.corsdomain "*" \
--http.api "admin,eth,debug,miner,net,txpool,personal,web3" --syncmode full --networkid 2371 \
--port 30300 --ws --ws.addr "0.0.0.0" --ws.port 9005 --ws.origins "*" \
--ws.api "admin,eth,debug,miner,net,txpool,personal,web3" \
--allow-insecure-unlock --unlock "0,1" --password "./node/password"
- --allow-insecure-unlock --unlock "0, 1" --password "./node/password"
- --unlock 옵션 값은 eth.accounts 기준 인덱스 값
- --password 옵션 값은 passphrase가 작성된 파일의 경로
geth를 실행했다면 Geth JS console 창을 열어 다음과 같이 변수 설정을 해주도록 하자. 이때 .bin 파일 안에 있는 바이트 코드를 변수에 할당할 때 "0x..." 와 같이 0x 를 붙여서 지정해줘야 함에 주의하도록 하자.
$ geth attach http://127.0.0.1:9000
// .bin 파일 안에 있는 바이트 코드를 bytecode 변수에 할당
bytecode="0x..."
// .abi 파일 안에 있는 값을 abi 변수에 할당
abi=[{...}]
이제 트랜잭션을 발생시키기 위한 txObject 객체를 다음과 같이 만들어주면 된다.
txObject={from: eth.coinbase, data: bytecode}
스마트 컨트랙트를 배포하기 위한 트랜잭션이므로 속성값으로는 트랜잭션을 발생시킬 계정이 들어갈 from 속성과 스마트 컨트랙트 내용이 담긴 data 속성만을 넣어 트랜잭션 객체를 생성해도 무방하다. 이제 eth.sendTransaction( ) 메소드의 인자값으로 txObject를 전달해 트랜잭션을 발생시켜주기만 하면 된다. 이 때 coinbase 계정을 unlock 한 상태이기 때문에 passphrase를 인자값으로 넣어주지 않아도 된다.
$ eth.sendTransaction(txObject)
eth.sendTransaction( ) 메소드의 return 값으로 나오는 Transaction Hash 값을 eth.getTransaction( ) 메소드의 인자값으로 전달해 txpool에 들어간 트랜잭션 내용을 조회하면 다음과 같이 나오는 것을 확인할 수 있다.
트랜잭션 객체의 input 속성값으로 스마트 컨트랙트를 컴파일 한 바이트 코드(.bin 파일 내용)가 들어가 있는 것을 알 수 있다. 마지막으로 miner.start( ) 메소드를 통해 마이닝을 진행하면 이더리움 네트워크 상에 스마트 컨트랙트 배포가 완료된다.
3. 스마트 컨트랙트 실행
앞선 과정들을 통해 이더리움 네트워크 상에 스마트 컨트랙트 배포를 완료하였다. 이제 배포된 스마트 컨트랙트를 실행시키기 위해 CA (Contract Address) 값을 조회해보도록 하자.
이더리움 네트워크 상에는 EOA (Externally Owned Account) 와 CA (Contract Address) 라는 두 종류의 계정이 존재한다. 각 계정이 하는 역할에 대해서는 이전 포스팅에서 간략히 다룬적이 있는데 아래 글을 참고하길 바란다.
2022.06.28 - [Ethereum] - Ethereum/이더리움 - 비트코인 vs 이더리움
CA에 대해 조금 더 설명을 보태면, CA 안에는 기본적으로 스마트 컨트랙트 관한 내용이 담겨있다. 정확히는 CA에 존재하는 코드 해시 값을 통해 스마트 컨트랙트 코드에 접근할 수 있게 된다. CA 는 스마트 컨트랙트가 이더리움 네트워크 상에 배포되었을 때 생성되며 eth.getTransactionReceipt( ) 메소드를 사용해 CA를 조회하는 것이 가능하다. 이 때 eth.getTransactionReceipt( ) 메소드의 인자값으로는 Transaction Hash 값을 전달해주면 된다.
$ eth.getTransactionReceipt(transactionHash)
위와 같이 contractAddress 속성값으로 CA 정보가 나오는 것을 확인할 수 있는데 이더리움 네트워크 상에 배포된 스마트 컨트랙트를 실행시키기 위해서는 이 CA 를 이용해야만 한다. 쉽게 말해, CA는 스마트 컨트랙트가 배포되었을 때 생성되는 계정으로 스마트 컨트랙트의 고유한 키 값이라고 생각하면 된다. 스마트 컨트랙트 안에 작성된 함수를 호출하거나 값을 가져올 때 CA를 사용하게 되므로 스마트 컨트랙트에 접근하기 위해서는 CA를 알아야만 한다.
우선 Geth JS console 창에서 다음과 같이 contract 변수를 할당해주자.
$ contract=eth.contract(abi)
eth.contract( ) 메소드의 인자값으로 abi 파일 안에 있는 JSON 형식의 내용을 전달해준 다음 반환 값을 contract 변수에 할당하였다. contract 변수에 담겨있는 내용을 조회하면 abi 파일의 내용이 추가된 것을 확인할 수 있다.
이더리움 네트워크 상에 배포된 스마트 컨트랙트는 바이트 코드로 되어 있기 때문에 실제 스마트 컨트랙트를 실행시킬 때는 abi 파일의 내용을 이용해 인코딩을 진행해줘야만 한다. 이에 abi 파일의 내용을 포함하고 있는 contract 객체를 생성해 준 것이며 contract.at( ) 메소드를 통해 스마트 컨트랙트 코드에 접근하는 것이 가능해진다.
contract.at( ) 메소드의 인자값으로는 CA 를 전달해주며 다음과 같이 반환 값을 instance 변수에 할당하였다.
$ instance=contract.at(CA)
instance 객체 안에는 abi 파일의 내용과 함께 스마트 컨트랙트 안에서 작성된 함수들이 들어있는 것을 확인할 수 있다. 블록이 마이닝 될 때 (스마트 컨트랙트가 배포될 때) EVM은 트랜잭션 객체의 data 속성값으로 들어간 바이트 코드를 실행시키고 해당 컨트랙트 코드를 토대로 인스턴스를 생성하게 된다. 그리고 CA 를 이용해 생성된 인스턴스를 불러오는 것이라고 생각하면 된다. 다시한번 언급하지만 abi 파일이 있어야만 바이트 코드로 작성된 내용을 알 수 있다는 것에 명심하자.
instance 객체 안에 있는 getText 함수를 실행시키기 위해서는 다음과 같은 메소드를 활용하면 된다.
$ instance.getText.call()
call( ) 메소드를 사용하게 될 경우 스마트 컨트랙트 안에서 정의된 getText 함수의 return 값인 text 변수의 값을 가져올 수 있다. 이를 "상태변수"라고 부르는데 스마트 컨트랙트 안에서 정의된 상태변수 값(여기서는 text 변수의 값)을 가져오기 위해 call( ) 메소드를 사용한다고 보면 된다.
간혹 스마트 컨트랙트 안에 작성된 함수를 이용해 상태변수의 값을 변경하는 경우가 존재한다. 스마트 컨트랙트에서 상태변수의 값을 가져오는 것과 변경하는 것에는 큰 차이점이 있다. 상태변수의 값을 바꾼다는 것은 상태변수의 저장 공간이 달라진다는 것을 의미한다. 블록체인 네트워크 상에서는 저장 용량이 한계적이기 때문에 저장 용량을 바꾸기 위해서는 추가적인 비용을 지불해야 하는 것이 불가피하다. 즉, EVM을 돌리기 위한 비용을 지불하는 셈이다. 따라서 상태변수의 값을 변경하는 행위는 트랜잭션을 발생시켜 수수료를 지불함으로써 처리하게 된다.
이렇게 가지고 있는 데이터를 불러오는 행위를 call 이라고 부르며 값을 전달해서 상태변수를 바꾸는 행위를 send 라고 부른다. call 에서는 비용을 지불할 필요가 없으며 send 에서는 트랜잭션을 발생시켜 수수료를 지불해야만 한다. 우리가 작성한 스마트 컨트랙트 안의 setText 함수는 인자값을 받아서 text 상태변수의 값을 바꿔주는 함수이다. 실제 트랜잭션을 발생시켜 setText 함수를 실행시켜 보도록 하겠다.
이번에는 eth.sendTransaction( txObject ) 메소드가 아닌 contract.new( txObject ) 메소드를 사용해 스마트 컨트랙트를 배포해 보았다. miner.start( ) 메소드를 통해 txpool 에 담긴 트랜잭션 객체를 블록에 담아 이더리움 네트워크 상에 배포한 다음 instance 객체를 사용해 setText 함수를 다음과 같이 실행하였다.
$ txObject={from: eth.coinbase, data: bytecode}
$ contract=eth.contract(abi)
$ # 트랜잭션 배포
$ instance=contract.new(txObject)
$ # 블록 마이닝
$ miner.start(4)
$ miner.stop()
$ # 상태 업데이트에서는 계정 정보 필요
$ instance.setText('Hello smart contract', {from: eth.coinbase})
setText( ) 메소드의 두번째 인자값으로 { from: eth.coinbase } 객체를 전달하였는데 상태변수의 값을 바꾸는 함수를 실행하기 위해서는 수수료를 지불해야 하기 때문에 트랜잭션을 발생시킬 계정의 정보를 인자값으로 같이 전달해줘야만 한다.
트랜잭션이 발생하였으므로 다시 마이닝을 진행한 다음 instance.getText.call( ) 메소드를 통해 상태변수의 값을 가져오면 다음과 같이 상태변수의 값이 바뀐 것을 확인할 수 있다.
'Ethereum' 카테고리의 다른 글
Ethereum/이더리움 - Truffle (스마트 컨트랙트 개발 프레임워크) (0) | 2022.07.13 |
---|---|
Ethereum/이더리움 - JavaScript로 스마트 컨트랙트 배포 및 실행 (0) | 2022.07.12 |
Ethereum/이더리움 - private 네트워크 RPC 설정하기 (1) | 2022.07.01 |
Ethereum/이더리움 - private 네트워크 (1) | 2022.06.30 |
Ethereum/이더리움 - 메타마스크 연결하기 (5) | 2022.06.29 |