400라인의 go코드로 구현한 하이퍼레저 패브릭 [1] - 전체조망 

몇일전에 200라인으로 구현한 블록체인 (golang) 을 우연히 알게 됬는데 해당 블로그 주인은 블록체인/네트워크/P2P/POW/IPFS 등 다양하게 구현해 놓았습니다. 초창기 블록체인에 대한 이해가 잘 안갔을때 저렇게 간략히 구현된 코드를 보고 오히려 이해가 잘 됬던 경험이 떠올라 그렇다면 하이퍼레저 패브릭을 간단하게 구현해 놓으면 누군가에겐 도움이 될 수 있지 않을까 하여 코드를 구현 해 봤습니다.

왜 golang 냐구요? 
예전에 스칼라 기반의 Akka로 코딩을 즐겁게 한 기억이 있는데, go의 동시성 모델은 더욱 심플하고 자유로우며 훨씬 더 큰 재미가 있었습니다. 그리고 최근엔 많은 서비스에서 golang이 채택되고 있으며 특히 하이퍼레저 패브릭과 이더리움이 Go로 만들어져 있습니다.  :-) 

처음에는 유행(?) 에 따라서 200라인으로 짜려고 했는데 구성요소들이 너무 많아 도저히 불가능 하였으며, 400라인 정도지만 중요한 구성요소들은 시뮬레이션 구현 하였습니다. 수만라인 이상의 코드를 400라인으로 압축시켜 놓았기 때문에 디테일한 부분은 많이 생략되었으며, 작동방식의 차이도 있습니다. 하나의 파일에 모두 구현하고자 각자 모듈로 나누어져서  TCP스트리밍이나 RPC 로 커뮤니케이션하는 부분을 없애고 하나의 프로세스안에서 돌아가게 하였습니다. 글 마지막쯤에 완성도를 높히기 위해 추가하여야 할 것들을 정리 하였으니 참고하세요.


500,000 TPS!!

이 간략한 블록체인 시스템은 제 노트북에서 500,000tps가 나왔는데요, 대단하다구요? 전혀 아닙니다. 

1. 네트워킹 부하
2. 체인코드(스마트컨트랙트) 실행 부하
3. 암호화/복호화/서명/인증 부하
4. CFT(crash fault tolerance) 를 위한 부하 

위의 주요4가지 부하가 없어서 그런데요. 이 4가지말고 가장 중요한 부하가 또 하나 있는데, 그게 신뢰의 비용입니다.비트코인에서는POW의 비용이 가장 크거든요. 이렇듯 속도가 빠르다고 자랑하는 블록체인 시스템들은 신뢰의 비용을 줄였다고 생각하면 됩니다. 이더리움처럼 신뢰 비용을 최대한 줄이지 않으면서 속도를 빠르게 하려는 도전을 
묵직하게 하는 모습은 그런면에서 참 대단합니다. 


- 하이퍼레저 패브릭이란?

아키텍쳐

하이퍼레저 패브릭은 콘소시엄 블록체인으로 허가된 조직이 체인에 참여하여 신뢰를 구축하는 모양새를 가집니다. 하이퍼레저 패브릭을 적용하기 위해서는 먼저 해당 분야에 "조직" 이라는 네트워크가 존재해야하며, 그 "조직" 간에 어떠한 신뢰 비용이 생기는지 대략적으로 라도 파악 할 수 있어야 합니다. 그런 조직들 간의 신뢰비용을 낮추기 위해 사용되는 분산저장소라고 생각하시면 되요.

                                                               (이미지 - packtpub 출판사에서 참조)

 따라서 전반적인 구성은 일반 퍼블릭 블록체인보다는 다양하며, 해결해야하는 코어 문제는 퍼블릭 블록체인보다는 비교적 쉽지만 응용SI를 하기 위해서는 굉장히 복잡해 보이는게 사실 입니다. 400라인의 코드안에는 위의 도식에서 보이는 Endorse / Commit Peer, Orderer , Fabric-CA, Ledger, Bockchain, WorldState등이 간단히 구현되어 어떻게 서로 유기적으로 작동하는지 살펴 볼 수 있는데요 하나씩 간략하게 설명하면 

* Endorse Peer -  클라이언트가 발생시킨 트랜잭션을 계산/보증한 후 리턴
* Commit  Peer -
  Orderer가 보내준 블럭을 장부(블록체인 및 상태저장소) 에 기록
* Orderer           -
보증된 트랜잭션(Read/Write Set) 을 받아서 정렬 한후 블록으로 만들어 Commit Peer에 전달.
* Kafaka            -
  Orderer를 도와서 정렬작업을 합니다. 
* Fabric CA       -
  각 Peer 와 사용자(조직)등에 대한 암호화 재료를 만들어 주며, CA의 역할을 합니다.
* MSP               -  
각 조직및 사용자에 대한 신원검증을 처리 합니다.
* Ledger            -  append only인 블록체인과 상태저장소를 가지고 있습니다.
* LevelDB            -  key,value 맵으로 상태를 저장하고 있습니다.

합의시스템

하이퍼레저도 블록체인이기 때문에 합의 시스템이 있답니다.



실행/보증-정렬-검증/저장 의 3단계를 거치는 합의가 하이퍼레저 패브릭의 기본 뼈대 합의를 이루며, 가운데의 Ordering부분을 여러가지 다양한 합의체로 구현하여 Pluggable 하게 붙힐 수 있는데요, Ordering부분의 합의는 신뢰의 합의는 아닙니다. 그저 Crash Fault Tolerance적인 합의에요.

- Hyperledger fabric 400 구성도 

제 코드의 주요 흐름은 2가지로 장부에 쓰기/읽기가 있습니다. 아래에 모듈간의 흐름을 그려보았는데요.

쓰기 순서
브라우저에서 신용장을 만들고 싶어서 해당 데이터를 백엔드로 보냅니다. 
벡엔드에서는 하이퍼레저 패브릭 시스템과 통신할 수 있는 SDK를 호출 합니다. 이 부분이 코드에서 미들웨어입니다.
1. 미들웨어는 Endorsing Peer들에게 트랜잭션을 요청함
2. Endorsing Peer 들은 해당 트랜잭션에 대한 체인코드를 호출하여 실행하고 결과값(RWSet)을 미들웨어로 돌려줌
3.  미들웨어는 RWSet을 받아서 보증에 대한 확인후 이상없으면 Orderer에 RWSet을 보냅니다.
4. Orderer들은 받은 트랜잭션을  Kafka의 채널에 Push하고 , Pull 하여 정렬합니다.
5. Orderer는 정렬된 트랜잭션 모음을 받아서 블록으로 가공합니다.
6. 가공된 블록을 Commit Peer로 보내고
7. Commit Peer는 블록을 검증하고 Ledger (Blockchain + State Storage)에 저장합니다.

읽기 순서
브라우저에서 신용장을 읽고 싶어서 해당 데이터를 백엔드로 보냅니다. 
벡엔드에서는 하이퍼레저 패브릭 시스템과 통신할 수 있는 SDK를 호출 합니다. 이 부분이 코드에서 미들웨어입니다.
1. 미들웨어는 Commiting Peer에게 트랜잭션을 요청함
2. Commit Peer는 Ledger (State Storage)에서 정보를 가져옵니다.
3. 미들웨어(클라이언트)에 반납합니다.

- 주요 코드를 살펴봅시다.

func WriteTrans(key string, value string) string {
rwset1, rwset2 := fabric.WriteTranaction(key, value, fabric.MSP_org1)
if rwset1.msp == fabric.MSP_peer1 && rwset2.msp == fabric.MSP_peer2 {
msps := [] string {rwset1.msp , rwset2.msp}
rwset := RWSet{ key:key, value: value, peers_msp: msps}
fabric.SendToOrderer(rwset)
return "ok"
}

return "failed"
}

미들웨어(client)에서 fabirc으로 저장정보와 자신의 신원증명을 매개변수로 트랜잭션을 일으킨 후 에 RWSet을 받아와서 각 피어들의 보증정보를 확인 후에 오더러로 전송합니다.(오더러 중에 살아있는 녀석으로 보냅니다.) fabric내부에서는 라운드로빈방식으로 각 오더러로 분배합니다.


func (o *Orderer) consumer() {
go func() {
for {
rwsets := o.kafka.Pull()
if rwsets == nil {
runtime.Gosched()
continue
}
newBlock := o.createBlock(rwsets)
for _, committer := range o.committer {
committer.addblock <- newBlock
}
}
}()
}

카프카를 통해서 받은 정렬된 트랜잭션을 가지고 임시블럭을 만든 후에 committing peer로 전송합니다. 


func (p *Peer) committing() {
go func() {
for {
select {
case block := <-p.addblock:
ok := p.validating(block)
if ok == false {
continue
}
for _, trans := range block.Trans {
p.ledger.setState(trans)
}
p.ledger.addBlock(block)
case <-p.peer_done:
return
}
}
}()
}

Orderer가 보낸 임시블럭을 받아서 검증하고 각각의 트랜잭션을 StateStorage에 저장하고 블록체인에도 연결해줍니다. 

func (l *Ledger) addBlock(block Block) {
ledger_mutex.Lock()
prevBlock := l.Blockchain[len(l.Blockchain)-1]
newBlock := l.generateBlock(prevBlock, block)
l.Blockchain = append(l.Blockchain, newBlock)
spew.Dump(newBlock)
ledger_mutex.Unlock()
}

리얼블록을 만들어서 이전 블록에 연결되고 있습니다.

- 추가 해야 할 것들 

@ 각 구성요소들을 모듈로 나누고 각각의 프로세스로 돌게 함.
@ 각 구성요소들간에 통신을 위한 TCP streamming 과 RPC를 구현한다.
@ 암호화 통신/프로토콜 정의/피어간 가쉽프로토콜 추가
@ 채널,조직,콘소시엄등에 대한 추상개념 적용 
@ Fabric-CA가 제대로된 PKI 기능을 하게 기능을 추가하고, 각 구성요소들은 MSP를 통해 검증기능을 갖게함
@ 체인코드 해석기
@ Order에 대한 합의알고리즘 구현 (kafaka방식/Raft방식/BFT방식)
@ LevelDB or CouchDB를 통한 상태저장
@ 커맨드라인옵션처리/Config/로깅/에러처리/주석/문서/도커배포 
@ 등등 

- 구현 코드 
https://github.com/wowlsh93/hyperledger-fabric-400

- 다음 시리즈 
400라인의 go코드로 구현한 하이퍼레저 패브릭 [2]- 블록전파/Gossip 프로토콜


   

이더리움의 P2P에서 리모트 피어와 메세지를 읽고/쓸때에는 위의 그림처럼 peer ( 이더리움에서 peer객체는p2p 와 eth에 각각있으며, eth의 peer 는 위 그림의 peer 와 protoRW를 포함한다) 를 통하는데, peer객체는 읽고/쓰기를 rlpxFrameRW를 통해서 한다. 이 글에서는 rlpx의 transport부분은 빼고 rlpxFrameRW를 살펴 볼 것이다.

func (pm *ProtocolManager) handleMsg(p *peer) error {
msg, err := p.rw.ReadMsg()
switch {
case msg.Code == GetBlockHeadersMsg:
...
case msg.Code == BlockHeadersMsg:
...
case msg.Code == GetBlockBodiesMsg:

먼저 eth서비스에서는 handleMsg로 메세지를 주기적으로 가져온다. 가져온 메세지의 코드에 따른 로직이 실행될 것이다. 참고로 이더리움엔 여러개의 서비스를 가질 수 있게 유연하게 설계되어 있으며 각각이 고유의 프로토콜을 가질 수 있다.  p.rw.ReadMsg()를 따라가보자. 여기서 p는 위 그림에서 peer이며 rw는 protoRW이다.


type protoRW struct {
Protocol
in chan Msg // receives read messages
closed <-chan struct{} // receives when peer is shutting down
wstart <-chan struct{} // receives when write may start
werr chan<- error // for write results
offset uint64
w MsgWriter
}

func (rw *protoRW) WriteMsg(msg Msg) (err error) {
if msg.Code >= rw.Length {
return newPeerError(errInvalidMsgCode, "not handled")
}
msg.Code += rw.offset
select {
case <-rw.wstart:
err = rw.w.WriteMsg(msg)
// Report write status back to Peer.run. It will initiate
// shutdown if the error is non-nil and unblock the next write
// otherwise. The calling protocol code should exit for errors
// as well but we don't want to rely on that.
rw.werr <- err
case <-rw.closed:
err = ErrShuttingDown
}
return err
}

func (rw *protoRW) ReadMsg() (Msg, error) {
select {
case msg := <-rw.in:
msg.Code -= rw.offset
return msg, nil
case <-rw.closed:
return Msg{}, io.EOF
}
}

 ReadMsg는 rw.in채널에서 메세지가 들어오길 기다리다가 들어오면 메세지코드msg를 리턴해준다. Go에 익숙하지 않다면 헷갈릴수 있는데, 위의 코드는 case 문에서 무엇인가 실행되기 전 까지는 블록된다. 만약 default 문을 추가한다면 default문을 실행하고 바로 리턴 될 것이다. 그럼 이제 rw.in 채널에 메세지를 넣어주는 부분을 찾아보자.

func (p *Peer) handle(msg Msg) error {
switch {
case msg.Code == pingMsg:
...
default:
proto, err := p.getProto(msg.Code)
if err != nil {
return fmt.Errorf("msg code out of range: %v", msg.Code)
}
select {
case proto.in <- msg:
return nil
case <-p.closed:
return io.EOF
}
}
return nil
}

p2p.peer 의 handle 메소드에서 msg 를 받은 후에 code가 어떤 프로토콜에 해당하는지  확인 후 해당 프로토콜에 proto.in <-msg: 를 통해서 메세지를 전달 해 준다. 그럼 매개변수 msg 는 어디서 왔을까?


func (p *Peer) readLoop(errc chan<- error) {
defer p.wg.Done()
for {
msg, err := p.rw.ReadMsg()
if err != nil {
errc <- err
return
}
msg.ReceivedAt = time.Now()
if err = p.handle(msg); err != nil {
errc <- err
return
}
}
}

다른 사람들의 이더리움에 해당되는 각각의 peer 안에는 고루프로 실행된 readLoop가 있어서 p.rw.ReadMsg()를 통해 메세지를 받아오고 있었다.  p.rw.ReadMsg()를 추적해보자.

func (t *rlpx) ReadMsg() (Msg, error) {
t.rmu.Lock()
defer t.rmu.Unlock()
t.fd.SetReadDeadline(time.Now().Add(frameReadTimeout))
return t.rw.ReadMsg()
}

rlpx가 나왔다. t.fd.SetReadDeadline 옵션값을 적당히 주고 t.rw.ReadMsg()를 통해 메세지를 읽기 시작한다.


func (rw *rlpxFrameRW) ReadMsg() (msg Msg, err error) {
// read the header
headbuf := make([]byte, 32)
if _, err := io.ReadFull(rw.conn, headbuf); err != nil {
return msg, err
}
// verify header mac
shouldMAC := updateMAC(rw.ingressMAC, rw.macCipher, headbuf[:16])
if !hmac.Equal(shouldMAC, headbuf[16:]) {
return msg, errors.New("bad header MAC")
}
rw.dec.XORKeyStream(headbuf[:16], headbuf[:16]) // first half is now decrypted
fsize := readInt24(headbuf)
// ignore protocol type for now

// read the frame content
var rsize = fsize // frame size rounded up to 16 byte boundary
if padding := fsize % 16; padding > 0 {
rsize += 16 - padding
}
framebuf := make([]byte, rsize)
if _, err := io.ReadFull(rw.conn, framebuf); err != nil {
return msg, err
}

// read and validate frame MAC. we can re-use headbuf for that.
rw.ingressMAC.Write(framebuf)
fmacseed := rw.ingressMAC.Sum(nil)
if _, err := io.ReadFull(rw.conn, headbuf[:16]); err != nil {
return msg, err
}
shouldMAC = updateMAC(rw.ingressMAC, rw.macCipher, fmacseed)
if !hmac.Equal(shouldMAC, headbuf[:16]) {
return msg, errors.New("bad frame MAC")
}

// decrypt frame content
rw.dec.XORKeyStream(framebuf, framebuf)

// decode message code
content := bytes.NewReader(framebuf[:fsize])
if err := rlp.Decode(content, &msg.Code); err != nil {
return msg, err
}
msg.Size = uint32(content.Len())
msg.Payload = content

// if snappy is enabled, verify and decompress message
if rw.snappy {
payload, err := ioutil.ReadAll(msg.Payload)
if err != nil {
return msg, err
}
size, err := snappy.DecodedLen(payload)
if err != nil {
return msg, err
}
if size > int(maxUint24) {
return msg, errPlainMessageTooLarge
}
payload, err = snappy.Decode(nil, payload)
if err != nil {
return msg, err
}
msg.Size, msg.Payload = uint32(size), bytes.NewReader(payload)
}
return msg, nil
}

드디어 오늘의 주인공 rlpxFrameRW 객체가 나왔으며, 이 Read/Write 함수를 하나씩 뜯어 보자. 

그 전에 선두지식이 필요한데, 여기서 Read/Write 하기전에 peer 끼리 커넥션이 맺어질때 RLPX은 먼저 상대방과 암호화 통신을 하기 위한 암호화 키들을 교환하는데, 그렇게 해서 만들어진 암호화 재료가 read/write에 사용된다 그 부분에 대해 좀 더 자세히 알아보려면 이 글을 필히 먼저 읽어보자. [이더리움] RLPX - Encryption handshake 

복잡해보이지만 여기서 필요한 내용은 아래와 같다.

- 고정적인 NodeID(public key) 와 노드 고유의 Private 키를 가지고 random pri-key / pub-key를 상호 생성/교환 
- 랜덤으로 생성한 pub/pri 키를 통해 secrets (대칭키AES와 HMAC) 생성이다.

func (h *encHandshake) secrets(auth, authResp []byte) (secrets, error) {
... s := secrets{
RemoteID: h.remoteID,
AES: aesSecret,
MAC: crypto.Keccak256(ecdheSecret, aesSecret),
}

...
if h.initiator {
s.EgressMAC, s.IngressMAC = mac1, mac2
} else {
s.EgressMAC, s.IngressMAC = mac2, mac1
}

return s, nil
}

이렇게 생성된 AEC/MAC 로는 암호화/복호화를 하고, Egress/IngressMAC으로는 메세지 authentication을 하게 된다. 

func newRLPXFrameRW(conn io.ReadWriter, s secrets) *rlpxFrameRW {
macc, err := aes.NewCipher(s.MAC)
if err != nil {
panic("invalid MAC secret: " + err.Error())
}
encc, err := aes.NewCipher(s.AES)
if err != nil {
panic("invalid AES secret: " + err.Error())
}
// we use an all-zeroes IV for AES because the key used
// for encryption is ephemeral.
iv := make([]byte, encc.BlockSize())
return &rlpxFrameRW{
conn: conn,
enc: cipher.NewCTR(encc, iv),
dec: cipher.NewCTR(encc, iv),
macCipher: macc,
egressMAC: s.EgressMAC,
ingressMAC: s.IngressMAC,
}
}

secrets (AES,MAC,IngressMAC,EgressMAC) 는 rlpxFrameRW속성에 대입된다.

 // read the header
headbuf := make([]byte, 32)
if _, err := io.ReadFull(rw.conn, headbuf); err != nil {
return msg, err
}

1. 헤더버퍼사이즈만큼 패킷읽음

 // verify header mac
shouldMAC := updateMAC(rw.ingressMAC, rw.macCipher, headbuf[:16])
if !hmac.Equal(shouldMAC, headbuf[16:]) {
return msg, errors.New("bad header MAC")
}
rw.dec.XORKeyStream(headbuf[:16], headbuf[:16]) // first half is now decrypted
fsize := readInt24(headbuf)
2. 헤더를 macCipher로 복호화하고 ingressMAC으로 해싱한후에, 받은 헤더의 16바이트 이후의 값과 대조하여 동일하면 문제가 없다고 판단하여 다음으로 진행. 그리고 MAC 은 항상 업데이트되서 보안이 더 강화된다. 업데이트를 위한 시드는 headbuf[:16]을 사용하였다.
 // read the frame content
var rsize = fsize // frame size rounded up to 16 byte boundary
if padding := fsize % 16; padding > 0 {
rsize += 16 - padding
}
framebuf := make([]byte, rsize)
if _, err := io.ReadFull(rw.conn, framebuf); err != nil {
return msg, err
}

3. 패딩이 추가된 framebuf 사이즈만큼 데이터를 읽는다. 

// read and validate frame MAC. we can re-use headbuf for that.
rw.ingressMAC.Write(framebuf)
fmacseed := rw.ingressMAC.Sum(nil)
if _, err := io.ReadFull(rw.conn, headbuf[:16]); err != nil {
return msg, err
}
shouldMAC = updateMAC(rw.ingressMAC, rw.macCipher, fmacseed)
if !hmac.Equal(shouldMAC, headbuf[:16]) {
return msg, errors.New("bad frame MAC")
}

4.들어온 frame 데이터를 ingressMAC.Write와 Sum 업데이트를 위한 fmacseed를 만들어서 해싱하여 프레임버퍼 이후에 붙어있는16바이트를 받아와 인증/검증확인한 후 이상없으면 진행한다.

// decrypt frame content
rw.dec.XORKeyStream(framebuf, framebuf)

5. 들어온 frame데이터를 secrets.AES로 만들어진 dec에 의해 복호화 하여 저장

// decode message code
content := bytes.NewReader(framebuf[:fsize])
if err := rlp.Decode(content, &msg.Code); err != nil {
return msg, err
}
msg.Size = uint32(content.Len())
msg.Payload = content

6. rlp 디코딩하여 msg.Payload에 저장

// if snappy is enabled, verify and decompress message
if rw.snappy {
payload, err := ioutil.ReadAll(msg.Payload)
if err != nil {
return msg, err
}
size, err := snappy.DecodedLen(payload)
if err != nil {
return msg, err
}
if size > int(maxUint24) {
return msg, errPlainMessageTooLarge
}
payload, err = snappy.Decode(nil, payload)
if err != nil {
return msg, err
}
msg.Size, msg.Payload = uint32(size), bytes.NewReader(payload)
}
return msg, nil


7. snappy가 사용가능하면 압축을 풀어서 최종완성된 msg 를 리턴해준다.


이더리움에서 TCP기반의 스트리밍을 위해 peer끼리 커넥션을 맺을때 아래와 같은 순서를 갖는데 

1. 암호화키를 교환하여 앞으로는 이 키 기반으로 통신하게 함
2. 프로토콜 정보(버전등)를 교환하여 서로 동업자인지 확인하고 
3. rlp 로 엔코딩하고 서로 약속된 frame에 맞춰서 메세지를 주고 받는다.

이 글에서는 1번에 대해서 코드와 함께 알아 볼 것이다.

접속을 하는 peer를 initiator 라고 하고 접속을 받는 peer를 receiver 라고 한다.
한번 접속했던 경우 known peer라고 하고, 처음 접속하는 peer를 new peer라고 한다. 
한번 접속했던 경우에는 session token을 보관하는데, 그렇더라도 항상 접속할때마다 그 기반으로 새로운 키를 만들어 사용한다.

(참고: 스트리밍과  RPC 와 차이점? 어차피 TCP소켓통신은 똑같지만 RPC는 한단계 더 추상화 한 것. 보통 함수호출 후 간단한 리턴을 받을때 RPC 사용. 스트리밍은 느낌 그대로 큰 데이터를 주고 받는 느낌으로 사용.이더리움에서 peer간 블록전송을 위해서는 스트리밍, 클라이언트가 트랜잭션 일으킬때는 HTTP 기반 JSON RPC API를 사용한다. 하이퍼레저 패브릭 패브릭에서는 성능이 좋은 gRPC를 적극적으로 사용하는데 반해 이더리움에서는 사용하지 않는 듯하다.  The Ethereum ABI to gRPC protobuf transpiler 이런게 있긴 하다.)

앞으로 글을 읽어 내려 갈때 만날 단어들 정리 (자세한 차이는 구글링)
ECDSA : Elliptic Curve Digital Signature Algorithm (기본 서명,암호화)
ECIES :  Elliptic Curve Integrated Encryption scheme (공유 대칭키 생성) 
ECDH : Elliptic Curve Diffie–Hellman key exchange (공유 대칭키 생성)
HMAC:  hash message authentication code  (해싱에 의한 메세지 검증 및 인증)


Encrpytion handshake

엘리스와 댄이 암호화 통신을 하기 위해서는 보통 댄의 공개키를 앨리스가 받아서, 그것으로 대칭키를 암호화해서 댄에게 주면  댄이 자신의 개인키로 복호화하여 안전하게 통신할수 있는것을 상상 할 수 있다. 근데 이때 문제는 공개키가 엘리스로 갈때 탈취 될 수 있다는 것이며, 댄의 공개키인지 확실히 알 수 없다. 보통 CA를 이용해서 전자서명을 통해 해결하곤 하는데, 여기서는 CA도 필요없고  대칭키를 이동하는 과정 없이 해결하는 방식을 사용한다. 

해당 과정에 대해서 코드를 통해 살펴보자. (가독성을 위해 예외처리는 모두 삭제)

// secrets represents the connection secrets
// which are negotiated during the encryption handshake.
type secrets struct {
RemoteID discover.NodeID
AES, MAC []byte
EgressMAC, IngressMAC hash.Hash
Token []byte
}

이후 과정은 결국 위의 자신과 리모트가 공유할 대칭키(secrets)를 갖기 위함이란 걸 알아두자.이 대칭키는 추후에 있을 실제 블록공유등의 read/write 메세지를 암호화하기 위해 이용될 것이다.

// This field must be set to a valid secp256k1 private key.
PrivateKey *ecdsa.PrivateKey `toml:"-"`

1. 각 peer는 자신만의 PrivateKey를 secp256k1 타입으로 가지고 있어야 한다. 

  --nodekey value       P2P node key file
  --nodekeyhex value    P2P node key as hex (for testing)

이 키는 위의 커맨드 라인  옵션에 따라  crypto.LoadECDSA(file) 나 crypto.HexToECDSA(hex) 함수로 임시적으로 만들어 진다.

if c.id, err = c.doEncHandshake(srv.PrivateKey, dialDest); err != nil {
return err
}

2. Encryption handshake 진입 (내 노드의 임시 개인키와 노드디스커버리를 통해 찾은 리모트 피어 정보를 매개변수로) 

if dial == nil {
sec, err = receiverEncHandshake(t.fd, prv, nil)
} else {
sec, err = initiatorEncHandshake(t.fd, prv, dial.ID, nil)
}

3. initiator 면 initiatorEncHandshake 를 receiver 면 receiverEncHandshake 호출. 

h := &encHandshake{initiator: true, remoteID: remoteID}
authMsg, err := h.makeAuthMsg(prv, token)
if err != nil {
return s, err
}
authPacket, err := sealEIP8(authMsg, h)
if err != nil {
return s, err
}
if _, err = conn.Write(authPacket); err != nil {
return s, err
}

authRespMsg := new(authRespV4)
authRespPacket, err := readHandshakeMsg(authRespMsg, encAuthRespLen, prv, conn)
if err != nil {
return s, err
}
if err := h.handleAuthResp(authRespMsg); err != nil {
return s, err
}
return h.secrets(authPacket, authRespPacket)

4. initiatorEncHandshake 내에서 makeAuthMsg로 진입한다. 

// 메개변수 prv 는 자기노드의 개인키이고, token은 널이다. func (h *encHandshake) makeAuthMsg(prv *ecdsa.PrivateKey, token []byte) (*authMsgV4, error) {
//리모트노드의ID는 이더리움에서 Public Key로 사용된다.ecdsa.PublicKey 로 리턴한다. rpub, err := h.remoteID.Pubkey()
// ECDSA public key 를 ECIES public key로 사용되게 변환한다.
h.remotePub = ecies.ImportECDSAPublic(rpub)
// random initiator nonce 생성
h.initNonce = make([]byte, shaLen)
if _, err := rand.Read(h.initNonce); err != nil {
return nil, err
}
// ECDH를 위한 랜덤 키페어 생성
h.randomPrivKey, err = ecies.GenerateKey(rand.Reader, crypto.S256(), nil)
// 자신개인키(prv)로 ECIES 키페어 만들고,remotePub을 섞어서 shared-secret 생성

token, err = h.staticSharedSecret(prv) // shared-secret 와 nonce 를 ^ 해서 sigend 생성
signed := xor(token, h.initNonce) // signed 를 리모트의 개인키를 통해 사인해 줘서 signature 생성
signature, err := crypto.Sign(signed, h.randomPrivKey.ExportECDSA())
// 최종적으로 authMsgV4 생성 // RLPx v4 handshake auth (defined in EIP-8).
msg := new(authMsgV4)
copy(msg.Signature[:], signature)
copy(msg.InitiatorPubkey[:], crypto.FromECDSAPub(&prv.PublicKey)[1:])
copy(msg.Nonce[:], h.initNonce)
msg.Version = 4
return msg, nil
}

5. authMsg 를 리턴한다. 


var padSpace = make([]byte, 300)

func sealEIP8(msg interface{}, h *encHandshake) ([]byte, error) {
buf := new(bytes.Buffer)
if err := rlp.Encode(buf, msg); err != nil {
return nil, err
}
// pad with random amount of data. the amount needs to be at least 100 bytes to make
// the message distinguishable from pre-EIP-8 handshakes.
pad := padSpace[:mrand.Intn(len(padSpace)-100)+100]
buf.Write(pad)
prefix := make([]byte, 2)
binary.BigEndian.PutUint16(prefix, uint16(buf.Len()+eciesOverhead))

enc, err := ecies.Encrypt(rand.Reader, h.remotePub, buf.Bytes(), nil, prefix)
return append(prefix, enc...), err
}

6. 5번에서 받은 auth 메세지에 패딩을 추가해서 리모트Pub키로 ecies.Encrypt 한후에 prefix를 추가해서 리턴한다.

if _, err = conn.Write(authPacket); err != nil {
return s, err
}

7. 6번에서 리턴한 패킷(authPacket)을 리모트로 쏴 준다.

// receiverEncHandshake negotiates a session token on conn.
// it should be called on the listening side of the connection.
//
// prv is the local client's private key.
// token is the token from a previous session with this node.
func receiverEncHandshake(conn io.ReadWriter, prv *ecdsa.PrivateKey, token []byte) (s secrets, err error) {
authMsg := new(authMsgV4)
authPacket, err := readHandshakeMsg(authMsg, encAuthMsgLen, prv, conn)
if err != nil {
return s, err
}
h := new(encHandshake)
if err := h.handleAuthMsg(authMsg, prv); err != nil {
return s, err
}

authRespMsg, err := h.makeAuthResp()
if err != nil {
return s, err
}
var authRespPacket []byte
if authMsg.gotPlain {
authRespPacket, err = authRespMsg.sealPlain(h)
} else {
authRespPacket, err = sealEIP8(authRespMsg, h)
}
if err != nil {
return s, err
}
if _, err = conn.Write(authRespPacket); err != nil {
return s, err
}
return h.secrets(authPacket, authRespPacket)
}

8. 리모트 peer는 해당 authPacket을 받아서 처리 (secrets 를 생성) 한후에 다시 initiator로 ack 해준다.


func readHandshakeMsg(msg plainDecoder, plainSize int, prv *ecdsa.PrivateKey, r io.Reader) ([]byte, error) {
buf := make([]byte, plainSize)
if _, err := io.ReadFull(r, buf); err != nil {
return buf, err
}
// Attempt decoding pre-EIP-8 "plain" format.
key := ecies.ImportECDSA(prv)
if dec, err := key.Decrypt(buf, nil, nil); err == nil {
msg.decodePlain(dec)
return buf, nil
}
// Could be EIP-8 format, try that.
prefix := buf[:2]
size := binary.BigEndian.Uint16(prefix)
if size < uint16(plainSize) {
return buf, fmt.Errorf("size underflow, need at least %d bytes", plainSize)
}
buf = append(buf, make([]byte, size-uint16(plainSize)+2)...)
if _, err := io.ReadFull(r, buf[plainSize:]); err != nil {
return buf, err
}
dec, err := key.Decrypt(buf[2:], nil, prefix)
if err != nil {
return buf, err
}
// Can't use rlp.DecodeBytes here because it rejects
// trailing data (forward-compatibility).
s := rlp.NewStream(bytes.NewReader(dec), 0)
return buf, s.Decode(msg)
}
authRespMsg := new(authRespV4)
authRespPacket, err := readHandshakeMsg(authRespMsg, encAuthRespLen, prv, conn)
if err != nil {
return s, err
}
if err := h.handleAuthResp(authRespMsg); err != nil {
return s, err
}
return h.secrets(authPacket, authRespPacket)
func (h *encHandshake) handleAuthResp(msg *authRespV4) (err error) {
h.respNonce = msg.Nonce[:]
h.remoteRandomPub, err = importPublicKey(msg.RandomPubkey[:])
return err
}

9. initiator 는 리모트 peer로부터 받은 auth메세지로 randomPub 키를 생성한다. 이 말은 상호간에 랜덤하게 생성된 pub/pri 키를 활용한다는 뜻이다.

// secrets is called after the handshake is completed.
// It extracts the connection secrets from the handshake values.
func (h *encHandshake) secrets(auth, authResp []byte) (secrets, error) {
ecdheSecret, err := h.randomPrivKey.GenerateShared(h.remoteRandomPub, sskLen, sskLen)
if err != nil {
return secrets{}, err
}

// derive base secrets from ephemeral key agreement
sharedSecret := crypto.Keccak256(ecdheSecret, crypto.Keccak256(h.respNonce, h.initNonce))
aesSecret := crypto.Keccak256(ecdheSecret, sharedSecret)
s := secrets{
RemoteID: h.remoteID,
AES: aesSecret,
MAC: crypto.Keccak256(ecdheSecret, aesSecret),
}

// setup sha3 instances for the MACs
mac1 := sha3.NewKeccak256()
mac1.Write(xor(s.MAC, h.respNonce))
mac1.Write(auth)
mac2 := sha3.NewKeccak256()
mac2.Write(xor(s.MAC, h.initNonce))
mac2.Write(authResp)
if h.initiator {
s.EgressMAC, s.IngressMAC = mac1, mac2
} else {
s.EgressMAC, s.IngressMAC = mac2, mac1
}

return s, nil
}

9. random 하게 생셩된 pub/priv를 사용하여 secrets를 만들기 시작한다. 

굉장히 복잡해 보이는게 사실이나 중요 포인트는 2가지이다.
- 고정적인 NodeID(public key) 와 노드 고유의 Private 키를 가지고 random pri-key / pub-key를 상호 생성/교환 
- 랜덤으로 생성한 pub/pri 키를 통해 secrets 생성이다.

이렇게 생성된 secrets.AES 로는 암호화/복호화를 하고, MAC으로는 메세지 authentication을 하게 된다. 
아래처럼 정리 될 수있다.

auth -> E(remote-pubk, S(ephemeral-privk, static-shared-secret ^ nonce) || H(ephemeral-pubk) || pubk || nonce || 0x0)
auth-ack -> E(remote-pubk, remote-ephemeral-pubk || nonce || 0x0)

static-shared-secret = ecdh.agree(privkey, remote-pubk)

E: 첫번째 인자로 암호화
S: 첫번째 인자로 사인 
H: 해싱
|| : 더하기 
^ : XOR
auth: initiator가 만들어서 receiver에 전송하는 메세지
auth-ack : receiver가 돌려주는 메세지
서로간의 ephemeral-pubk (임시공개키)를 교환하는것이 핵심이다.
이렇게 교환된 임시공개키와 자신이 가지고있는 임시개인키를 이용하여 각자 secrets를 아래처럼 만든다.

ephemeral-shared-secret = ecdh.agree(ephemeral-privkey, remote-ephemeral-pubk)
shared-secret = keccak256(ephemeral-shared-secret || keccak256(nonce || initiator-nonce))
aes-secret = keccak256(ephemeral-shared-secret || shared-secret)
# destroy shared-secret
mac-secret = keccak256(ephemeral-shared-secret || aes-secret)
# destroy ephemeral-shared-secret

Initiator:
egress-mac = keccak256.update(mac-secret ^ recipient-nonce || auth-sent-init)
# destroy nonce
ingress-mac = keccak256.update(mac-secret ^ initiator-nonce || auth-recvd-ack)
# destroy remote-nonce

Recipient:
egress-mac = keccak256.update(mac-secret ^ initiator-nonce || auth-sent-ack)
# destroy nonce
ingress-mac = keccak256.update(mac-secret ^ recipient-nonce || auth-recvd-init)
# destroy remote-nonce

 임시 대칭키 (shared_secrete) = 자신의 임시 개인키 와 상대방의 임시공개키 만드는 것을 볼 수 있다.
 이 임시 대칭키는 상대방의 임시개인키와 자신의 임시공개키로도 똑같이 만들 수 있고 ECDH라고 한다.
마지막으로 임시대칭키를 이용해서 인증과 데이타가 변경되지 않았다는 것을 보증하는 MAC도 생성하며 처음 연결시에 만들어진 Ingress/egressMAC은 데이터를 주고 받으면서 계속 업데이트 되어 보안이 더 강화된다.



얼마전에 블록체인사 CTO분들하고 대화중에 하이퍼레저 패브릭의 합의 알고리즘에 대해서 얘기하다가 "뇌정지"가 온적이 있다. 비트코인 POW부터 POS,DPOS,PBFT,텐더민트식,캐스퍼등에 대한 이해를 거친후에 더 이상은 컨센서스 알고리즘에 대한 공부는 그만 뒀고, (분산네트워킹만이라도 잘하자 싶어서..) 하이퍼레저 패브릭은 그냥 실행-오더링-커밋이지 뭐...라고 평소에 생각했었는데 이번 기회를 통해 정리를 좀 해보려 한다. 알고리즘 자체에 대한 내용이라기 보다는 전체조망이랄까? 주저리주저리 해 볼 생각이다.

거의 유일한 하이퍼레저 패브릭 서적이라고 볼 수 있는데,
이 책을 완독한 후에도 합의 관련된 이해는 할 수 는 없다. 관련 내용이 (구체적으로) 없으니까~
즉 모든 원흉은 이 책이다. 농담이다..
400페이지가 넘어가며 정말 많은 내용을 충실히 작성해 놓은 이 책에서 합의알고리즘 내용은 
58page에 합의 항목이 나오는데 "하이퍼레저패브릭에서 합의 시스템은 보증-오더링-검증모델 기반하에 플러그인 될 수 있다. 하이퍼레저 패브릭의 오더링 서비스는 합의 시스템을 표현한다." 정도의 표현만 있다. 
(근데 사실 이게 핵심이자 모든것이기도 하다.) 

이제 본격적으로 히스토리를 살펴보자.
하이퍼레저 패브릭 0.6버전에서는 PBFT를 사용했다고 한다. (PBFT에 대한 좀 더 구체적인 것은 여기 참고)

그림 - 하이퍼레저 패브릭 0.6

위의 0.6 도식도와 같이 오더러가 없으며, (컨센서스가 대행함) 보증하는 부분과 커미팅하는 부분이 나뉘지도 않았다.
이 Peer들끼리 아래와 같은 PBFT를 수행했으며 간락하게 설명하면 

그림 - PBFT 

Pre-prepare단계에서 트랜잭션을 모두 공유하고, 
Prepare단계에서 각자 Peer들은  트랜잭션을 받았다는 것을 알리며,
Commit단계에서 각자 Peer들은 이 트랜잭션에 대해서 합의를 해준다라는 것을 모두에게 알리며 
마지막으로 동료Peer들의 합의 상태에 따라서 OK인지 아닌지를 각자 확인해서 Reply 해준다.

이 알고리즘은 보통 2/3 이상의 합의가 있으면 통과이고, 대략 2n^2번의 커뮤케이션이 필요하게 된다.
즉 Peer숫자가 50개만되도 엄청 느려진다는 말이다. 처음 하이퍼레저패브릭을 설계했을때, 허가형블록체인으로 소수의 노드를 대상으로 서비스 한다고 생각 했을 테니 일리가 있지만, 무리가 따랐나보다. 1.0에서는 아키텍쳐가 달라진다.
* 하이퍼레저 0.6 PBFT에 대한 좀 더 자세한 설명은 여기  
근데 중국어라는게 함정 (간자체로 번역하세요)

그림 - 하이퍼레저 패브릭 1.0  Endorsing - Ordering - Validating 3단계 구조 

PBFT식으로 Peer들간에 커뮤니케이션 하는 부분이 너무 느려서 없어지고,  Endorsing - Ordering - Validating 3단계 구조를 기반으로 합의 알고리즘이 완성된다.  Endorsing 에서는 보증역할을 맡은 Peer들이 트랜잭션에 대해 실행을 하고 결과 값 사인한후에 클라이언트에 돌려준다. 여기서 Peer들간의 커뮤니케이션은 없다. 이런 값을 오더러에게 전달하면 오더러는 트랜잭션에 순서를 매기고, 블록으로 완성 한후 Validator 에게 전달하면 Validatior에서는 보증이 어떻게 되있는지 확인하여 만족 시키면 장부에 기입한다.(좀 더 자세한 설명 여기) 자 이렇게 되면 커뮤니케이션 비용은 확실히 낮아지게 되는데 병목 부분을 생각해보면 3군데 있는데
1. Endorsing 하는 부분에서 체인코드를 실행하는 비용
2. Signning (서명 및 확인)부분이다. 
3. 앞으로 말할 Ordering 서비스에 어떤 합의 알고리즘이 들어가냐의 문제이다.

그럼 글 서두에 말한 "하이퍼레저 패브릭의 Ordering 서비스는 합의 시스템을 표현한다." 는 무엇인가? 
https://github.com/hyperledger/fabric/tree/master/orderer  다음 링크를 살펴보면 아래와 같이 나온다.

  • Solo ordering 서비스 (테스팅 목적): 쉽게 이용되도록 만들어진 극히 간단한 서비스로써 프로덕션용은 아니다. 싱글 프로세스로 구성되어 있으며, 하나가 모든 클라이언트들에 대해 서비스 한다. 따라서 사실상 합의라는게 없다. availability 나 scalability에 대해 고려되지 않으며 따라서 이것은 그냥 테스트용~
  • Kafka기반 오더링 서비스(프로덕션)Pub/Sub구조의 메세지큐 미들웨어인 Kafka 기반의 ordering service 이다. Peer 오더러 클라이언트 코드가 카프카에 대해 상세히 작성되지 않도록 ab.proto 정의로 이것을 추상화 한다.Kafka는 현재 높은 처리량과 고가용성을 요구하지만 byzantine 내결함성은 요구하지 않는 운영 배치에서 선호되는 선택이다.
  • PBFT 오더링 서비스(지연됨):  BFT적인 방식에 의해 메세지들을 나열하고 순서 매이기 위한 구현으로 만들어 질 것이다. (현재 개발중) 

즉 위와 같이 구현 할 수 있으며, 현재는 Zookeeper/Kafka기반하여 속도중시/부하분산/복제와리더재선출에 의의한 HA/오더러가 순서매기는데 도움/ 정도의 역할로 외부시스템을 이용해서 이루어져 있는 것이다. 하지만 카프카가 오더러들에 대한 HA까지 보장해주진 않는다. 멀티오더러에 대해서는 node sdk를 사용하는 미들웨어 단에서 잘 작동하는 오더러에 대한 선택을 하는 수동적인 방식이 있는 것 같다. 만약 좀 더 신뢰/오더러에 대한HA를 중시하고 싶으면 BFT계열(PBFT등)를 추가 구현 할 수도 있을것이다. 개인적으로도 허가과정등을 통해 신뢰비용에 대해 어느정도 보완 했다고 판단하기 때문에 Kafka기반이면 충분하지 않나 싶기도 하는데 외부모듈을 무겁게 가져다 사용하는건 문제다. 그리고 Kafka가 하는 역할에 대해  CFT (Crash Fault Tolerance) 같은 용어를 쓰는데, BFT처럼 제대로된(?) 합의 알고리즘이 아니라, 주키퍼-카프카 구성으로 장애에 대한 대처가 가능한 상태에서 오더링을 하기 때문이다. 아무튼 개인적으로는 현재 카프카-주키퍼 오더링 부분과 앞으로 나올 Raft식의 오더링에 대한 행위에 대해서 '합의' 는 POW식의 '신뢰의 합의' 는 아니고 고전적인 분산시스템에서의 내고장 합의를 말하는 것이다. (오더러 부분도 각 조직들에 의해 권한을 분산해서 나누어 갖는 형태가 되면 신뢰의 합의까지 되겠지만 확실하지 않다. 만약 그렇게 까지 하는건 좀 오버인거 같다.)

그림 - 하이퍼레저 패브릭 로드맵 

구글링을 해보면 위의 표를 많이 볼 수 있는데 1.3 에서는 RAFT오더러, 1.4에서는 BFT오더러의 구현을 계획했는데 2019년 1월 현재 1.4가 출시된 상황에서 어떻게 되었을까?

What’s new in v1.3 
에는 RAFT오더러에 관련된 내용이 없다.
What’s new in v1.4 에도 BFT 오더러에 대한 내용은 없다.

그럼 2.0버전에서는 어떻게 될까? 프로젝트 현황에 대해 지라 와 제안문서 살펴보니 RAFT / etcd/raft 기반으로 개발중인듯 싶은데 비교적 단순한 알고리즘으로 
BFT처럼 무겁지 않고 CFT 역할에 그치더라도 1) 카프카-주키퍼에 의존하지 않는 오더링 서비스를 위함. 즉 굳이 외부미들웨어 가져다 복잡하게 설치해서 쓸 필요 있냐 하는 2) 차후에 개발될 BFT 개발을 위한 기반쯤으로 생각하는 듯하다. 카프카-주키퍼 모델에 비해 성능이 어떨지에 대한 예측 및 어떻게 BFT개발을 위한 기반이 될 까에 대해서는 깊이 파보진 않았다. 

제안문서 B.II)   Raft 는 어떻게 BFT개발의 주춧돌이 되어 줄 수 있나?
RAFT는 BFT와 마찬가지로 리더 기반 프로토콜입니다. 잘 검증 된 리더 기반 합의 프로토콜을 통합한다는 것은 Fabric이이 프로토콜 군에 대한 합의 된 (confensus) 플러그인 작성자에게 제공하는 인터페이스 개선에 초점을 맞추는 것을 의미합니다. 그러한 의미에서 Raft에 대한 실험은 우리가 PBFT 기반 오더링 서비스의 토대를 마련하는 데 도움이됩니다. 연장선 상에서 Raft가 CFT 프로토콜 임에도 불구하고 BFT 컨텍스트에서만 의미가 있는 제안 된 구현에서 특정한 결정을 내립니다. 우리는 그것을 의도적으로 수행하여 BFT 기반 서비스로 전환 할 때 Fabric 핵심 변경 사항의 수를 최소로 유지 하려고 합니다. (역주: Kafka 분산복제처리도 리더기반인데..리더가 읽기/쓰기 담당하고 팔로워는 복제만) 

결국 Pluggable 하기 때문에 업체에 따라서 자신들이 만들어 추가 할 수도 있다지만 하이퍼레저 패브릭 자체에서도 추가하는데 오래걸리는데 이걸 직접 자신의 입맛에 맞게 요리해서 사용하려는 업체가 얼마나 될런지는 모르겠다. 특히 BFT알고리즘에 대한 논문은 많지만 성공적으로 구현하는것은 굉장히 어렵다. 

참고로 BFT계열의 컨센서스 플러거블 라이브러리(BFT-SMART)도 있다. 현재 정식버전으로 들어 간것은 아니며 추후 RAFT 이후에 BFT계열로 SBFT가 들어갈지, BFT-SMART가 들어갈지 모르겠다. BFT-SMART는 논문상에 10,000TPS 급의 성능을 보여주며, SBFT는 아직 테스팅된게 없는것으로 알고 있다.

결론 )

개인적으로는 카프카 대체제로는 RAFT로 충분하고 BFT계열은 간단하든 복잡하는 오버스펙이라 시작할맘도 없을 거 같습니다.참고로 JP모건 애들이 만드는 콘소시엄체인은 RAFT랑 IstabulBFT 라는것을 쓰는데 패브릭도 BFT계열을 겨우 오더링하는데 사용 할 바엔 E-O-V 프로세스를 폐기하는게 낫다고 봅니다. 아마 거기 아키텍트도 똑같은 생각하고 있을듯..ㅎㅎ



 

 

3줄 정리

- 자신과 연계된 UTXO 를 소비 하려면 그때 자신의 서명을 생성해서 트랜잭션 내부의 INPUT에 넣어 보낸다.
- 검증노드들은 해당 서명을 이전 트랜잭션의 OUTPUT과 매칭이 되면 ㅇㅋ 해줌
- 즉 현재 트랜잭션 내부의 INPUT은 이전 트랜잭션의 OUTPUT과 매칭용이고, 현재 트랜잭션의 OUTPUT은 받는 사람의 계정과 연결되되, 나중에 받는 사람이 이 UTXO를 사용하는 트랜잭션을 만들 때 그 INPUT과 매칭될 것이다. 
- 최종적으로 TxID를 만드는 과정은 : 
https://steemit.com/kr/@niipoong/id-create-bitcoin-txid 를 참고한다.

받는 사람 : ScriptPubKey(잠금스크립트)에서  OP_HASH160 다음에 위치한 문자열(B라고하자)를 이용해서 실질적 비트코인 주소 생성( 비트코인 주소는 Private Key -> Publick Key -> B -> 20바이트비트코인 주소로 만들어진다. 따라서 ScriptPubKey에서 받는 사람 주소를 얻을 수 있다)

보내는 사람 : ScriptSig 에서는 서명+공개키가 포함되어있으므로, 해당 공개키로 비트코인 주소를 만들면 된다. 

* 비트코인 주소 생성 방법 : https://ihpark92.tistory.com/6

참고로 아래 트랜잭션 경우는 한개의 UTXO 에서 한개의 UTXO로 보내고 있다.vin (trnasaction input)이 1이고 vout(transaction output) 도 1이다. 보내야 할 돈이 많이 필요해서 한개의 계정에 묶인 여러개의 UTXO를 통해 보내고, 잔액까지 받아야 한다면vin 이 n이 되고, vout은 2가 될 것이다.



[이더리움으로 배우는 GO언어]  자료구조 & 컬렉션 

이번 글에서는 사실 이더리움하고 크게 상관없이 go 자료구조 기본에 대해서 알아 봅니다.

Array 

var a [5]int  // [0 0 0 0 0]
a[4] = 100 // [0 0 0 0 100]

5개의 비어있는 배열 생성과 값 삽입 

b := [5]int{1, 2, 3, 4, 5} // [1 2 3 4 5]

5개 요소가 들어있는 배열 생성 

a2 := [...]string{"USA", "China", "India", "Germany", "France"}
b2 := a2 // a copy of a is assigned to b
b2[0] = "Singapore"
fmt.Println("a is ", a2) // [USA China India Germany France]
fmt.Println("b is ", b2) // [Singapore China India Germany France]

5개 요소가 들어있는 배열 생성과 배열복사시 깊은 복사 이루어 지는 모습 

a3 := [...]float64{67.7, 89.8, 21, 78}
for i := 0; i < len(a3); i++ {
fmt.Printf("%d th element of a is %.2f\n", i, a[i])
}

sum := float64(0)
for i, v := range a3 {// i는 인덱스, v 는 값
fmt.Printf("%d the element of a is %.2f\n", i, v)
sum += v
}

순회를 도는 2가지 방식 

Slice

a4 := [5]int{76, 77, 78, 79, 80}
var b4 []int = a4[1:4] //creates a slice from a[1] to a[3]
b4[0] = 100
fmt.Println(a4) // [76 100 78 79 80]

배열을 가지고 슬라이스 생성 / 그렇게 만들어진 슬라이스는 배열과 레퍼런스로 연결되어 있다. 

i := make([]int, 5, 5)

make 를 이용한 슬라이스 생성 

cars := []string{"Ferrari", "Honda", "Ford"}
cars = append(cars, "Toyota") // [Ferrari Honda Ford Toyota]

append를 이용하여 슬라이스 뒤에 요소 추가 

cars := []string{"Ferrari", "Honda", "Ford"}
cars = append(cars[:0], "Toyota","KIA") // [Toyota KIA]
fmt.Println(cars)

append를 이용하여 새로운 슬라이스 생성. 

veggies := []string{"potatoes","tomatoes","brinjal"}
fruits := []string{"oranges","apples"}
food := append(veggies, fruits...)

append를 이용하여 슬라이스 뒤에 슬라이스 추가 ( 추가될 슬라이스 변수에 ... 를 추가한다) 

countries := []string{"USA", "Singapore", "Germany", "India", "Australia"}
neededCountries := countries[:len(countries)-2]
countriesCpy := make([]string, len(neededCountries))
countriesCpy2 := countries[:len(countries)-2]
copy(countriesCpy, neededCountries) //copies neededCountries to countriesCpy
countriesCpy[0] = "korea"
countriesCpy2[1] = "japan"

fmt.Println(countriesCpy) // 깊은복사 [korea Singapore Germany]
fmt.Println(countriesCpy2) // 얖은복사 [USA japan Germany]
fmt.Println(countries) // [USA japan Germany India Australia]

슬라이스의 일부분을 복사하는데 make로 만든 배열에 copy를 이용하여 복사하면 깊은 복사가 된다.

startTasks := func(ts []task) (rest []task) {
i := 0
for ; len(runningTasks) < maxActiveDialTasks && i < len(ts); i++ {
t := ts[i]
...
runningTasks = append(runningTasks, t)
}
return ts[i:]
}

현재 실행되고 있는 업무(runningTasks)가 정해진 숫자(maxActiveDialTasks) 를 넘지 않아서 대기중인 업무를 할 수 있는 상태일때,  매개변수로 받은 대기중인 업무들을 실행한다. 추가 실행한 업무는 runningTasks 에 append를 통해서 실행중인 업무로 추가되고, 실행업무에 추가되지 못한 업무들은 ts[i:] 로 리턴된다. 

scheduleTasks := func() {
queuedTasks = append(queuedTasks[:0], startTasks(queuedTasks)...)

if len(runningTasks) < maxActiveDialTasks {
nt := dialstate.newTasks(len(runningTasks)+len(queuedTasks), peers, time.Now())
queuedTasks = append(queuedTasks, startTasks(nt)...)
}
}

대기업무중인 업무들을 startTasks로 보내서 실행하게 하고, 실행이 안된 업무를 queuedTasks[:0] 같이 append 하여 다시 구성한다. 실행중인 업무가 maxActiveDialTasks 작을경우 새로운 업무들을 만들어서 (newTasks) startTasks로 실행한후에 남은 태스크는  queuedTasks 에 append한다.

Map

m := make(map[string]int)
m["first"] = 1
fmt.Println(m["first"]) // 1

키가 string 값이 int 은 맵을 make 를 통해 만들어서 추가/출력 한다.

var fileExtensions = map[string]string{
"Python": ".py",
"C++": ".cpp",
"Java": ".java",
"Golang": ".go",
"Kotlin": ".kt",
}
fmt.Println(fileExtensions) // map[Python:.py C++:.cpp Java:.java Golang:.go Kotlin:.kt]
delete(fileExtensions, "Kotlin")
delete(fileExtensions, "Javascript")
fmt.Println(fileExtensions) // map[Python:.py C++:.cpp Java:.java Golang:.go]

 delete 를 통해서 삭제한다.

s := map[int]string{5: "five", 2: "second"}
_, ok := s[5] // check for existence // ok true
_, ok2 := s[6] // check for existence // ok false

 키가 없을 경우에는 ok 가 false

var m1 = map[string]int{
"one": 1,
"two": 2,
"three": 3,
"four": 4,
"five": 5,
}

var m2 = m1
fmt.Println("m1 = ", m1) // map[one:1 two:2 three:3 four:4 five:5]
fmt.Println("m2 = ", m2) // map[one:1 two:2 three:3 four:4 five:5]

m2["ten"] = 10
fmt.Println("m1 = ", m1) // map[ten:10 one:1 two:2 three:3 four:4 five:5]
fmt.Println("m2 = ", m2) // map[ten:10 one:1 two:2 three:3 four:4 five:5]

맵끼리의 대입은 레퍼런스식으로 공유한다.

var personAge = map[string]int{
"Rajeev": 25,
"James": 32,
"Sarah": 29,
}

for name, age := range personAge {
fmt.Println(name, age)
}

 맵을 순회하는 방식은 range를 사용하여 key와 value를 추출한다.


MapSet("github.com/deckarep/golang-set")
이 자료구조는 도커,이더리움,쿠버네이트에서 사용되는 간단한 set 타입이다.

s := mapset.NewSet()
s.Add("Cooking")
s.Add("English")
s.Add("Math")
s.Add("Biology")
s.Add("Biology")
s.Add("Biology")
s.Add("Biology")
fmt.Printf(s.String()) // Set{Biology, Cooking, English, Math}

중복적재가 안되는 모습이구요~

s.Pop() // 아무거나 랜덤으로 하나 제거 합니다.

scienceSlice := []interface{}{"Biology", "Chemistry","Biology"}
scienceClasses := mapset.NewSetFromSlice(scienceSlice)
fmt.Printf(scienceClasses.String()) //Set{Biology, Chemistry}

슬라이스로부터 셋을 만드는 모습이구요~ 덤으로 중복요소가 없어졌습니다.

requiredClasses := mapset.NewSet()
requiredClasses.Add("Biology")
requiredClasses.Add("Cooking")

scienceSlice := []interface{}{"Biology", "Chemistry"}
scienceClasses := mapset.NewSetFromSlice(scienceSlice)

electiveClasses := mapset.NewSet()
electiveClasses.Add("Welding")
electiveClasses.Add("Music")

bonusClasses := mapset.NewSet()
bonusClasses.Add("Go Programming")
bonusClasses.Add("Python Programming")

allClasses := requiredClasses.Union(scienceClasses).Union(electiveClasses).Union(bonusClasses)
fmt.Println(allClasses) //Set{Python Programming, Biology, Cooking, Chemistry, Welding, Music, Go Programming}

Set 을 Union으로 합쳤습니다. 역시 중복요소는 제거됩니다.

//포함여부 검사?
fmt.Println(scienceClasses.Contains("Cooking")) //false

//과학클래스 과목을 제외한 모든 클래스는?
fmt.Println(allClasses.Difference(scienceClasses)) //Set{Music, Go Programming, Python Programming, Cooking, Welding}

//교집합
fmt.Println(scienceClasses.Intersect(requiredClasses)) //Set{Biology}

//How many bonus classes do you offer?
fmt.Println(bonusClasses.Cardinality()) //2

 Contains 로 포함여부 검사/Difference 로 포함그룹군 제외/ Intersect로 교집합추출/ Cardinality로 요소갯수

container/List

values := list.New()
// Add 3 elements to the list.
values.PushBack("bird")
values.PushBack("cat")
values.PushFront("snake")
// Add 100 elements at the front.
for i := 0; i < 20; i++ {
// Convert ints to strings.
values.PushFront(strconv.Itoa(i))
}

// Loop over container list.
for temp := values.Front(); temp != nil; temp = temp.Next() {
fmt.Println(temp.Value)
}

앞,뒤로 삽입할수 있는 리스트 

container/heap


// An Item is something we manage in a priority queue.
type Item struct {
value string // The value of the item; arbitrary.
priority int // The priority of the item in the queue.
// The index is needed by update and is maintained by the heap.Interface methods.
index int // The index of the item in the heap.
}

// A PriorityQueue implements heap.Interface and holds Items.
type PriorityQueue []*Item

func (pq PriorityQueue) Len() int { return len(pq) }

func (pq PriorityQueue) Less(i, j int) bool {
// We want Pop to give us the highest, not lowest, priority so we use greater than here.
return pq[i].priority > pq[j].priority
}

func (pq PriorityQueue) Swap(i, j int) {
pq[i], pq[j] = pq[j], pq[i]
pq[i].index = i
pq[j].index = j
}

func (pq *PriorityQueue) Push(x interface{}) {
n := len(*pq)
item := x.(*Item)
item.index = n
*pq = append(*pq, item)
}

func (pq *PriorityQueue) Pop() interface{} {
old := *pq
n := len(old)
item := old[n-1]
item.index = -1 // for safety
*pq = old[0 : n-1]
return item
}

// update modifies the priority and value of an Item in the queue.
func (pq *PriorityQueue) update(item *Item, value string, priority int) {
item.value = value
item.priority = priority
heap.Fix(pq, item.index)
}

// This example creates a PriorityQueue with some items, adds and manipulates an item,
// and then removes the items in priority order.
func main() {
// Some items and their priorities.
items := map[string]int{
"banana": 3, "apple": 2, "pear": 4,
}

// Create a priority queue, put the items in it, and
// establish the priority queue (heap) invariants.
pq := make(PriorityQueue, len(items))
i := 0
for value, priority := range items {
pq[i] = &Item{
value: value,
priority: priority,
index: i,
}
i++
}
heap.Init(&pq)

// Insert a new item and then modify its priority.
item := &Item{
value: "orange",
priority: 1,
}
heap.Push(&pq, item)
pq.update(item, item.value, 5)

// Take the items out; they arrive in decreasing priority order.
for pq.Len() > 0 {
item := heap.Pop(&pq).(*Item)
fmt.Printf("%.2d:%s ", item.priority, item.value)
// 05:orange 04:pear 03:banana 02:apple
}
}

heap을 이용해서 우선순위큐를 구현한 모습이구요~

container/ring

package main

import (
"container/ring"
"fmt"
"time"
)

func main() {
number := []string{"1", "2", "3", "4", "5"}

// 버퍼 생성
r := ring.New(len(number)) // 버퍼에 값 채움
for i := 0; i < r.Len(); i++ {
r.Value = number[i]
r = r.Next()
}
// 버퍼 순회하며 출력
r.Do(func(x interface{}) {
fmt.Println(x)
})


        // 버퍼 무한 순회하며 출력
for _ = range time.Tick(time.Second * 1) {
fmt.Println(r.Value)
r = r.Next()
}
}

Do를 통해서 한바퀴 순회하고, Next()를 통해서 무한이 순회할 수 있는 환영큐입니다.

컴포지트(composite) 디자인 패턴과 이름이 헷갈린 컴포지션은 UML 측면에서는 연관(Aggregation) 하고도 헷갈리기도 하는데 이 글에서는 컴포지션과 연관을 구분하지 않겠다. 컴포지션이란 간단히 말해 내가(객체)가 가지고 있어야 하는 특성을 외부에서 가져오는 것을 말하는데, 가져오는 방식이 상속을 통하는 방법과 다르게 외부에서 주입되는 방식이다. 상속의 경우는 폴리모피즘이 반드시 필요하면 제한적으로 사용하되, 컴포지션을 통해서 객체를 구축하는 방식을 추천한다. 

이 글에서는 Golang에서 컴포지션을 어떻게 지원하는지 살펴 볼 것이다. 자바/C++ 보다는 훨씬 깊숙히 임베디드 되는 모습을 보게 될 것이란 것을 미리 귀뜸해 둔다.

먼저 자바의 컴포지션을 살펴보자. 

abstract class Form{
public void run(){};
}
class WolfForm extends Form{
public void run (){
System.out.println("늑대처럼 달린다!!!");
}
}
class Player{
public String name;
public Form form;
public Player(String name, Form form){
this.name = name;
this.form = form;
}
}
public class Main {
public static void main(String[] args) {
Player player = new Player("hama", new WolfForm());
player.form.run();
}
}

Player 객체는 Wolf 폼 객체를 포함(컴포지션)하고 있으며, Player객체에서 Wolf폼 객체의 기능을 사용하려면 당연히player.wolf.run() 식으로 내부에서 wolf 로 접근한 후에 사용한다. 근데  Golang에서는??
그냥 player.run()으로 접근가능하다.

Golang에서의 컴포지션 

*  객체/값의 구분을 하지 않았음을 알려드린다. 이 글에서는 포인팅되는지 값 자체인지의 구분이 중요치 않다.

type Form interface{
Run()
}
type WolfForm struct{
}
func (wolf *WolfForm) Run() {
fmt.Print("늑대처럼 달린다!!!");
}
type Player struct{
Form
}
func main(){
player := &Player{Form : &WolfForm{}}
player.Run()
}

Player 구조체는 Form을 혼연일체 (임베디드) 되어 가지고 있게 된다. 즉 자신의 메소드인 양 그냥 사용 해 버릴수 있다. 어찌보면 강력해 보이기도 하는데, 이게 코드읽기/관리가 될 때는 좀 피곤 할 수 있을 것이다. 
자바의 경우 player.wolf.run()으로 관계가 명시적으로 코드에 보이는 반면에, Golang식의 player.run()은 도대체 run은 어디에 구현되어있는지 알수 없으면, 찾아보는 수고를 해야한다. player 내부의 Form 인터페이스를 확인해야하며, 이 Form인터페이스를 덕타이핑으로 구현상속한 녀석을 또 찾아봐야한다. player 내부에 Form인터페이스 말고 다른것들도 있다면?? 두배로 힘들어지게 된다. 아래 이더리움 코드를 통해 피부로 느껴보자.

이더리움코드에서의  컴포지션 
[이더리움 코어] DevP2P 소스코드 분석 (feat. golang) 를 같이 읽으면  좋을 거 같다.

이더리움에서 P2P 부분을 보면 리모트서버와 접속이 이루어짐과 동시에 conn 객체가 만들어 지는데,
conn 구조체는 아래와 같다. 위에 공부해 보았듯이 transport 가 컴포지션 되었다는 것을 알 수 있다.

type conn struct {
fd net.Conn
transport
flags connFlag
cont chan error
id discover.NodeID
caps []Cap
name string
}

conn 객체에는 net.Conn이라는 소켓파일디스크립터과 그 fd 를 가지고transporting 하는 방식을 책임지는 transport 인터페이스를 구현상속한 녀석을 가지고 있다. 그럼 transport는 무엇을 하는 것일까? transport 인터페이스를 살펴보자.

type transport interface {
doEncHandshake(prv *ecdsa.PrivateKey, dialDest *discover.Node)(discover.NodeID, error)
doProtoHandshake(our *protoHandshake) (*protoHandshake, error)
MsgReadWriter
close(err error)
}

리모트노드와 접속이 이루어지면 Encrypt와 Protocol 정보를 교환해야하는 책임을 가지고 있으며, 
MsgReadWriter 인터페이스를 구현상속한 또 어떤 녀석을 통해서 리모트노드와 읽고,쓰기를 하는거 같다.
이더리움에서는 메세지를 시리얼라이즈 하는데  RLP 라는 인코딩/디코딩 알고리즘을 사용하는데, MsgReadWriter 인터페이스를 구현한 녀석은 RLP의 능력도 가지고 있다. 

이제 conn 객체의 생성 코드를 살펴보자. 확인해 볼것은 2가지인데 하나는 transport 인터페이스는 어떤 객체로 만들어지나이고,  transport 인터페이스가 가지고 있는 MsgReadWriter 인터페이스 또한 어떤 객체로 만들어 지나이다.

func (srv *Server) SetupConn(fd net.Conn, flags connFlag, dialDest *discover.Node) error {
...
c := &conn{fd: fd, transport: srv.newTransport(fd), flags: flags, cont: make(chan error)}
...
}

transport 에 srv.newTransport(fd) 가 컴포지션 되고 있다.

srv.newTransport = newRLPX

func newRLPX(fd net.Conn) transport {
return &rlpx{fd: fd}
}

rlpx 객체를 만드는 것 같다. (내부에 fd 를 가지고 있으며, rlp 엔코딩/디코딩으로 시리얼라이즈를 한 후에 fd로 소켓read/write호출)  

type rlpx struct {
fd net.Conn

rmu, wmu sync.Mutex
rw *rlpxFrameRW
}

rlpx 구조체는 위와 같으며, 이 구조체는 transport 인터페이스가 가지고 있는 메소드를 모두 덕타이핑(자바처럼 extends로 명시적으로 상속한다고 말하지 않아도 동일한 함수시그니처를 구현하고 있으면 폴리모피즘적으로 같다라고 봄) 으로 포함 할 것이다. 그래야만 rlpx 가 transport가 될 수 있으니~ 아래와 같이~

func (t *rlpx) doProtoHandshake(our *protoHandshake) (their *protoHandshake, err error) {

....서로가 가진 프로토콜 정보 확인 ...난 eth63을 가지고있는데 넌 무엇을?
}

func (t *rlpx) doEncHandshake(prv *ecdsa.PrivateKey, dial *discover.Node) (discover.NodeID, error) {
....서로가 가진 암호화 정보 확인 ECC기반...auth정보 교환..
t.rw = newRLPXFrameRW(t.fd, sec)
...
}

마지막으로 transport 인터페이스에서 선언한 MsgReadWriter 또한 transport를 구현상속한 rlpx 가 가지고 있어야 하는데 어디 있을까? 다시 rlpx 구조체를 보자.

type rlpx struct {
fd net.Conn

rmu, wmu sync.Mutex
rw *rlpxFrameRW
}

rlpxFrameRW가 뭔가 읽고,쓰기와 관련되어 있는 감이 오지 않나? (자바라면 명시적으로 보였을텐데..) 
그럼rlpxFrameRW가 생성되는 곳을 살펴봐야한다. 이것은 위의 doEncHandshake 메소드 안에 있다.

t.rw = newRLPXFrameRW(t.fd, sec)

newRLPXFrameRW 는 이렇다.

func newRLPXFrameRW(conn io.ReadWriter, s secrets) *rlpxFrameRW {
...
return &rlpxFrameRW{
conn: conn,
enc: cipher.NewCTR(encc, iv),
dec: cipher.NewCTR(encc, iv),
macCipher: macc,
egressMAC: s.EgressMAC,
ingressMAC: s.IngressMAC,
}
}

rlpxFrameRW 라는 구조체를 만든다. 이쯤되면 감 잡힐 것이다. 이 구조체는 MsgReadWriter 인터페이스를 구현 상속 했다는 것을..(RW라는 이름을 통해서 유추해야한다. 따라서 Golang에서 이름짓기는 더 중요할 것이다) 

type MsgReadWriter interface {
MsgReader
MsgWriter
}}

type MsgReader interface {
ReadMsg() (Msg, error)
}
type MsgWriter interface {
WriteMsg(Msg) error
}

MsgReadWriter 인터페이스는 MsgReader/MsgWriter라는 인터페이스를 또  가지고 있으며, rlpx 에서는 ReadMsg/WriteMsg만 덕타이핑해 놓으면 될 것이다. 아래 코드에서 확인하자.

func (t *rlpx) ReadMsg() (Msg, error) {
t.rmu.Lock()
defer t.rmu.Unlock()
t.fd.SetReadDeadline(time.Now().Add(frameReadTimeout))
return t.rw.ReadMsg()
}

func (t *rlpx) WriteMsg(msg Msg) error {
t.wmu.Lock()
defer t.wmu.Unlock()
t.fd.SetWriteDeadline(time.Now().Add(frameWriteTimeout))
return t.rw.WriteMsg(msg)
}

이렇게 이더리움에서 컴포지션이 이루어지는 모습을 살펴보았다. 어떻게 느껴졌는가?? 강력하다? 헷갈린다?? 모든 기술은 장,단이 있으며 사람의 취향에 따라서 달라질 것이다. 자바스러움이 좋은 사람, Golang스러움이 좋은 사람~개인적으로는 자바스러움이 좋긴한데, Golang의 동시성  철학과 빠른컴파일속도에 점수를 주어서 Golang에 집중하고 있다. 언어 선택에 더 중요한것은 내가 지금 하는 일에 어떤 언어가 더 가깝냐는것이 겠지만~




윤대근님의 하이퍼레저 패브릭으로 배우는 블록체인의 내용을 참조하여 구성되었습니다.

간단정리:

Fabric-CA를 통한 보안메터리얼들 생성 

가. Fabric-CA 를 이용해서 각 조직의 admin msp 를 생성한다.
나. 각 조직의 admin은 조직이 가지고 있는 각 Peer의 msp 를 생성한다.
                   ---- 여기까지가 8번 -----

인프라 (채널등) 구성하고 시작하기 
다. genesis.block 을 각 조직의 공개키를 가지고 생성하여 오더러를 구동한다. 
라. 각 peer 들을 구동한다.
마. 채널을 생성한다.
바. 채널에 peer들을 참여시킨다.
사. 앵커피어 업데이트
                 ------ 여기까지가 15번 ----

체인코드 설치 및 읽고,쓰기
아. 각 Peer에 체인코드 설치
자. 하나의 peer 에서 체인코드 인스턴스화
차. 분산원장 읽고/쓰기 


설치도우미들

-  
FABRIC_CFG_PATH 설정 : core.yaml , configtx 관련요소를 실행시키기위한 패스정보. 
-  configtx.yaml: 컨소시엄정보,조직정보,채널정보,오더러설정,제네시스블록,앵커피어등을 생성하기위한 설정파일이다.
- core.yaml : peer를 실행시키기 위한 설정파일을 담고 있다. 
- orderer.yaml : orderer 를 실행시키기 위한 설정파일을 담고 있다. 

+ Recent posts