336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

현재 비트코인과 이더리움의 풀 노드를 운영하는데 필요한 디스크 사이즈가 비트코인이 150GB, 400GB 정도로 추정됩니다. 이더리움이 비트코인보다 늦게 시작되었다는 점을 감안하면 이더리움의 디스크 사용량이 빠르게 증가했음을 추측할 수 있습니다. 개인용 랩탑이나 데스크탑의 스토리지 구성이 일반적으로 1TB 이하인 것을 생각해보면 개인이 풀 노드를 운영할 때 부담스럽게 느껴질 수 있는 부분입니다.

그러나 저 수치는 블록체인의 모든 블록과 상태를 저장했을 때 얘기이고, 더 이상 참조할 가능성이 없는 불필요한 정보를 삭제하면 비트코인이 약 5GB, 이더리움이 30GB 수준인 것으로 보입니다. 용량 수치 내용은 아래 2개 링크를 참고했습니다.

https://bitcoin.org/en/full-node#minimum-requirements
https://dev.to/5chdn/the-ethereum-blockchain-size-will-not-exceed-1tb-anytime-soon-58a

비트코인을 예로 들면 새로운 블록을 실행/검증하는데 중요한 것은 블록 내 트랜잭션 Input이 가리키는 Output이 사용 가능한지 여부입니다. 이를 효과적으로 처리하기 위해 비트코인 클라이언트는 UTXO(Unspent Transaction Outputs) set을 관리하며, 신뢰할 수 있는 UTXO set이 있다면 굳이 모든 트랜잭션 히스토리를 저장하고 있지 않더라도 마이닝이 가능합니다. 다만 비교적 최근에 실행된 트랜잭션은 저장하고 있어야 합니다. 블록이 교체될 경우 빠진 블록의 트랜잭션을 되돌리는 과정이 필요하기 때문입니다.

이더리움의 경우 State를 Merkle-Patricia Trie를 통해 상태를 관리하고 루트의 hash를 블록 헤더에 저장합니다. 블록 헤더에 저장되는 내용은 PoW로 보호되기 때문에 특정 State를 다른 노드로부터 통째로 받았을 때 검증해볼 수 있는 항목이 존재하는 장점이 있습니다.

이더리움도 마찬가지로 블록이 교체될 경우를 대비해야 하는데, 비트코인이 트랜잭션을 되돌리는 것과 달리 이더리움은 예전 State 자체를 저장합니다. Trie는 해시포인터 구조로 저장되어 변경되는 경로에 있는 일부 노드만 추가로 저장되기 때문에 효과적인 방법입니다.

리플의 경우 BFT 계열 합의 알고리즘으로 포크가 일어나지 않음을 가정하고 있습니다. 이는 트랜잭션 히스토리를 저장할 필요없이 마지막 State와 해당 State에 대한 합의 내용(서명)만 저장하고 전파하면 됩니다. 리플에서 마지막으로 합의된 상태를 LCL(Last Closed Ledger)라고 부릅니다.

이더리움에서도 PoS 도입을 통해 Block Finality를 보장하면 삭제할 수 있는 정보가 더 늘어나게 됩니다. 리플과 마찬가지로 마지막으로 PoS로 합의한 상태와 합의 내용과 그 검증만 있으면 그 이전의 트랜잭션 및 State는 저장하지 않아도 됩니다. 다만 PoS 합의 내용 자체를 검증하기 위해선 그 전체 히스토리가 필요할 수 있습니다.

'이더리움 Technical Background' 카테고리의 다른 글

트랜잭션의 마이닝 우선순위  (0) 2018.02.18
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

Solidity 개발은 기타 새로운 프로그래밍 언어나 프레임워크를 배우는 것보다 더 까다롭습니다. 국문 자료가 부족한 점도 있고, Solidity 문법을 이해하여 작성하는 것까지는 가능해도 이를 실행시켜보려면 이더리움 클라이언트를 실행하고 이 클라이언트에 JSON-RPC 콜을 보내는 방법 역시 필요합니다. 이 전체적인 과정이 깔끔하게 진행되면 좋겠지만, 이더리움이 코어/프로토콜 개발에만 몰두되어 있고 스마트 컨트랙 개발자 생태계는 상대적으로 투자가 덜 되어서 관련 툴의 완성도가 떨어지는 편입니다. 종종 사소한 실수조차 에러 메시지가 너무 일반적이어서 초심자들은 에러의 원인을 알기 어려운 경우가 있습니다.

이 글은 스마트 컨트랙의 배포와 실행 과정은 최대한 무시한 채 일단 Solidity 문법을 공부해서 작성해보고 실행해보기를 원하는 분들을 위해 작성했습니다. Truffle Framework 4 버전을 기준으로 작성되었습니다. 또한 기본적인 터미널 사용 환경과 Node.js에 익숙하다는 전제 하에 작성되었습니다.

개발 환경을 구축하기 위해 사용할 Truffle Framework는 Node.js 5 버전 이상을 요구합니다. 가이드 작성에는 8 버전을 사용했고 예제에 사용된 문법 역시 다를 수 있으니 참고하시기 바랍니다.

truffle 설치 및 프로젝트 환경 설정

npm 명령어로 truffle을 우선 설치합니다.
`npm install -g truffle`





-g 옵션으로 설치했으면 truffle 명령어를 실행 가능한 상태가 됩니다. `truffle unbox` 명령어로 예제 코드가 포함된 프로젝트를 생성합니다. 추후에 예제 코드를 제외한 상태로 프로젝트를 생성하려면 `truffle init` 명령어를 수행하면 됩니다.


프로젝트 생성이 완료되면 디렉토리와 파일들이 생성되고 친절하게 자주 사용하게 될 명령어 3개를 보여줍니다. compile, migrate, test 입니다.

contracts/ 디렉토리는 Solidity 컨트랙 코드가 있는 디렉토리입니다. `truffle compile` 명령어를 수행하면 Solidity 컴파일을 수행해서 관련 내용을 build/ 디렉토리에 저장합니다.



`truffle compile`을 수행하면 build 디렉토리가 생성되어 그 밑에 컨트랙 별로 json 파일이 생성됩니다. 이 파일을 열어보면 ABI, 바이트코드, 컴파일러, 네트워크 정보가 저장되는 것을 볼 수 있습니다. 막 컴파일한 상태에서는 networks 필드가 빈 오브젝트로 나오는데, 이는 migration 시에 네트워크에 배포하게 되면 네트워크에 배포한 정보를 저장합니다.

`truffle migrate`를 수행하려면 이더리움 클라이언트를 실행시키고 관련 설정을 먼저 해주어야 합니다.

테스트용 이더리움 노드 설정

geth나 parity 등 실제 이더리움 클라이언트를 설치하여도 무방하지만, 개발 학습용으로 적합하지 않습니다. 이 가이드에선 이더리움 노드의 동작을 시뮬레이션하는 ganache-cli(구 TestRPC)를 사용하는 방법 두 가지를 소개합니다.

`truffle develop`

truffle에 내장되어 있는 ganache-cli 실행 방법입니다. 포트 기본값이 9545 입니다.





`npm install -g ganache-cli`
`ganache-cli`


ganache-cli를 직접 설치해서 실행하는 방법입니다. 포트 기본값이 8545 이고 ganache-cli의 특정 버전을 쓰고 싶거나 특정 옵션이 필요할 때 쓰기에 적합한 방법인 것 같습니다.




실행 시 처음 보여주는 계정 10개는 100 ETH를 가지고 있습니다. Ctrl+C 등으로 프로세스가 종료되면 데이터는 모두 날아갑니다. truffle로 테스트하는 입장에서는 deploy 부터 다시 해주어야 하니 만약 데이터 저장이 필요하다면 ganache-cli의 --db 옵션을 써서 저장하여야 합니다.

이더리움 노드 설정이 완료되었다면 truffle 프로젝트 디렉토리에 있는 truffle.js를 수정해서 host와 port를 맞게 설정하면 됩니다.

truffle migrate

`truffle migrate` 명령어를 수행하면 migrations 디렉토리에 있는 스크립트를 차례대로 수행합니다. 스크립트 파일 이름에 붙어 있는 숫자 prefix는 truffle에서 인식하는 숫자입니다. migrate 명령 수행시 이전에 수행한 스크립트가 있으면 그 숫자 이후의 마이그레이션만 수행합니다. 만약 컨트랙 코드를 바꾸어 다시 배포해야 한다면 `truffle migrate --reset`과 같이 --reset 플래그를 주어 첫번째 스크립트부터 다시 실행하도록 할 수 있습니다.




참고로 `truffle migrate —network development`와 같은 방식으로 network를 지정할 수 있습니다.

잠시 migrations/2_deploy_contracts.js 를 살펴보겠습니다.



처음 부분의 artifacts.require(“…”) 구문은 해당 컨트랙의 artifacts를 불러오는 구문입니다. artifacts 객체는 truffle이 이 스크립트를 실행할 때 제공하는 객체이고, 컨트랙을 배포할 때 바이트코드를 얻어오거나 배포된 컨트랙과 상호작용할 때 사용됩니다. build/ 디렉토리 밑 json 파일에 컨트랙의 배포된 주소, ABI 등을 저장하기 때문에 가능합니다. 따라서 배포될 때 트랜잭션 결과로 나온 컨트랙 주소를 개발자가 직접 기록해둘 필요가 없습니다.

export 하는 함수는 ConvertLib과 MetaCoin을 배포합니다. MetaCoin이 ConvertLib을 라이브러리로 사용하기 때문에 중간에 deployer.link를 부르는 부분이 존재합니다.

truffle test


`truffle test` 명령어로 test 디렉토리 아래에 있는 테스트를 수행할 수 있습니다.




test는 Solidity와 Node.js 둘 다 작성할 수 있습니다. 이는 상황에 맞게 사용하면 됩니다. 테스트하려는 데이터의 타입이 자바스크립트보다는 Solidity가 더 적합한 경우도 있고, 케이스가 복잡하여 Solidity로는  작성하기 어려운 경우도 있습니다.

`truffle test` 명령어로 수행 가능합니다. `migrate`와 마찬가지로 `—network` 옵션으로 어떤 네트워크에서 테스트할 것인지 지정할 수 있습니다. 참고로 이더리움의 snapshot 기능을 사용하기 때문에 테스트 환경에서 발생한 트랜잭션이 누적되어 저장되지는 않습니다.

truffle exec

truffle 환경의 스크립트는 컨트랙 정보를 미리 저장해두기 때문에 때때로 유용합니다. 그러나 migrations/test에 들어가는 스크립트는 그 용도가 정해져 있습니다. 이 용도가 아닌 스크립트를 실행하기 위해서 truffle exec 명령어가 있습니다. 예를 들어 실배포 환경에서 컨트랙에 특정 함수를 호출하고 싶다면 아래와 같이 스크립트를 작성해서 `truffle exec <script_filename> --network <network>` 명령어를 실행하면 됩니다.

const MetaCoin = artifacts.require("MetaCoin");
 
module.exports = async function(deployer) {
        const instance = await MetaCoin.deployed();
        const { accounts } = web3.eth;

        const receipt = await instance.sendCoin(accounts[1], web3.toBigNumber("10"), { from: accounts[0] });
        console.log(receipt);
}


이처럼 Truffle을 사용하면 web3에 ABI와 컨트랙 주소를 넘겨줘서 컨트랙 객체를 얻어오는 과정을 간소화할 수 있습니다. 컨트랙 함수를 호출할 때 함수가 view 함수이면 receipt 대신 함수가 리턴하는 객체를 반환합니다. 컨트랙의 상태를 확인할 때에도 truffle 환경 스크립트를 사용하면 유용할 수 있습니다.


참고자료

Truffle: http://truffleframework.com/

web3.js 0.20.x API: https://github.com/ethereum/wiki/wiki/JavaScript-API

web3.js 1.0.x API: https://web3js.readthedocs.io/en/1.0/

Solidity: http://solidity.readthedocs.io/en/v0.4.21/index.html

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

최근 이더리움 클라이언트 중 하나인 Parity 구현을 보면서 알게 된 트랜잭션 우선순위와 관련한 내용을 공유하는 글입니다.

코드를 직접 읽어보고자 한다면 링크의 위치부터 시작하셔도 좋을 것 같습니다. Parity는 러스트로 작성되어 있습니다.


이더리움 트랜잭션

이더리움 트랜잭션을 보낼 때에는 5가지를 결정해야 합니다.


- to

- value

- data

- gasprice

- startgas (Gas Limit)


이더리움 트랜잭션에 대해 잘 모르는 분들을 위해 설명하자면, to는 보낼 주소, value는 보낼 이더의 양입니다. data는 contract를 생성하거나 특정 함수를 호출할 때 사용하고, 일반적으로 유저가 직접 입력하진 않습니다. 트랜잭션이 블록에 포함될 때 가스 사용량만큼 마이너에게 수수료를 줘야 하는데, 이와 관련된 필드가 gaspricestartgas입니다. gasprice는 1 gas 당 지불할 이더입니다. 일반적으로 1~20 Gwei 수준입니다. 참고로 1 Gwei = 0.000000001 ETH 입니다. startgas는 해당 트랜잭션 내에서 허용 할 최대 가스 사용량입니다. 이름이 좀 불만스러운데, 지갑 앱 등에서는 Gas Limit 혹은 Gas 라는 이름이 더 많이 보입니다. 좀 더 직관적인 설명을 위해 Gas Limit이란 표현을 쓰겠습니다.


트랜잭션의 가스 사용량을 예측하기 어렵기 때문에 Gas Limit이라는 개념이 필요합니다. 트랜잭션이 컨트랙트의 함수를 실행시키는 경우 컨트랙트의 상태(state)에 따라 가스 사용량이 달라질 수 있습니다. 트랜잭션을 로컬에서 실행시켜서 100000 Gas가 나왔다 하더라도, 트랜잭션이 마이닝 되기 전에 다른 트랜잭션이 먼저 실행되어 해당 컨트랙트의 상태가 바뀌게 되면 100000 Gas보다 높아질 수도, 낮아질 수도 있습니다. 심지어 컨트랙트가 블록 정보(nonce, 블록 시간, 블록 번호 등)를 쓴다면 마이닝되는 시점에 따라 가스 사용량이 바뀔 수 있습니다.


EVM은 튜링 머신이기 때문에 최악의 경우 무한 루프에 빠져서 가스를 무한정 사용할 가능성도 배제할 수 없습니다. 이 때 가스 사용량이 Limit을 넘게 되면 해당 트랜잭션은 Out of Gas 에러를 내고 실행되지 않습니다. 실행되지 않지만 블록에는 포함되기 때문에 수수료는 발생합니다.


그러나 (아직까지는)스마트 컨트랙트 구현이 방어적이기도 하고, 복잡하지 않기 때문에 의도치 않게 가스를 많이 내게 되는 경우는 잘 없는 것 같습니다. 만약 Gas Limit을 적게 주어 Out of Gas로 수수료만 날릴 일이 생기는 것보단 Gas Limit을 넉넉히 주는 것이 좋을까요?


Parity 클라이언트의 트랜잭션 우선순위 전략

Gas Limit은 트랜잭션에만 있는 것이 아니라 블록에도 존재합니다. 이를 Block Gas Limit이라 하며 비트코인에서의 블록 크기 제한과 같은 역할을 해줍니다. 따라서 마이너는 최대 이익을 위해 최적의 트랜잭션 조합을 선택해야 하는 문제가 있습니다. 이 문제가 0-1 배낭 문제인 것을 인식하면 Gas Price가 높은 순으로 트랜잭션을 뽑는 것이 항상 최적이 아님을 알 수 있습니다. 그렇다고 마이닝 경쟁을 해야하는 시점에 트랜잭션 조합만 계산하고 있을 순 없기 때문에 Gas Price 순으로 뽑는 것도 하나의 방법이긴 합니다. Parity 클라이언트는 Gas Price 순을 포함해 세 가지 우선순위 전략을 가지고 있습니다.


- GasAndGasPrice

- GasFactorAndGasPrice (default)

- GasPriceOnly


GasAndGasPrice와 GasFactorAndGasPrice는 Gas Limit이 낮을 수록 우선순위가 높은 전략입니다. 특히 GasAndGasPrice는 Price보다 Limit를 우선 비교합니다.


GasFactorAndGasPrice는 가중치를 두어서 계산하는 휴리스틱으로 보입니다. transaction_queue.rs의 테스트 코드를 보면 아래처럼 우선순위가 매겨집니다.


Gas Limit

Gas Price

Priority 

 30000

 15 

 1

 150000

 62

 2

 40000

 16

 3

 150000

 40

 4


테스트 예제의 1순위 트랜잭션이 Gas Price가 가장 낮습니다. 마이너가 트랜잭션을 빨리 포함시켜주길 원한다면 Gas Price 뿐만 아니라 Gas Limit의 설정에도 신경써야 한다는 것을 알 수 있습니다. 즉, 실제 가스 사용량을 정밀하게 예측할 수 있다면 그만큼 Gas Price를 낮게 줄 만한 여지가 있다는 의미입니다.




위의 결과로 봤을 때 GasFactorAndGasPrice가 마이너에게 가장 이익이 되는 전략은 아닐 것으로 보입니다. Parity를 개발하는 입장에선 이더리움 네트워크의 성공 역시 중요하기 때문에 트랜잭션 처리량을 높이는 방향으로 정책을 조정하고 싶었을 것입니다.


마이너들이 실제로 어떤 전략을 주로 취하는지 정확히는 알 수 없으나 etherscan에서 같은 시간대의 블록들의 Gas Price가 10배씩 차이나는 경우가 있는 것을 보면 적어도 블록에 포함되는 요인이 Gas Price만이 아님은 알 수 있습니다.


336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

이전 포스팅에서 트랜잭션의 Input/Output의 자료구조와 UTXO 모델에 대해 설명하였습니다.


Output에는 누가(scriptPubKey) 얼마(nValue)를 받았는지 표시되고, 어떤 Input은 이 Output을 가리켜 해당 nValue만큼의 비트코인을 쓸 수 있다고 하였습니다. 그리고 아직 소비되지 않은 Output을 UTXO(Unspent Transaction Outputs)이라 한다고 하였습니다.


트랜잭션 Input 검증

트랜잭션 검증에서 중요한 일 중 하나는 트랜잭션 Input을 검증하는 일입니다. 예를 들어 A가 채굴에 성공해서 50 비트코인의 Output을 가지고 있었는데, 이를 A가 아닌 다른 사람이 쓰면 안되기 때문에 scriptPubKey라는 A만 풀 수 있는 잠금 장치를 걸어두고, 이를 A가 쓸 때에는 scriptSig라는 일종의 잠금해제 장치를 제공해야 합니다.


이전 포스트에서 트랜잭션 Input은 prevout과 scriptSig로 구성된다고 하였습니다. prevout이 가리키는 트랜잭션 Output에는 scriptPubKey가 있습습니다. scriptSig와 scriptPubKey 두 개 스크립트 쌍이 맞아야 합니다. 개념적으로는 scriptPubKey는 공개키이고 scriptSig는 비밀키 서명입니다. 그러나 실제로는 명령어 코드(OPCODE)와 데이터로 구성된 스크립트이고 이 명령어를 수행한 결과에 의해 Input 검증 성공/실패가 결정됩니다.


비트코인 스크립트 실행 방법

비트코인 스크립트 실행은 스택이 빈 상태에서 input의 scriptSig를 먼저 실행하고, 그 다음 scriptPubKey를 실행시킵니다. 실행 결과 스택의 맨 위 값이 FALSE이면 실패, TRUE(0이 아닌 값)이면 성공한 것입니다. 성공할 경우에만 해당 input 검증을 통과한 것으로 처리합니다.


스크립트는 후위연산식으로 생각할 수 있습니다. 아래는 후위연산식 실행의 간단한 예제입니다.


4 5 +


위의 후위연산식을 실행하면 아래 결과가 나옵니다.


9


조금 더 복잡한 후위연산식


4 5 * 2 +


결과


22


아래와 같이 scriptPubKey가 있다고 가정해 봅시다.


4 + 10 ==


== 연산자는 2개 값을 읽어 같으면 TRUE, 같지 않으면 FALSE를 반환하는 연산자입니다.

+ 연산자는 2개 피연산자를 요구하는데, 그 앞에 4 밖에 없어서 위의 스크립트는 무효합니다. 4 앞에 어떤 하나의 숫자를 제시하면 유효한 스크립트를 만들 수 있습니다.

위의 scriptPubKey를 풀기 위해서는 scriptSig를 제공해야 합니다.


scriptPubKey: 4 + 10 ==

scriptSig: x


여기서 scriptSig와 scriptPubKey를 차례대로 실행시키면 아래 스크립트를 실행시키는 것과 같은 효과입니다.


x 4 + 10 ==

위의 스크립트는 x가 6이면 실행 결과가 TRUE이고, 아닐 경우 FALSE입니다.


즉, "4 + 10 =="는 풀어야 할 문제이고, "6"은 그 해답입니다.



scriptPubKey: 문제

scriptSig: 해답



표준 스크립트

비트코인은 비밀키 서명을 검증할 수 있는 OP_CHECKSIG 명령어를 제공합니다. "x y OP_CHECKSIG" 스크립트는 x를 비밀키 서명, y를 공개키로 취급하여 유효한 서명이면 TRUE, 아닐 경우 FALSE를 반환합니다. OP 코드의 목록은 비트코인 위키에서 확인할 수 있습니다.


현재 비트코인 네트워크에서는 표준 스크립트만 허용되고 있습니다. (https://bitcoin.org/en/developer-guide#standard-transactions)


- P2PKH

- P2SH

- P2PK

- Multisig

- Null Data


가장 간단한 표준 스크립트는 P2PK 스크립트입니다.

P2PK (Pay to Public Key)

scriptPubKey

<pubkey> OP_CHECKSIG

scriptSig

<sig>



잘 아시다시피 비트코인 거래 내역은 공개되어 누구나 볼 수 있습니다. 즉 풀어야 할 문제가 미리 공개되어 있고, 누군가 그 문제를 푼다면 해답 역시 공개됩니다. 미리 공개되어 있는 문제는 충분히 어려워야 하고, 제시된 해답은 다른 UTXO를 공격할 힌트가 되지 않도록 조심해야 합니다.


위의 P2PK 스크립트는 내가 비트코인을 받는 시점에서 공개키가 드러나게 됩니다. 만약 ECDSA가 공격당할 경우 P2PK로 만든 UTXO 모두 공격당하게 됩니다. 따라서 더 안전한 표준 스크립트인 P2PKH 스크립트를 쓰는 것이 보안상 권장됩니다.


P2PKH(Pay to Public Key Hash)

scriptPubKey

OP_DUP OP_HASH160 <PubKeyHash> OP_EQUALVERIFY OP_CHECKSIG

scriptSig

<sig> <pubkey>


P2PKH의 scriptPubKey에는 공개키의 해시값만 있습니다. scriptSig를 제시할 때 공개키를 내가 직접 제공해야 합니다. scriptPubKey의 OP_DUP부터 OP_EQUALVERIFY는 공개키 해시를 검증하는 부분이고, 이게 사라지면 "<sig> <pubkey> OP_CHECKSIG"로 P2PK와 같은 형태가 됩니다.


중요한 점은 scriptPubKey에는 공개키의 해시만 있다는 점입니다. 만약 ECDSA 알고리즘이 완전히 뚫리더라도 scriptPubKey에 있는 공개키 해시만 보고 비밀키를 알아내기 위해서는 ECDSA뿐만 아니라 해시 함수까지 공격해야 합니다. 해당 UTXO를 사용하는 순간 scriptSig에 공개키가 드러나면 비밀키를 공격할 수 있습니다. 하지만 이는 매번 새로운 공개키를 사용하면 문제가 되지 않습니다.


P2PKH는 블록 익스플로러나 지갑 앱에서 보여주는 1로 시작하는 주소(예: 1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2)에 해당됩니다. 공개키 해시에 네트워크 ID와 체크섬을 붙이고 Base58로 변환한 값입니다.

Multisig

scriptPubKey

<m> <A pubkey> [B pubkey] [C pubkey...] <n> OP_CHECKMULTISIG

scriptSig

OP_0 <A sig> [B sig] [C sig...]


OP_CHECKMULTISIG는 n개 중에 m개의 서명을 받으면 TRUE를 반환하는 OP코드입니다. 이는 다양한 응용에 쓰일 수 있습니다. 예를 들면 두 명이 공동으로 자산을 관리하는데, 돈을 쓰기 위해선 2명의 서명을 모두 필요로 할 수 있고, 한 명의 서명만을 필요로 하게 할 수도 있습니다. 변호사같은 중립적인 역할을 두어 3개 중 2개 서명이 필요하도록 할 수도 있습니다.


여기에선 기본적으로 공개키가 2개 이상이기 때문에 주소를 효율적으로 만들기가 까다롭습니다. 주소라는 것은 결국 저 scriptPubKey를 특정할 수 있어야 합니다. 예를 들면 A가 B에게 돈을 주기 위해서 B에게 주소를 물어보면, B가 n이 10인 Multisig로 받기 위해서는 공개키 10개를 모두 전달해야한다는 의미가 됩니다. 이는 비효율적이고 공개키가 모두 공개되는 문제가 있습니다. Multisig로 돈을 받기 위해서는 P2SH 표준 스크립트를 사용하는 것이 권장됩니다.

P2SH(Pay to Script Hash)

scriptPubKey

OP_HASH160 <Hash160(redeemScript)> OP_EQUAL

scriptSig

<sig> [sig...] <redeemScript>


P2SH에선 scriptSig에 있는 redeemScript가 실제 "문제" 역할을 담당합니다. scriptSig에는 문제와 해답(<sig>)이 동시에 존재하는 셈입니다. 임의의 공격자가 스스로 자기가 풀 수 있는 문제를 제시하고 혼자 푸는 상황을 방지하기 위해, 문제지의 해시를 scriptPubKey에 기록합니다. 


아래는 Multisig를 redeepScript로 사용하는 예입니다.

scriptPubKey

OP_HASH160 <Hash160(redeemScript)> OP_EQUAL

redeemScript

<OP_2> <A pubkey> <B pubkey> <C pubkey> <OP_3> OP_CHECKMULTISIG

scriptSig

OP_0 <A sig> <C sig> <redeemScript>


P2SH가 지갑 앱에서 보여주는 3으로 시작하는 주소(예: 3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy)에 해당됩니다. 3으로 시작하는 주소는 Multisig일 가능성이 있습니다. 그러나 3으로 시작하는 주소가 실제로 어떤 redeemScript를 쓰는지는 scriptSig가 제시되기 전까진 알 수 없습니다. P2PKH 일 수도 있습니다.


그렇다고 P2SH를 쓰는 것이 항상 좋은 것만은 아닙니다. 스크립트의 크기가 커지면 트랜잭션이 커지고, 이는 수수료가 비싸지는 것을 의미합니다. 최근에 비트코인 수수료가 매우 비싸지면서 P2PK를 쓰는 UTXO도 많이 보이고 있습니다. 통상적인 P2PKH Input 1개 + Output 2개 트랜잭션이 약 226 바이트인데, 주는 사람이 P2PK를 쓸 경우 126 바이트로 공간 비용을 절약할 수 있습니다.

'비트코인 Technical Background' 카테고리의 다른 글

비트코인 주소에 대한 이해 (1) - UTXO 모델  (0) 2018.01.26
비트코인 SPV  (2) 2018.01.17
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

이더리움에서는 랜덤 함수가 없습니다. 어떤 노드가 실행하더라도 같은 결과가 나와야만 트랜잭션을 검증할 수 있는 구조이기 때문입니다. 이는 확률 요소가 있는 게임이나 도박 응용을 구현하는데 문제가 됩니다. 트랜잭션을 미리 실행시켜보고 그 결과가 자신에게 불리할 경우 네트워크에 전파하지 않을 것이기 때문입니다. 이번 포스팅에서는 어떻게 랜덤 함수를 구현할 수 있는지에 대해 알아보겠습니다.



블록 스페셜 변수

우선 Solidity를 기준으로 랜덤 시드가 될 수 있는 스페셜한 블록 변수들이 있습니다.


- 블록의 번호

- 블록의 시간

- 블록해시


그러나 기본적으로 블록 변수들은 마이너에 의제 통제됩니다. 만약 트랜잭션의 결과가 블록 보상 이상의 인센티브가 있는 경우 이를 랜덤 시드로 쓰는 것은 적절하지 않습니다. 예를 들어 블록 보상이 3 이더인데, 100 이더짜리 도박판이 열리면 마이너는 PoW에 성공했더라도 그 블록에 자신이 도박판에서 지는 트랜잭션이 포함될 경우 그 블록을 발표하지 않고 버릴 수도 있음을 염두에 두어야 합니다.



블록의 번호를 쓸 경우 마이너 뿐만 아니라 유저에게도 예측될 수 있습니다. 예를 들어 1~6의 주사위를 던져 짝수가 나오면 이기는 게임이라 했을 때, 블록의 번호를 하나씩 올려가보면서 시뮬레이션하여 짝수가 나오는 블록에 포함되도록 트랜잭션 발표를 조정할 수 있습니다.


블록의 시간은 마이너에 의해 통제된다고 볼 수 있습니다.


블록의 해시는 마이너와 사용자 모두 예측하기 어렵지만, 마이너가 이해관계에 놓여있는 경우 위에서 언급한 것처럼 블록 자체를 발표 안 할 가능성이 존재합니다.



유저가 제출한 랜덤 시드를 XOR

블록 변수를 쓰는 것은 마이너가 이해당사자일 경우 동작을 안하는 단점이 있기 때문에, 이를 피하기 위해 유저가 직접 랜덤 시드를 제공하는 방법이 있습니다.


A, B가 1~6 주사위를 굴려서 홀짝을 가리는 게임을 한다고 가정해봅시다.


1. A는 랜덤한 값 x, B는 랜덤한 값 y를 생성해 자신만 알 수 있도록 기록해 둡니다. 생성한 랜덤 값 원본을 pre-image라 부릅니다

2. A, B는 pre-image를 해시한 값을 드러내는 트랜잭션을 합니다. 네트워크 상에선 hash(x), hash(y)가 발표되고 서로의 pre-image는 모르는 상태가 됩니다.

3. A, B 모두 해시값을 발표했으면 pre-image를 드러내는 트랜잭션을 합니다. A, B는 pre-image와 그 해시값을 검증한 뒤 (x XOR y) 값을 랜덤 시드로 쓰일 수 있습니다.


이는 A, B, C, D, ...로 확장되어도 똑같은 구성으로 랜덤 시드를 생성할 수 있습니다. 랜덤 시드를 제공한 사람들 중 일부가 담합하더라도, 그 중에서 단 한 명만이라도 랜덤한 값을 생성하였다면 결과 값이 랜덤해지는 속성이 있습니다.




유저가 제출한 랜덤 시드를 사용할 때 두 가지 주의점이 있습니다.

1. Replay

- A, B, C가 주사위 게임을 하고 B, C가 담합한 상태라고 가정합니다.

- B는 A를 따라 합니다.

  - A가 해시를 발표하면 B는 pre-image는 모르지만 해시를 똑같이 발표할 수는 있습니다.

  - pre-image를 발표하는 단계에서 B는 A가 발표하는 pre-image를 본 뒤에 똑같이 발표합니다.

- A와 B의 pre-image는 똑같으므로 XOR 연산에 의해 0이 되어서 C의 pre-image가 랜덤 시드가 됩니다.


해시를 만들때 [자신의 주소(공개키), pre-image]를 해시하도록 하면 B가 따라하더라도 검증 단계에서 걸러지게 됩니다.


2. DoS

pre-image를 발표하는 단계에서 마지막으로 발표를 하는 사람은 그 결과를 예측하고 자신의 pre-image를 발표하지 않을 수도 있습니다. 이를 위해 처음에 해시를 발표할 때 Timelock을 이용해서 일정 시간 안에 pre-image를 발표하지 않으면 패널티를 주는 장치를 해두어야 합니다.






참고:


http://solidity.readthedocs.io/en/develop/units-and-global-variables.html#special-variables-and-functions


336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

https://blockexplorer.com/tx/89c05b4ea989a19e529b93a6fd883c5089aaacfd83350d8fce28d0822259ce2f


비트코인 트랜잭션을 볼 수 있는 BlockExplorer에서 트랜잭션을 보다 보면 "Unparsed Address"를 자주 발견 할 수 있습니다. 이번 포스팅과 다음 주에 이어지는 포스팅을 통해 비트코인 트랜잭션에 표시되는 주소가 어떤 원리에 의해 표현되는지 이해함으로써 비트코인의 내부 동작을 이해해 보도록 하겠습니다.


- UTXO 모델

- Script (2월 3일 작성 예정)


UTXO (Unspent Transaction Outputs)

비트코인 트랜잭션을 생각할 때 흔히 할 수 있는 착각 중 하나는 블록체인 상에 증여자(giver)와 수증자(receiver)의 비트코인 주소가 기록될 것이라는 점입니다. 여러 사람의 거래 내역을 적는 원장(ledger)의 개념으로 비트코인을 처음 접하거나 어카운트(account) 기반인 이더리움을 먼저 공부하셨다면 이런 선입견을 가지기 쉽습니다. 그러나 비트코인은 그런 방식으로 동작하지 않습니다.


비트코인의 레퍼런스 구현인 Bitcoin Core 구현의 트랜잭션 자료구조를 살펴보겠습니다. 트랜잭션은 보내는 사람에 해당하는 CTxIn과 받는 사람에 해당하는 CTxOut 클래스가 있습니다.


https://github.com/bitcoin/bitcoin/blob/9cf6393a4f82b9c81d3b4b468a17a89db10531a2/src/primitives/transaction.h#L61

https://github.com/bitcoin/bitcoin/blob/9cf6393a4f82b9c81d3b4b468a17a89db10531a2/src/primitives/transaction.h#L131





A(증여자)가 B(수증자)에게 10 BTC를 주는 트랜잭션 작성하는 것으로 예로 들어보겠습니다.

A는 B의 주소를 알고 있으며 아래와 같이 트랜잭션을 만들 수 있습니다.


CTxIn prevout - A의 공개키로 받은 CTxOut에 대한 포인터

CTxIn scriptSig - A의 서명 (비밀키)


CTxOut nValue - 비트코인의 양 (10 BTC)

CTxOut scriptPubKey - B의 공개키

 

위의 트랜잭션이 블록에 포함되면 B는 미래에 이 CTxOut을 포인팅해서 10 BTC를 쓸 수 있게 됩니다.


CTxIn은 가리킬 수만 있고 비트코인의 양을 지정할 수 없습니다. 즉, CTxOut은 무조건 한 번에 소비됩니다. 만약 B가 C에게 3 BTC만 주고 싶다면, (10 BTC의 CTxIn) -> (3 BTC의 CTxOut), (7 BTC의 CTxOut)의 트랜잭션을 만들어 3 짜리는 C에게 주고, 7 짜리를 다시 자기 자신에게 돌려주면 됩니다.


B의 입장에서 7 짜리 거스름돈을 받을 때 공개키를 재사용할 이유가 없습니다. B는 새로운 키쌍을 생성해 해당 키에 돌려 받습니다. 이 과정에서 이 거래와 상관없는 D가 블록체인에 기록된 거래내역을 봤을 때 무엇이 거스름돈인지 알 수가 없습니다. 심지어 거스름돈이 없이 수증자가 2명인 가능성도 배제할 수 없습니다. 이 모델은 프라이버시 측면에서 장점이 있습니다.


이 글 처음에 있는 스크린샷을 다시 보겠습니다. 1개 CTxIn과 2개의 CTxOut이 있는 경우입니다. 



위의 경우는 추측컨데 0.099 BTC를 누군가에게 주는 트랜잭션일 것입니다. 남은 돈(5.92.. BTC)은 자신의 공개키로 다시 CTxOut을 만든 것을 볼 수 있습니다.


CTxOut의 오른 쪽에 초록색 폰트로 (U)가 있는데, 이는 해당 CTxOut이 아직 쓰이지 않았다는 것(Unspent)을 의미합니다. 이를 UTXO(Unspent Transaction Outputs)라 합니다. 미래에 이 CTxOut을 포인팅하는 트랜잭션이 일어나면 (S)로 표시됩니다.



비트코인 지갑이나 BlockExplorer에서 보여주는 잔고(balance)는 하나 이상의 UTXO의 합이라 할 수 있습니다. 이로 인해서 비트코인 트랜잭션을 만들 때 여러 옵션이 존재할 수 있습니다.


A가 10 BTC, 5 BTC, 3 BTC의 UTXO를 가지고 있고 B에게 6 BTC를 줘야 하는 상황을 가정해보면 아래와 같습니다.


- 10 을 쓴다. -> 잔고: 4 + 5 + 3

- 5 + 3 을 쓴다. -> 잔고: 10 + 2

- 10 + 5 + 3 을 쓴다 -> 잔고: 12


조합에 따라 트랜잭션 바이트 크기가 달라지고, 바이트 크기가 작을수록 마이너에게 지불하는 수수료를 낮출 수 있기 때문에 위의 조합 문제를 풀어야 하는 요구사항이 있습니다.


결론

비트코인을 보유하는 사람은 공개키를 여러 개 소유할 수 있고, 각 공개키는 하나 이상의 UTXO와 연결될 수 있지만, 프라이버시 측면에서 하나의 공개키 당 하나의 UTXO만 연결되는 것이 바람직합니다. 보유자는 이 UTXO를 조합하여 지불할 수 있습니다.


사용자 입장에서 UTXO 모델은 이해하기 더 어렵고, 위에서 설명한 조합 문제나 UTXO set을 관리하는 문제 등 기술적인 문제들도 있습니다. 하지만 트랜잭션의 병렬 처리나 프라이버시 측면에서 이더리움의 어카운트(Account)처럼 주소가 재사용되는 구조에 비해 장점이 분명히 존재합니다.


앞서 CTxOut의 scriptPubKey를 설명할 때 이를 공개키라고 표현했지만, 실제로는 공개키해시를 포함하는 실행 가능한 스크립트이고, 엄밀히 말하면 공개키 해시를 포함하지 않을 수도 있습니다. 다음 포스팅에서는 비트코인 스크립트의 실행과 표준 스크립트에 대해 설명하겠습니다. 표준 스크립트는 비트코인 주소와 직접적으로 연관되어 있습니다.


336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

2018년 1월 기준으로 비트코인 블록체인 데이터는 150GB를 넘어섰습니다. 스마트폰에 저장하기엔 너무 크기 때문에 스마트폰에서는 풀노드를 구동하기 어렵습니다. SPV란 Simplified Payment Verification의 약자인데, 비트코인을 받았다는 사실을 전체 블록체인을 다운로드하지 않고도 검증하는 방법이며 이는 사토시 나카모토의  화이트페이퍼에서도 소개되고 있습니다.



비트코인의 트랜잭션 구성은 아래처럼 구성됩니다.


- input (비트코인을 보내는 사람의 서명)

- output (비트코인을 받는 사람)

- 버전

- nLockTime


비트코인을 받는 입장에서는 위의 트랜잭션을 까보고 서명을 검증할 수는 있지만, 실제로 비트코인을 받았는지 확신할 수 없습니다.

보내는 사람이 실제로 비트코인을 그만큼 소유하고 있었고, 이중지불을 하지 않았어야 하는데 이는 트랜잭션만 봐서는 알 수 없습니다.


SPV 클라이언트는 풀노드로부터 머클 증명과 일부 블록의 헤더만 받아서 트랜잭션의 유효성을 검증할 수 있습니다.


일단 비트코인 블록 헤더 구성을 보면


- 버전 (4바이트)

- 이전 블록해시 (32바이트)

- 머클루트 해시 (32바이트)

- 블록 시간 (4바이트)

- nBits (4바이트)

- nonce (4바이트)


로 총 80바이트입니다. 1년 동안 발생하는 52560개 블록 헤더 용량이 4MB 수준이니 매우 가볍다고 할 수 있겠습니다.

참고로 nBits는 블록 난이도를 계산할 수 있는 필드입니다.


받는 사람 입장에서 증명되어야 할 사실은 두 가지입니다.


1. 내가 받은 트랜잭션이 블록에 포함되었는가?

2. 그 블록이 고아블록은 아닌가?




트랜잭션이 블록에 포함되었는지 여부는 머클 증명으로 확인할 수 있습니다.


만약 내가 Tx1이 블록에 포함되었는지 여부를 알고 싶다면 풀노드로부터 Hash0과 Hash23을 받아 해시를 3번만 돌려보면 머클루트 해시를 알 수 있습니다.

블록에 트랜잭션이 2000개쯤 있어도 해시함수를 11번 정도만 돌려보면 됩니다. 해시가 충돌할 수도 있지 않냐구요? 음... 아니요...


머클루트 해시를 알았으면 받은 블록 헤더의 머클루트 해시와 일치하는지 확인하면 됩니다.


블록이 철회 될 확률이 작다는 것을 증명하기 위해서는 해당 블록 뒤에 이어지는 블록 헤더들을 받아서 PoW를 확인하면 됩니다. 통상적으로 6 confirmation이면 안전하다고 판단하니, 6개의 블록 헤더를 받아 검증하면 됩니다.



풀노드가 PoW를 수행해 고아블록을 생성해서 나를 속이거나, 해시함수를 공격해서 나를 속일 가능성이 0인 것은 아닙니다. 다만, 비트코인은 컴퓨팅 파워가 있다면 정직하게 PoW를 해서 블록 생성 보상을 받는게 경제적으로 유리하도록 설계된 프로토콜임을 기억하시길 바랍니다.




위의 SPV 방식은 PoW 알고리즘에 의존하고 있습니다. 따라서 PoS 등 다른 합의 알고리즘에서는 다른 SPV 방식이 필요할 것입니다. 예를 들어 PoS라고 생각해 본다면 "Staking 정보를 전체 블록체인 데이터없이 어떻게 증명할 것인가?" 를 생각해볼 수 있겠습니다. 관련한 비탈릭의 이더리움 블로그 포스트 링크를 남깁니다.


https://blog.ethereum.org/2015/01/10/light-clients-proof-stake/

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

1월 7일 코인원에서 미량의 비트코인을 출금하려 했는데, OTP까지 입력하고 나서야 "전송을 실패했습니다" 메시지로 막히고 더이상 진행 할 수 없었던 문제가 있었다.






그리고 1월 5일자 비트코인 출금이 원활하지 않은 이슈가 있다는 공지를 확인했다.

https://coinone.co.kr/talk/notice/329/


공지에는 시간을 두고 재시도하라는 답변이 있었지만, 다음 날에도 시도했지만 역시나 실패했다. OTP 비밀번호를 30번은 넘게 입력한 것 같다.

실패 끝에 고객센터에 문의를 하고 받은 답변은 공지 내용과 다름이 없었다.


결국은 비트코인 출금을 포기할 수 밖에 없었고, 다른 경로로 출금했다.




나는 이게 출금이 원활하지 않은 수준이었는지 부터가 의심스럽다.


이미 내놓은 트랜잭션의 수수료가 상대적으로 낮아서 블록에 포함이 안되는 문제가 있었을 것이고,

이런 트랜잭션이 쌓이면 핫월릿에서 쓸 수 있는 UTXO가 다 떨어졌을 수도 있다. 출금 기록을 남기려면 트랜잭션 정보를 고객한테 줘야하는데 컨펌도 안된 UTXO를 쓰는 트랜잭션을 고객한테 주는 것은, 송금시간을 포함해 정책 상 문제가 될 수 있으니 트랜잭션 생성 실패로 처리한다는 상상을 해볼 수 있다. 어디까지나 상상 속의 시나리오다.



유쾌한 경험은 아니었다. 오늘자 기사 중에는 빗썸에서 원화 출금도 지연되고 있다는 소식이 있는데, 출금 지연 혹은 실패로 발생할 수 있는 손실은 거래소에서 충당해 줄 것인가? 이런 부분에서는 거래소 규제가 빠르게 이루어져야 한다고 생각한다.


http://www.yonhapnews.co.kr/bulletin/2018/01/17/0200000000AKR20180117067400002.HTML

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

이번 포스팅에선 이더리움 기반 토큰을 만들기 위한 Solidity 문법과 토큰 표준인 EIP20(ERC20)에 대해 알아보겠습니다.


첫 번째로 살펴볼 내용은 mapping 데이터 타입입니다.


예제 출처: http://solidity.readthedocs.io/en/develop/types.html#mappings


mapping은 일종의 key-value 스토리지 타입입니다. 기존 범용 프로그래밍 언어에서 흔히 볼 수 있는 Map<Key, Value>와 다른 점은 모든 키에 대해 Value가 존재하는 것처럼 동작합니다. 즉, 특정 키에 값을 쓰기 전에도 읽기가 가능하고, mapping 생성 시 초기화는 모든 키에 대해 값이 0인 것과 같습니다. 이런 특성 때문에 mapping 타입 자체에는 key에 대한 순회 방법이 존재하지 않습니다. key를 순회하기 위해서는 순회하려는 key의 리스트를 별도로 관리해야 합니다.


msg.sender는 transfer()를 호출한 이더리움 계정의 주소입니다. 즉, 위의 예제는 해당 이더리움 계정을 가진 사람만이 자신의 balance를 업데이트 할 수 있습니다.


이 mapping(address => uint) 타입이 이더리움 기반 토큰의 핵심을 담고 있습니다. 우리가 비트코인이나 이더리움의 이더를 생각해보면 어떤 암호화폐의 소유란 결국 내가 소유한 주소(공개키)에 얼마가 있는지가 공개되어 있다는 것입니다. 만약 우리가 이더리움 주소를 생성하면 그 주소에는 0 ETH가 들어있는 것으로 취급됩니다. 누군가 입금을 하기 전까지는요. mapping와 같지요. 이제 토큰 보내는 함수를 보겠습니다.



require(bool condition)는 조건이 만족하지 않으면 해당 트랜잭션을 취소합니다. 예제 구현에서는 자신이 가진 토큰보다 더 많은 토큰을 보내려 할 때 이를 막기 위해 쓰여졌습니다. 내 이더리움 계정을 가지고 transfer(address to, uint value) 함수를 호출함으로써 to 주소로 value 만큼 토큰을 보낼 수 있습니다. to는 다른 이더리움 계정이겠지요. 이러한 원리로 이더리움 계정은 ETH를 소유함과 동시에 이더리움 기반 토큰을 소유할 수 있습니다. 이제 남은 것은 토큰의 발행 정도이군요. 아주 간단한 예제를 하나 보겠습니다.



InitialSupplyExample은 생성자를 두어 생성자를 호출한 사람에게 10만 토큰을 넣습니다. 즉, 토큰을 만든 사람에게 10만 토큰을 발행한 것이지요. 위 예제의 경우 해당 토큰은 10만 개가 총 발행량이 되고, 더 이상 발행될 수 없습니다. 추후에 더 발행할 수 있는 토큰을 만들고 싶다면 아래처럼 작성해야 합니다.




mint 함수는 owner가 토큰을 새로 생성할 수 있는 함수입니다. 생성자를 호출한 사람을 owner로 기억해두기 때문에 해당 토큰을 만든 사람만이 토큰을 생성할 권리를 가지게 됩니다. 물론 위의 예제처럼 아무런 제약없이 토큰을 생성할 수 있다면 아무도 이 토큰에 가치를 신뢰하지 않을 겁니다.


토큰을 owner가 찍어내는 방식이 아닌 payable 함수를 두어 ETH를 받고 토큰을 발행해주는 방식도 생각해볼 수 있겠습니다. 스마트 컨트랙트 코드를 어떻게 짜느냐가 그 토큰의 특성을 정의하게 됩니다.


여기까지 간단한 이더리움 기반 토큰 구현에 대해 살펴보았고, 이제 ERC20 인터페이스에 대해 살펴보겠습니다. ERC20 토큰 표준안은 각종 토큰을 구현하는 스마트 컨트랙트가 지켜야 할 인터페이스입니다. EIP20 표준으로 채택되어 이 인터페이스를 지킨다면 지갑 앱이나 explorer 앱에서 이 토큰과 관련된 정보를 보여줍니다. 아래 스크린샷은 etherscan.io에서 Bread TOKEN 정보를 보여주는 예입니다.



아래는 EIP20 토큰이 지켜야 할 contract 인터페이스입니다.




여기서 Approval과 관련된 transferFrom/approve/allowance 함수는 설명이 필요할 것 같습니다. 이더리움 토큰은 토큰을 직접 보내는 방법도 있지만, 다른 사람이 내가 가진 토큰 사용을 허가하는 방법도 있습니다.


A라는 이더리움 계정이 approve(B, 100)을 호출하면 B 계정이 A 계정에 있는 토큰 100개를 사용할 수 있습니다. B는 transferFrom(A, C, 60)을 호출해서 C 계정에 60 토큰을 보낼 수 있습니다.

allowance는 balanceOf의 approval 버전입니다. A가 B에게 100을 approve하고, B가 C에게 transferFrom으로 60을 보내고 나면 allowance(A, B)는 40이 반환됩니다.


직접 transfer대신 approve하는 방식은 수수료 문제로 자주 쓰입니다. 예를 들어 어떤 토큰이 transfer 함수 내에서 msg.sender에게 1토큰의 수수료를 부과한다고 가정해 봅시다.


transfer 방식

A calls transfer(B, 100)

이 경우 A에서 101 토큰이 출금되고 B에 100이 입금됩니다.


approve 방식

A calls approve(B, 100)

B calls transferFrom(A, B, 100)

이 경우 A에서 100 토큰이 출금되고 B에 99가 입금됩니다.


수수료를 내는 주체가 달라집니다. 물론 이는 transfer에 수수료가 있다는 가정 하에 그렇습니다. 앞서 토큰 발행 시에 payable 함수를 두어 ETH를 토큰으로 바꾸는 예를 살짝 소개했는데, 토큰을 ETH로 바꾸는 환불의 경우 트랜잭션 수수료가 강제되기 때문에 ETH 출금같은 경우에는 거의 이 방식이 쓰입니다. 물론 이는 보내는 사람의 수수료 비용을 줄여주기도 하지만 ETH를 보낼 때 발생할 수 있는 보안 위협을 줄일 수 있는 방법이기도 합니다. 자세한 내용은 다른 보안 이슈 관련 포스팅에서 소개할 기회가 있으면 좋겠습니다.


지금까지 이더리움 기반의 토큰과 그 표준인 EIP20에 대해 알아보았습니다. 토큰에 관한 구체적인 구현이 궁금하신 분들은 OpenZeppelin의 라이브러리를 살펴보시면 도움이 될 것 같습니다.


https://github.com/OpenZeppelin/zeppelin-solidity/tree/master/contracts/token


20분만에 이더리움 토큰 발행하기라는 재미있는 포스팅도 있어 소개합니다. 이미 짜여진 라이브러리를 수정하면 이름 붙이고 어떻게 발행할 지만 결정하면 되는 정도라 매우 간단합니다.


https://medium.com/bitfwd/how-to-issue-your-own-token-on-ethereum-in-less-than-20-minutes-ac1f8f022793


336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.



이 포스트는 비트코인이 우리가 쓰는 현금이나 신용카드와 어떤 점이 다른지를 설명하는 글입니다. 비트코인에 대한 사전지식이 없더라도 읽기 편하도록 구성했습니다.


비트코인 화이트페이퍼 제목인 "A Peer-to-Peer Electronic Cash System"에 등장하는 P2P와 Electronic Cash를 이해하는 것부터 시작하려 합니다. 요즘은 모두 신용카드와 페이류 앱들을 쓰면서 현금을 잘 쓰지 않는데요. 제가 페이코를 가끔씩 쓰기 때문에 페이코를 가지고 설명을 좀 하겠습니다. 언젠가 자원봉사를 하고 페이코 상품권(?)을 받은 적이 있습니다. 상품권의 코드를 페이코 앱에 입력하면 페이코 포인트가 생기고, 이 포인트는 페이코 가맹점에서 현금처럼 쓸 수 있습니다. 일종의 전자 화폐이지요. 전자 화폐는 현금과 어떻게 다를까요?

전자 화폐 (Electronic Cash)

현금은 거래가 매우 간단합니다. 물건을 살 때 현금을 건네주면 됩니다. 현금을 받은 사람이 위조 여부를 확인하면 거래가 성립됩니다. 이 거래는 당사자만 관여되어 있는 Peer-to-Peer(이하 P2P) 거래라고 볼 수 있습니다. 페이코 포인트 같은 전자화폐 거래는 다소 다릅니다. 제가 페이코 앱을 켜서 PIN 번호를 입력하고 바코드를 점원에게 보여주면 점원은 바코드를 찍어서 결제 정보를 페이코 서버로 보내겠지요. 이 페이코 서버로부터 정상 응답을 받으면 거래가 성립됩니다. 만약 그 시점에 페이코 서버가 응답을 하지 못하면 거래는 성립되지 않습니다. 이 경우는 돈을 보내는 사람과 받는 사람 사이에 지급을 보장해주는 거래 중개자가 필요합니다. 이 과정에서 중개자는 거래 당사자들로부터 거래 수수료를 챙길 수 있습니다.


P2P 거래는 이 거래 수수료를 줄일 수 있다는 점에서 장점이 있습니다. 전자지불을 P2P로 한다면 어떤 형태일까요? 디지털 파일은 손쉽게 복사가 가능하기 때문에 현금을 넘기는 것처럼 어떤 파일을 전송하는 방식으로는 가능하지 않습니다. 해결 방법은 모두가 볼 수 있는 데이터베이스를 두어 누가 얼마를 가지고 있는지를 기록하는 것입니다. 이를 보다 자세히 이해하기 위해 필요한 개념 중 하나가 분산 원장(Distributed Ledger)입니다. 원장(Ledger)은 계좌에 입출금 내역이 기록되는 것을 연상하면 됩니다. 계좌를 처음 만들면 0원에서부터 시작해서 입출금 내역이 쭉 있고 이를 더하기 빼기하면 계좌의 현재 잔고를 알 수 있지요.


비트코인은 전세계 모든 계좌에 대한 거래를 단일한 원장으로 기록하는 프로토콜입니다. 모든 거래 기록을 모두가 처음부터 끝까지 볼 수 있습니다. 비트코인을 가지고 있다면 누구나 거래 기록을 쓸 수 있습니다. 계좌에 빗대어 표현하면 "보내는 계좌, 받는 계좌, 금액, 보내는 계좌의 인감 도장"이 있는게 하나의 거래 기록이고, 이 거래 기록을 처음부터 마지막까지 모두 기록해 두고, 이 기록을 따라 쭉 더하기, 빼기를 하고 나면 어떤 계좌에 얼마가 있는지를 알 수 있습니다. 다시 거래 얘기로 돌아와서 비트코인으로 물건을 사기 위해서는 내 인감 도장을 찍어주고, 그 거래 기록이 분산 원장에 기록되기를 기다려야 합니다. 인감 도장이 전자적으로 어떻게 동작하는지가 궁금하신 분들은 전자서명에 대해 알아보시기 바랍니다. 우리가 뱅킹앱으로 송금할 때 공인인증서 서명하는 것과 암호학적 원리는 같습니다. 

이중지불 문제 (Double spending problem)

이중지불 문제는 거래 기록이 분산되어 있을 때 발생할 수 있는 문제입니다. 예를 들어 A라는 계좌에 만원이 남아 있습니다. 이 계좌의 인감 도장을 복사해서 B와 C에 동시에 만원을 송금했다고 가정해 봅시다. 두 개 거래 중 하나는 무효가 되어야 합니다. C 입장에서는 A와 B의 사이의 거래를 알기 전까지는 돈을 받았다고 생각할 수도 있습니다. 이 시간 차를 이용해서 A의 만원을 전세계 곳곳에서 두 번, 세 번, 그 이상 쓸 수도 있습니다. 당연히 이 문제를 막아야 하고, 그러기 위해서는 거래 기록을 일렬로 세우고 거래를 확정하는 프로토콜이 필요합니다. 이를 합의 알고리즘(Consensus Algorithm)이라 하고 현재 비트코인과 이더리움의 경우 PoW(Proof of Work)를 채택하고 있습니다. PoW 프로토콜을 통해서 비트코인의 참여자 중 한 명을 랜덤으로 뽑아 트랜잭션을 블록으로 확정 짓습니다. 블록으로 만들 때에는 거래의 순서가 생기기 때문에 이중 지불을 방지하는 검증이 가능합니다. 현재 비트코인 프로토콜은 평균적으로 10분에 1개 꼴로 블록을 생성하도록 구현되어 있습니다.


여러 개의 트랜잭션(Tx)를 하나의 블록으로 만들어 이전 블록에 연결


P2P 상에서 랜덤으로 뽑기 때문에 어쩔 수 없이 블록이 동시에 생성되는 불행한 경우도 있습니다. 이 때문에 블록이 철회될 수도 있는데, 중요한 점은 PoW 프로토콜 하에서는 뒤에 이어진 블록이 많을 수록 철회될 확률이 급격하게 낮아집니다. 예를 들어 나와 관련된 거래 기록이 블록에 포함된 직후보다는 그 뒤로 블록이 5개 이상 이어진 경우가 내 거래 기록이 철회될 가능성이 훨씬 낮다는 것입니다.


이 블록이 이어진 모양을 보고 이 기술을 블록체인이라 하고, 데이터의 조작을 방지하는데 효과적으로 쓰일 수 있습니다.

현재 비트코인 결제가 어려운 이유

최근 비트코인은 그 가격이 수십 퍼센트를 왔다 갔다 하기 때문에 실질적으로 쓰일 수 없습니다. 가격이 올라간다면 아무도 쓰지 않고 가지고 있을 것이고, 가격이 떨어진다면 어느 상점도 받고 싶어하지 않을 겁니다. 다른 이유로는 거래 확정 속도가 느리다는 점이 있습니다. 약 10분에 한 번 꼴로 블록이 만들어지는데, 통상적으로 3블록~6블록을 거래 확정으로 취급하는데, 이는 바로 블록에 포함되더라도 약 30분에서 한 시간이 걸립니다. 서비스를 제공하고 나면 회수할 방법이 없는 상품에 대해서는 비트코인 결제가 현실적이지 않습니다. 그리고 앞서 P2P 거래는 거래 수수료를 줄일 수 있다고 언급했습니다. 그러나 실제로는 중간에 블록 생성자가 개입하기 때문에 블록 생성자에게 수수료를 지불해야 합니다. 대신 수수료를 누군가 독점하지 않는 차이점이 있습니다. 문제는 이 수수료를 비트코인으로 지급해야 하는데, 현재 비트코인 자체의 가격이 너무 올라 수수료가 한화로 수 천원에서 수 만원까지 한다는 것입니다.


요약하면 점심을 먹고 6000원 어치 비트코인을 결제하면, 식당 주인이 결제가 확정되지 않았다고 놔주질 않고, 결제 확정을 기다리는 동안 비트코인 가격이 올라 10000원 어치가 되어 있을 수도 있습니다. 아, 그리고 블록 생성자에게 내는 수수료 만원어치 비트코인은 별도입니다.


하지만 이런 문제가 블록체인 기술의 근본적인 한계로 보진 않습니다. 비트코인을 포함해 여러 블록체인 기술들이 사이드 체인, 새로운 합의 알고리즘 등을 통해 처리속도 및 용량을 높이기 위한 시도를 하고 있습니다. 

암호화폐 (Cryptocurrency)

비트코인은 암호화폐입니다. 거래는 암호학적인 서명에 의해 이루어지며 서명이 없으면 쓸 수 없습니다. 여기서 중앙화된 시스템과 큰 차이가 생기는데, 한 번 잃어버린 비트코인은 되찾을 수 없습니다. 만약 제가 페이코 포인트 5만원이 있는 계정을 잃어버렸다거나 해킹으로 잃어버렸다면 페이코 시스템 관리자가 포인트를 다시 쓸 수 있게 해주는 방법이 존재합니다. 그러나 비트코인은 키를 분실하거나 유출 당해 잃어버렸다면, 그 비트코인을 되찾을 수 있는 시스템적인 방법은 없습니다. 이는 해커들이 비트코인을 요구하는 이유이기도 합니다. 이런 특성은 암호화폐를 아직 제도권에서 받아들일 수 없는 이유이기도 합니다. 물론 이 역시 기술이 성숙해지면 프로토콜 추가 등의 방법으로 극복할 수 있는 부분이기도 합니다.

결론

비트코인은 기술도 초기 단계이고 현재 투기 열풍(?)으로 수수료가 비싸 전자지불 수단으로는 갈 길이 멀어보입니다. 블록체인 기술과 관련 서비스의 발전으로 극복될 수 있는 부분이지만 미래에 암호화폐가 통용되는 때가 온다면 그 중에 비트코인이 포함될 지는 역시 알 수 없습니다. :)

+ Recent posts