스마트 컨트랙트를 만드는 순서는 다음과 같다.
스마트 컨트랙트가 블록체인에 올라가면 블록체인 상의 모든 노드는 해당 스마트 컨트랙트의 바이트 코드를 가지고 있게 된다. 따라서, 각 노드, 이더리움 클라이언트는 해당 스마트 컨트랙트를 자신의 EVM에서 실행시킬 수가 있다. 한 노드에서 스마트 컨트랙트의 내용을 변경시키는 명령은 트랜잭션을 발생시켜 내용 변경이 이루어져, 다른 모드 노드가 컨트랙트에 접근하면 바뀐 내용을 얻게 된다.
다음은 스마트 컨트랙트의 간단한 예시이다. 다음처럼 코딩하면, 공용 변수에 값을 변경하고 읽는 기능을 수행할 수 있다.
pragma solidity ^0.4.8; // (1) 버전 프라그마
// (2) 계약 선언
contract HelloWorld {
// (3) 상태 변수 선언
string public greeting;
// (4) 생성자
function HelloWorld(string _greeting) {
greeting = _greeting;
}
// (5) 메서드 선언
function setGreeting(string _greeting) {
greeting = _greeting;
}
function say() constant returns (string) {
return greeting;
}
}
위 코드를 솔리티티 컴파일러로 컴파일 하면 아래와 같은 EVM 바이트 코드가 생성된다. 컴파일은 remix나 geth의 solidity 컴파일러로 할 수 있다. Remix와 Geth에 대한 부분은 추후 포스팅 에서 다룰예정이다.
0x6060604052341561000f57600080fd5b60405161046d38038061046d833981016040528080519091019050600081805161003d929160200190610044565b50506100df565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061008557805160ff19168380011785556100b2565b828001600101855582156100b2579182015b828111156100b2578251825591602001919060010190610097565b506100be9291506100c2565b5090565b6100dc91905b808211156100be57600081556001016100c8565b90565b61037f806100ee6000396000f3006060604052600436106100565763ffffffff7c0100000000000000000000000000000000000000000000000000000000600035041663954ab4b2811461005b578063a4136862146100e5578063ef690cc014610138575b600080fd5b341561006657600080fd5b61006e61014b565b60405160208082528190810183818151815260200191508051906020019080838360005b838110156100aa578082015183820152602001610092565b50505050905090810190601f1680156100d75780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34156100f057600080fd5b61013660046024813581810190830135806020601f820181900481020160405190810160405281815292919060208401838380828437509496506101f495505050505050565b005b341561014357600080fd5b61006e61020b565b6101536102a9565b60008054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156101e95780601f106101be576101008083540402835291602001916101e9565b820191906000526020600020905b8154815290600101906020018083116101cc57829003601f168201915b505050505090505b90565b60008180516102079291602001906102bb565b5050565b60008054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156102a15780601f10610276576101008083540402835291602001916102a1565b820191906000526020600020905b81548152906001019060200180831161028457829003601f168201915b505050505081565b60206040519081016040526000815290565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106102fc57805160ff1916838001178555610329565b82800160010185558215610329579182015b8281111561032957825182559160200191906001019061030e565b50610335929150610339565b5090565b6101f191905b80821115610335576000815560010161033f5600a165627a7a72305820352cec017ed93c8351ac6fbc835eda354ea6dbc9e672ae6b60c16f29c49a5cd30029
컴파일된 코드라 기계만 이해할 수 있는 코드이니 모르는게 당연하다. 이 컴파일 된 부분이 코드블록에 배포된다는 것만 알고 있으면 충분하다.
컴파일된 바이트 코드를 블록체인에 배포하는 절차이다. Remix로 소스 코드를 컴파일하면, 바이트 코드와 ABI도 자동으로 얻을 수 있다. Geth를 사용한다면, 바이트 코드와 ABI를 일일이 생성해야 한다.
contractAbiDefinition=sourceCompiled['/tmp/geth-compile-solidity602335484:HelloWorld'].info.abiDefinition
[{
constant: true,
inputs: [],
name: "say",
outputs: [{
name: "",
type: "string"
}],
payable: false,
stateMutability: "view",
type: "function"
}, {
constant: false,
inputs: [{
name: "_greeting",
type: "string"
}],
name: "setGreeting",
outputs: [],
payable: false,
stateMutability: "nonpayable",
type: "function"
}, {
constant: true,
inputs: [],
name: "greeting",
outputs: [{
name: "",
type: "string"
}],
payable: false,
stateMutability: "view",
type: "function"
}, {
inputs: [{
name: "_greeting",
type: "string"
}],
payable: false,
stateMutability: "nonpayable",
type: "constructor"
}]
Geth에서는 이런 과정을 통해 컨트랙트의 ABI를 얻을 수 있다.eth.contract
함수를 이용하면 ABI로 부터 스마트 컨트랙트 객체를 만들 수 있다. 이 단계는 일반적 코딩에서 클래스를 정의한 것과 유사하다. 실제 스마트 컨트랙트의 주소가 생성된것은 아니다.sourceCompiledContract= eth.contract(contractAbiDefinition)
{
abi: [{
constant: true,
inputs: [],
name: "say",
outputs: [{...}],
payable: false,
stateMutability: "view",
type: "function"
}, {
constant: false,
inputs: [{...}],
name: "setGreeting",
outputs: [],
payable: false,
stateMutability: "nonpayable",
type: "function"
}, {
constant: true,
inputs: [],
name: "greeting",
outputs: [{...}],
payable: false,
stateMutability: "view",
type: "function"
}, {
inputs: [{...}],
payable: false,
stateMutability: "nonpayable",
type: "constructor"
}],
eth: {
accounts: ["0x9751574414138b22986eb80ce2713cd2f5508c5c", "0xe37aa5cd578bb1ac568298e5621e11b8a8a113eb", "0xc94593e5164b6f4b5a2f9f0165c1b520858438de", "0x7a1809177f225053ed413743d7321fba8413a7b5"],
blockNumber: 1179,
coinbase: "0x9751574414138b22986eb80ce2713cd2f5508c5c",
compile: {
lll: function(),
serpent: function(),
solidity: function()
},
defaultAccount: undefined,
defaultBlock: "latest",
gasPrice: 20000000000,
hashrate: 131062,
mining: true,
pendingTransactions: [],
syncing: false,
call: function(),
contract: function(abi),
estimateGas: function(),
filter: function(fil, callback),
getAccounts: function(callback),
getBalance: function(),
getBlock: function(),
getBlockNumber: function(callback),
getBlockTransactionCount: function(),
getBlockUncleCount: function(),
getCode: function(),
getCoinbase: function(callback),
getCompilers: function(),
getGasPrice: function(callback),
getHashrate: function(callback),
getMining: function(callback),
getNatSpec: function(),
getPendingTransactions: function(callback),
getRawTransaction: function(),
getRawTransactionFromBlock: function(),
getStorageAt: function(),
getSyncing: function(callback),
getTransaction: function(),
getTransactionCount: function(),
getTransactionFromBlock: function(),
getTransactionReceipt: function(),
getUncle: function(),
getWork: function(),
iban: function(iban),
icapNamereg: function(),
isSyncing: function(callback),
namereg: function(),
resend: function(),
sendIBANTransaction: function(),
sendRawTransaction: function(),
sendTransaction: function(),
sign: function(),
signTransaction: function(),
submitTransaction: function(),
submitWork: function()
},
at: function(address, callback),
getData: function(),
new: function()
}
_greeting="Hello, World"
그 다음, 매개변수와 함께, 스마트 컨트랙트를 하나의 트랙잭션처럼 생성한다. 이 때, 트랜잭션의 송신자, 바이트 코드, 사용될 예상 gas양을 같이 입력한다. [Geth console에서 스마트 컨트랙트 배포 예시]contract= sourceCompiledContract.new( _greeting, {from:eth.accounts[0], data:sourceCompiled['/tmp/geth-compile-solidity602335484:HelloWorld'].code gas:'4700000'})
{
abi: [{
constant: true,
inputs: [],
name: "say",
outputs: [{...}],
payable: false,
stateMutability: "view",
type: "function"
}, {
constant: false,
inputs: [{...}],
name: "setGreeting",
outputs: [],
payable: false,
stateMutability: "nonpayable",
type: "function"
}, {
constant: true,
inputs: [],
name: "greeting",
outputs: [{...}],
payable: false,
stateMutability: "view",
type: "function"
}, {
inputs: [{...}],
payable: false,
stateMutability: "nonpayable",
type: "constructor"
}],
address: undefined,
transactionHash: "0x99ddfd763478ce7a0d328fbc67f3c10fec377efa18a8e9c41f61321feb836cd1"
}
스마트 컨트랙트 배포 예시의 마지막에는, address: undefined
라는 부분을 볼 수 있다. 이 트랜잭션이 채굴을 통해 블록체인에 등록되지 않았기 때문에, 컨트랙트의 주소값이 비어있는 상태이다. 마이너가 트랜잭션이 포함된 블록을 채굴하게 되면 비로소 스마트 컨트랙트 주소가 생성된다.
{
address: "0x6f9c338bb987f1baf619697784c9457b9afa119c",
transactionHash: "0x99ddfd763478ce7a0d328fbc67f3c10fec377efa18a8e9c41f61321feb836cd1",
allEvents: function(),
greeting: function(),
say: function(),
setGreeting: function()
}
위 예시에서 address: "0x6f9c338bb987f1baf619697784c9457b9afa119c"
이 부분이, 생성한 스마트 컨트랙트의 주소값이다.
위에서 생성한 스마트 컨트랙트의 트랜잭션 내용을 살펴보겠습니다. 위에서 생성한 트랜잭션의 해시값은 transactionHash:"0x99ddfd763478ce7a0d328fbc67f3c10fec377efa18a8e9c41f61321feb836cd1"
이다. 이 트랜잭션의 내용을, Geth 콘솔에서 확인할 수 있다. 트랜잭션 해시와 스마트 컨트랙트의 주소값은 전혀 다른 값 이다.
아래의 예시를 보면 일반적인 송금과는 달리, 트랜잭션의 input:에 값이 있다. 이 것이 바로 스마트 컨트랙트 소스 코드를 컴파일한 바이트 코드이다. 일반적인 거래에서는 바이트 코드가 필요없기 때문에 input: 0x
로 나타난다. 그리고 수신자가 없다. 즉, 송신자만 있고, 수신자는 없다. 스마트 컨트랙트는 이것을 생성한 송신자가 특정 수신을 지정하지 않고 자신이 생성한계약 코드를 블록체인에 올린 것이기 때문에 당연한 현상이다
> eth.getTransaction("0x99ddfd763478ce7a0d328fbc67f3c10fec377efa18a8e9c41f61321feb836cd1")
{
blockHash: "0x2691dcfbd09febcd6d60537386974ba2147ef2ada4e7693a541000442aa5d91d",
blockNumber: 1230,
from: "0x9751574414138b22986eb80ce2713cd2f5508c5c",
gas: 4700000,
gasPrice: 20000000000,
hash: "0x99ddfd763478ce7a0d328fbc67f3c10fec377efa18a8e9c41f61321feb836cd1",
input: "0x6060604052341561000f57600080fd5b60405161046d38038061046d833981016040528080519091019050600081805161003d929160200190610044565b50506100df565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061008557805160ff19168380011785556100b2565b828001600101855582156100b2579182015b828111156100b2578251825591602001919060010190610097565b506100be9291506100c2565b5090565b6100dc91905b808211156100be57600081556001016100c8565b90565b61037f806100ee6000396000f3006060604052600436106100565763ffffffff7c0100000000000000000000000000000000000000000000000000000000600035041663954ab4b2811461005b578063a4136862146100e5578063ef690cc014610138575b600080fd5b341561006657600080fd5b61006e61014b565b60405160208082528190810183818151815260200191508051906020019080838360005b838110156100aa578082015183820152602001610092565b50505050905090810190601f1680156100d75780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34156100f057600080fd5b61013660046024813581810190830135806020601f820181900481020160405190810160405281815292919060208401838380828437509496506101f495505050505050565b005b341561014357600080fd5b61006e61020b565b6101536102a9565b60008054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156101e95780601f106101be576101008083540402835291602001916101e9565b820191906000526020600020905b8154815290600101906020018083116101cc57829003601f168201915b505050505090505b90565b60008180516102079291602001906102bb565b5050565b60008054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156102a15780601f10610276576101008083540402835291602001916102a1565b820191906000526020600020905b81548152906001019060200180831161028457829003601f168201915b505050505081565b60206040519081016040526000815290565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106102fc57805160ff1916838001178555610329565b82800160010185558215610329579182015b8281111561032957825182559160200191906001019061030e565b50610335929150610339565b5090565b6101f191905b80821115610335576000815560010161033f5600a165627a7a72305820352cec017ed93c8351ac6fbc835eda354ea6dbc9e672ae6b60c16f29c49a5cd300290000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000c48656c6c6f2c20576f726c640000000000000000000000000000000000000000",
nonce: 1,
r: "0x7e7bbc34563f0a6e7d5113344eaea0761d4edf815d41ac752d3cb33b4f65fb43",
s: "0x61a968fcab4935d0f17b67a5274c31ed2a499ccbc5aa605a9080192a940834e9",
to: null,
transactionIndex: 0,
v: "0x1b",
value: 0
}
그러면, 이 스마트 컨트랙트가 포함된 블록(blockNumber: 1230)이 채굴된 상태에서 블록 정보를 살펴 보자.
[Geth console에서 배포된 스마트 컨트랙트가 포함된 블록 정보 예시]
eth.getBlock(1230)
{
difficulty: 234190,
extraData: "0xd783010505846765746887676f312e362e32856c696e7578",
gasLimit: 40355674,
gasUsed: 293248,
hash: "0x2691dcfbd09febcd6d60537386974ba2147ef2ada4e7693a541000442aa5d91d",
logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
miner: "0x9751574414138b22986eb80ce2713cd2f5508c5c",
mixHash: "0xdd2c7606df86047b0c769a375074f4630a850345a63a8236a8b0f8628a9ba43e",
nonce: "0x1f1450b847e902d0",
number: 1230,
parentHash: "0xf72c4cc5ec6c6ce1ad477396cc1dfcf81a5028844d0b21aa563fb651cf9c62e1",
receiptsRoot: "0xff4331768f55966c79f25c49800b70572d62856ab00960371b411430a022d05e",
sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
size: 1858,
stateRoot: "0xf39ba07306e185b8fc5a66fd54de9aef311fd00e59f268453cdde7db4d121f29",
timestamp: 1515859762,
totalDifficulty: 218484208,
transactions: ["0x99ddfd763478ce7a0d328fbc67f3c10fec377efa18a8e9c41f61321feb836cd1"],
transactionsRoot: "0x63e1a02b6d3a89fe118f481e5ab23093d52e89d475a9ff83e4dbb37c3ce3c7a2",
uncles: []
}
앞서 생성한 스마트 컨트랙트가 블록체인에 올라갔으니, 이제 스마트 컨트랙트 주소를 이용하여 정보를 읽고 쓰고 할 수 있다. geth 콘솔에서 새로운 컨트랙트 객체를 만드는데 이 때는 기존에 존재하는 컨트랙트의 주소를 이용한다. 여기서는 앞에서 생성한 스마트 컨트랙트의 주소값인0x6f9c338bb987f1baf619697784c9457b9afa119c
를 이용하여 contract2라는 객체를 만든다.
contract2= eth.contract(contractAbiDefinition).at("0x6f9c338bb987f1baf619697784c9457b9afa119c")
{
abi: [{
constant: true,
inputs: [],
name: "say",
outputs: [{...}],
payable: false,
stateMutability: "view",
type: "function"
}, {
constant: false,
inputs: [{...}],
name: "setGreeting",
outputs: [],
payable: false,
stateMutability: "nonpayable",
type: "function"
}, {
constant: true,
inputs: [],
name: "greeting",
outputs: [{...}],
payable: false,
stateMutability: "view",
type: "function"
}, {
inputs: [{...}],
payable: false,
stateMutability: "nonpayable",
type: "constructor"
}],
address: "0x6f9c338bb987f1baf619697784c9457b9afa119c",
transactionHash: null,
allEvents: function(),
greeting: function(),
say: function(),
setGreeting: function()
}
contract2를 이용하여 스마트 컨트랙트의 공유 변수 값을 바꿀 수 있다
> contract2.setGreeting.sendTransaction( "Hello, Ethereum", {from:eth.accounts[0], gas:1000000})
"0xa6b71f81b5d6d5c71248afb0e89f34aa2e0e52f98e353899bf80166b072fed36"
contract2를 통해서 스마트 컨트랙트의 공유 변수 값을 바꾸는 것도 하나의 트랙잭션을 생성한다. 위 명령의 결과로 트랜잭션 "0xa6b71f81b5d6d5c71248afb0e89f34aa2e0e52f98e353899bf80166b072fed36"
이새롭게 생성되었다. 이 트랙잭션이 포함된 블록이 채굴되면, 스마트 컨트랙트의 공유 변수 값이 바뀌게된다
contract, contract2에서 공유 변수를 접근해 보면, 변경된 값이 동일하게 읽어와 진다. 다시 말해, 복수의 사용자가 각각 스마트 컨트랙트 객체를 만들고 해당 스마트 컨트랙트에 접근하여 값을 변경하거나 읽어 올 수 있다. 이 때, 스마트 컨트랙트에 쓰는 것는 것은 트랜잭션을 발생시키지만, 값을 읽어 오는 것은 별도의 트랙잭션이 발생하지 않는다. 다음과 같이 공유 변수 값을 읽기만 하는 경우는 트랜잭션이 발생하지 않는다. 읽는 작업은 거래라고 보기 어렵기 때문이다.
> contract.say.call()
"Hello, Ethereum"
> contract.setGreeting.sendTransaction( "Hello, Bitcoin", {from:eth.accounts[0], gas:1000000})
"0xe3878aa2689efc199f1159eb8c839882206405c5fb9d3c5eebfbe719a6b49d44"
이더리움에서 스마트 컨트랙트는 새로운 스마트 컨트랙트를 생성하거나, 특정 스마트 컨트랙트상의 함수를 실행하거나, 이더를 전송하는 방식중의 하나로 실행이 된다. 또한 사용자 어카운트(EOA)에 의해서 발생한 트랜잭션이나 다른 컨트랙트에 의해서만 실행된다.
또 무한반복같은 악의적인 코드를 막고, 데이터의 무결성를 지키기 위해 모든 트랜잭션을 실행할 때 해당 트랜잭션의 실행 비용을 지급하도록 규정하고 있다. 그리고 모든 트랜잭션의 기본 실행 비용은 21,000 가스이다. 트랜잭션 실행 비용에는 발송자 어카운트 주소에 대한 ECDSA(Elliptic Curve Digital Signature Algorithm)를 위한 비용과, 트랜잭션 저장을 위한 스토리지 비용, 네트워크 대역폭 비용이 포함된다. 이렇게 스마트 컨트랙트 실행시 비용을 지불하도록 정의하여, 디도스 공격 같은 무한 실행 공격처럼, 악의적인 의도로 컨트랙트를 실행하는 걸 방지할 수 있다.
스마트 컨트랙트간의 호출은 메시지라는 특별한 구조체를 사용하여 호출된다. 메시지는 외부 어카운트(EOA)가 아니라 컨트랙트 어카운트(CA)에 의해서만 생성되며, 함수 호출시에 다른 컨트랙트로 전달된다. 메시지는 트랜잭션과는 달리 EVM 내부에서만 존재하기 때문에 가스비용이 발생하지 않는다.
다음은 메시지의 구조이다. 메시지는 EVM내에서 컨트랙트를 실행하기 위해서 Call
, CallCode
, DelegateCall
, StaticCall
등이 호출될때에 생성된다. 이들 Call
코드들은 공통적으로 컨트랙트 주소를 매개변수로 전달받아 이를 실행하고 처리한다.
[코드] Message 구조 예시
type Message struct {
to *common.Address // 메시지 수신처
from common.Address // 메시지 발신처
nonce uint64 // 거래 실행시 수행되도록 허용된 최대 트랜잭션 수행횟수
amount *big.Int // 메시지와 함께 전달되는 이더(wei 단위)
gasLimit uint64 // 트랜잭션 수행시 소비될 총 가스량에 대한 추정치
gasPrice *big.Int // 가스 가격
data []byte // 매개변수 전달시 사용되는 데이타 필드(Optional)
checkNonce bool
}
다음과 같이 Call
, CallCode
, DelegateCall
이 호출될때에 메시지가 만들어진다
contract D {
uint public n;
address public sender;
function callSetN(address _e, uint _n) {
_e.call(bytes4(sha3("setN(uint256)")), _n); // E's storage is set, D is not modified
}
function callcodeSetN(address _e, uint _n) {
_e.callcode(bytes4(sha3("setN(uint256)")), _n); // D's storage is set, E is not modified
}
function delegatecallSetN(address _e, uint _n) {
_e.delegatecall(bytes4(sha3("setN(uint256)")), _n); // D's storage is set, E is not modified
}
}
contract E {
uint public n;
address public sender;
function setN(uint _n) {
n = _n;
sender = msg.sender;
// msg.sender is D if invoked by D's callcodeSetN. None of E's storage is updated
// msg.sender is C if invoked by C.foo(). None of E's storage is updated
// the value of "this" is D, when invoked by either D's callcodeSetN or C.foo()
}
}
contract C {
function foo(D _d, E _e, uint _n) {
_d.delegatecallSetN(_e, _n);
}
}
이더리움에서 스마트 컨트랙트는 Solidity 언어로 프로그래밍한다. solidity 언어로 프로그래밍된 스마트 컨트랙트는 컴파일러(solc)에 의해 바이트 코드로 컴파일되고, 컴파일된 바이트코드는 블록에 포함되어, EVM(Ethereum Virtual Machine)에 의해 실행된다.
이전 포스팅에서 다뤘던 바와같이 EVM(Ethereum Virtual Machine)은 이더리움 스마트 컨트랙트의 바이트 코드를 실행하는 32바이트 스택 기반의 실행환경으로 스택의 최대 크기는 1024바이트이다. 이더리움의 각 노드는 EVM을 포함하고 있으며, EVM을 통해 바이트 코드를 OP코드로 변환하고 스택기반으로 각각의 OP코드를 실행한다.
EVM은 휘발성, 비휘발성 메모리로 구성되어 있으며, 여기에 바이트 배열 형태로 스택의 항목들을 저장한다.
EVM은 바이트코드를 내부 OP 코드로 재해석한다. 즉, solidity 언어로 개발된 스마트 컨트랙트의 컴파일된 바이트 코드는 EVM에서 OP 코드로 치환하여 실행한다.
1+2
를 계산하는 바이트 코드를 가지고 EVM의 동작을 알아보도록 하자.1+2
를 계산하는 코드의 바이트코드는 6001600201
이다. 이 바이트코드를 치환하여 OP 코드로 분리하면 0x60, 0x01, 0x60, 0x02, 0x01
이다.
OP 코드표를 참고로 하면, 0x60
은 PUSH OPCODE를 의미하고, 0x01
, 0x02
는 값 1, 2를 의미하고, 마지막 0x01
은 ADD OPCODE
를 의미한다. 즉 EVM의 스택에 1,2를 Push하고 Add
연산을 수행하라는 의미이다.
이 챕터에선 콘텐츠에서는 노드끼리의 통신을 다룬다.
P2P는 인터넷에 연결된 다수의 개별 사용자들이 중개기관을 거치지 않고 직접 데이터를 주고받는 것으로, 네트워크에 중앙 컨트롤러가 없고, 모든 참여자가 서로 직접 소통한다. P2P 방식은 기존의 서버-클라이언트 방식의 데이터 전송과는 본질적으로 다른 구조를 가지고 있다. 서버-클라이언트 구조에서 개별적인 참여자는 우선 서버에 데이터를 올려야 하고, 다른 참여자가 해당 서버로부터 데이터를 받아오는 방식으로 작동한다. 그러나 P2P에서는 모든 노드가 클라이언트이면서, 동시에 서버의 역할을 수행한다.
💡 블록체인은 P2P 방식으로 운영된다.
기존의 클라이언트-서버 모델은, 종단의 노드의 수가 늘어날수록 최소 분배 시간이 선형적으로 증가한다. 기존의 모델과 비교하여, P2P 파일 시스템은 노드가 확장되더라도 최소 분배 시간이 로그함수 형태로 증가합니다.
분배 시간(distribution time)은 모든 N개의 피어들이 파일의 복사본을 얻는 데 걸리는 시간이다. 최소 분배 시간은 모든 피어가 파일의 복사본을 얻는 데에 필요한 최소한의 값을 말한다. 분배 시간이 작을수록, 모든 피어가 동일한 파일의 복사본을 가지는 시간이 적게 들고, 따라서 하나의 파일을 모든 피어가 가지는 속도가 빨라진다.
P2P 파일 시스템으로는 비트토렌트가 가장 대표적이다.
RPC(Remote Procedure call)란, 별도의 원격 제어를 위한 코딩 없이 다른 주소 공간에서 리모트의 함수나 프로시저를 실행 할 수 있게 해주는 프로세스간 통신이다.
RPC 모델은 분산컴퓨팅 환경에서 많이 사용되었으며, 현재에는 MSA(Micro Software Archtecture)에서 마이크로 서비스 간에도 많이 사용되는 방식이다. 서로 다른 환경이지만 서비스간의 프로시저 호출을 가능하게 해줌에 따라, 언어에 구애받지 않고 환경에 대한 확장이 가능하며, 좀 더 비지니스 로직에 집중하여 생산성을 증가시킬 수 있다.
JSON-RPC는 무상태성(stateless)의 경량 RPC(원격 프로시저 호출) 프로토콜이다. Spec(사양; specification)에는 여러 데이터 구조와 처리 규칙을 정의하고 있다. 소켓 통신이나 HTTP 등 다양한 메시지 전달 환경에서 사용할 수 있도록 추상화되어 있다. 이 프로토콜은 JSON(RFC 4627)의 데이터 형식으로 사용한다.
JSON-RPC API를 통해 이더리움 노드와 직접 상호작용할 수도 있다. 그러나 dapp 개발자를 위한 더 편리한 옵션도 있다.이미 자바스크립트와 백엔드를 위한 API 라이브러리가 많이 존재한다. 이런 라이브러리를 통해 개발자는 (원리를 잘 모르더라도), 직관적인 한 줄의 메서드로 JSON-RPC API를 사용할 수 있다.
Web3.js는 대표적인 이더리움 자바스크립트 API 이다.
인프라란?
일상생활에서 사용되는 구글, 유튜브 등등 이런한 서비스를 어디에서 방대한 데이터를 주고 받을까? 그리고 이런 데이터는 어떻게 처리해서 우리에게 보여줄까? 이런 것들을 처리하고 보여주고 관리하는 환경을 인프라라고 한다.
아키텍처란?
아키텍처란 무엇일까? 구조를 뜻한다. 위에서 언급한 인프라에서 데이터를 관리하고 처리하는 구조에는 무엇이 있을까?4가지로 나눌 수 있다.
https://velog.io/@ssdii44/1장.-인프라-아키텍처#인프라란
CAP 이론은 분산 데이터베이스 시스템을 바탕으로, 네트워크 분할 허용(Network Partition)이 이루어지는 경우에 일관성(Consistency), 가용성(Availability)을 동시에 만족하는 것은 불가능하며, 이들 사이에 양자택일해야 한다는 이론이다.
CAP 이론에 따르면 모든 분산 네트워크는 일관성을 지키는 상황 (CP)과 가용성을 지키는 상황(AP)으로 나눌 수 있다고 보았다.
모든 노드는 데이터를 업데이트하는 과정에서 다른 작업이 불가능하다. 따라서 업데이트가 진행되는 시간에는 일시적으로 데이터베이스에 접속할 수 없게 되어 서비스가 중지된다. 모든 노드에 정보를 업데이트하여 일관성을 지키되 가용성을 포기하는 게 맞을까? 혹은 일부 노드에만 정보를 업데이트하여 가용성을 지키되 일관성를 포기할 것인가? 에 대한 문제가 CAP 이론이다.
💡 이해를 돕기 위한 Not Linearizable한 시스템을 시나리오
철수는 근처 펍에 가서 2018 월드컵 조별리그, 한국 대 독일의 경기를 보고있다.
영희는 한국이 이기지 못할 것으로 생각하고, 학교에서 시험 공부를 하고있다.
경기가 끝나고 한국이 2:0 으로 이겼다.
철수는 감격한 나머지 포털 사이트에 접속해서 경기 결과를 재확인했다.
철수는 영희에게 카카오톡으로 메시지를 보내 경기 결과를 알려주었다.
영희는 포털 사이트에 접속했지만.영희가 접속한 포털 사이트에는 여전히 게임이 진행되고 있다라는 표시가 나타난다. 철수는 최신 업데이트가 된 정보를 받을 수 있었다. 하지만 영희는 최신 업데이트가 된 정보를 받지 못했다.
즉, 가용성을 우선시하고 Linearizable 한 일관성을 포기한 것이다.
💡 CAP 이론에서 의미하는 가용성은 일반적인 가용성과 의미가 다르다
CAP 이론에서는 모든 노드로부터 응답을 받을 수 있어야 하는 반면에 일반적인 가용성에선 정상적으로 작동하고 있는 노드 중 일부만 응답을 받을 수 있어도 가용성이 있다고 본다.
또 일반적인 가용성은 현실적인 응답시간(realistic response time)을 고려하지만 CAP 이론의 가용성은 응답시간에 대한 제한을 걸어두지 않는다는 문제가 있다. 아무리 늦게 응답을 보내도 받기만 하면 충분하다는 것 이다.
블록체인에서 네트워크 분할은 분기라는 논리적 사건을 이유로 발생하기도 한다.
비슷한 시점에 같은 높이의 블록이 채굴되거나 새로운 원장이 제시될 경우 분기가 발생함
그러나 시간이 지남에 따라 포크 선택 규칙(합의알고리즘)에 의해 하나로 합의하게 된다 이 점을 미루어 보아 CAP 관점에서의 블록체인은 가용성과 분할 허용을 충족하는 AP 시스템이다.
통상의 AP 시스템은 고성능을 위하지만 블록체인 시스템은 자가 제한을 통해 성능을 제한한다는 점이 특이한 점이다. 대신 이를 통해 과거 블록의 위/변조를 어렵게 만들고 궁극적 일관성을 확보했다.
CAP 이론에 따르자면 일관성과 가용성은 완전한 대립 관계에 있지만, 사실 완벽한(perfect) CP 시스템과 AP 시스템은 쓸모가 없다. 그리고 완벽한 CA 시스템은 가질 수 없습니다.
실제로는 일관성과 가용성의 절충안에 해당하는 연속적인 중간 영역이 존재한다. 이러한 중간 영역을 고려하고 네트워크의 정상 상황과 분할 상황을 모두 담아낼 수 있는 표현법으로 Daniel Abadi가 제시한 PACELC 이론이 있다.
[그림] 네트워크 상황을 중심으로 한 PACELC 이론의 다이어그램. 붉은색 선은 시스템의 정도를 나타낸 것이다.
샤딩(sharding) 데이터 저장방법중 하나로, 단일의 데이터를 다수의 데이터로 쪼개어 나누는 걸 말한다, 단일의 데이터베이스에서 저장하기 너무 클 때 사용하여 데이터를 구간별로 쪼개어 나눈다. 이를 통해 노드에 무겁게 가지고 있던 데이터를 빠르게 검증할 수 있어, 트랜잭션 속도를 향상시킬 수 있다. 샤딩을 통해 나누어진 블록들의 구간(epoch)을 샤드(shard)라고 부른다.
블록체인에서는 검증자를 소규모 그룹으로로 분리해 각 그룹이 서로 다른 트랜잭션을 동시다발적으로 처리하는 방식이다. 즉, 하나의 데이터를 샤드라는 단위로 조각내고, 네트워크를 통해 분산하여 저장해(수평 분할) 이를 병렬적으로 처리하여 블록체인에 확장성을 부여한다.
만약 10만큼의 데이터와 10명의 노드가 참여했다고 가정한다면 기존의 블록체인은 10명의 노드 개개인은 10만큼을 모두 가지고 있으면서 공유한다. 그러나 샤딩은 10을 조각내서 10명의 노드 개개인은 1만큼씩만 보관함으로써 보관 데이터가 가벼워져 거래처리 속도가 크게 향상된다.
블록체인 시스템의 확장성 문제를 해결하기 위해 등장한 기술이지만, 샤딩을 적용할 경우 한 샤드 내에서의 전송이 아닌 여러 샤드 간의 전송은 절차가 훨씬 복잡하고 느려진다. 여러 샤드로 쪼갤수록 인터샤드 트랜잭션은 확률적으로 많아지는데 각 샤드는 자기 샤드의 데이터만 있고, 다른 샤드의 데이터는 가지고 있지 않으므로 샤드간 데이터를 어떻게 참조할 것인지, 어떻게 검증할 것인지 문제가 생겨서 알고리즘이 복잡해진다.
그리고 샤드마다(분할된 네트워크=노드 그룹) 트랜잭션의 빈도, 노드의 수, 밸리데이터의 비율 등에서 차이가 나기 때문에 한 번 샤드가 정해진 다음에도, 샤드의 구성원을 재배치하여 샤드간 균형을 맞추는 알고리즘이 필요하다. 뿐만 아니라, 예상했던 것보다 전체 트래픽이 높아질 경우, 처음 설정해둔 샤드의 수를 늘릴 필요가 있다. 한 번 쓰여진 데이터를 해쉬나 서명으로 묶어 위변조나 조작을 하지 못하도록 만들어진 분산 원장 구조에서는 샤드를 중간에 추가하는 등의 동적 샤딩 기술을 구현하기가 매우 어렵다.
공개형 블록체인의 맥락에서, 네트워크에 올려진 트랜잭션은 네트워크상의 서로 다른 노드들로 이루어진 여러 샤드(shards)로 분할된다. 각각의 노드는 들어오는 트랜잭션들의 일부만을 처리할 수 있게 되고, 네트워크상에서 병렬식으로 다른 노드들에서도 똑같이 실행된다. 따라서, 네트워크를 여러 샤드로 쪼개면 동시에 더 많은 트랜잭션을 처리하고 증명할 수 있어 네트워크가 커질수록 점점 더 많은 트랜잭션 처리가 가능하게 되는 속성을 가진다. 이 속성을 병렬식 확장(horizontal scaling)이라고도 한다.
네트워크가 확장할수록 처리량이 증가하면, 샤딩의 이 독특한 특성은 블록체인의 빠른 채택을 촉진하는 이상적인 촉매제가 될 수 있다.