하이퍼레저 패브릭 MSP 

MSP는 하이퍼레저 패브릭에서 각 피어와 사용자에 대한 인증/인가 작업에 대한 추상층입니다. 퍼블릭 블록체인과는 다른 콘소시엄 블록체인에서만 복잡하게 존재하는 모듈로써,  구현하는거 자체도 PKI의 복잡성을 그대로 물려받기 때문에 복잡하지만 실제 콘소시엄 블록체인을 구축하여 조직들간의 거버넌스 정책을 만들어 나가는 프로세스는 더욱 더 복잡하지 않을 까 싶습니다. 즉 새로운 조직을 어떻게 추가시키는지 같은? 현재 대부분의 패브릭 프로젝트에서 설립자 주도적 네트워크 구성을 하고 있는것으로 아는데 (즉 조직간 합의가 필요없음. 그냥 짱이 알아서 하는?) 진짜 조직별로 권한이 균등이 나누어져 있는 네트워크에서의 실제 사용사례에 대한 레퍼런스에 대한 공개가 기대됩니다.

이 글에서는 하이퍼레저 패브릭에서 사용되는 MSP를 간단하게 피어계열인증서/사용자계열인증서 기반의 트랜잭션을 일으키고 오더러에서 확인하는 과정을 코드로써 살펴 볼 것입니다.  

하이퍼레저 패브릭에서 암호화 재료 만들기 

하이퍼레저 패브릭에서는 3가지 방식중 선택하여 암호화 재료를 만들 수 있습니다.

1. OpenSSL을 이용하여 (테스팅) 
2. CryptoGen 유틸리티를 이용하여 (테스팅)
3. Fabric-CA 서비스를 이용하여  (프로덕트)

심플 MSP를 만들어보기


진행중..

MSP는 하이퍼레저 패브릭에서 각 피어와 사용자에 대한 인증/인가 작업에 대한 추상층입니다. 퍼블릭 블록체인과는 다른 콘소시엄 블록체인에서만 복잡하게 존재하는 모듈로써,  구현하는거 자체도 PKI의 복잡성을 그대로 물려받기 때문에 복잡하지만 실제 콘소시엄 블록체인을 구축하여 조직들간의 거버넌스 정책을 만들어 나가는 프로세스는 더욱 더 복잡하지 않을 까 싶습니다. 즉 새로운 조직을 어떻게 추가시키는지 같은? 현재 대부분의 패브릭 프로젝트에서 설립자 주도적 네트워크 구성을 하고 있는것으로 아는데 (즉 조직간 합의가 필요없음. 그냥 짱이 알아서 하는?) 진짜 조직별로 권한이 균등이 나누어져 있는 네트워크에서의 실제 사용사례에 대한 레퍼런스에 대한 공개가 기대됩니다.

이 글에서는 하이퍼레저 패브릭에서 사용되는 MSP를 간단하게 구현해보는 시간을 갖으려 합니다. 해싱/암호화/PKI 등에 대한 기본지식은 알고 있다고 가정하며(사실 읽으면서 찾아보면 됩니다) 첫번째로는 각 기술에 대한 코딩 지식 기반과 무엇을 할 수 있는지에 대한 감을 잡고, 후에는 실제 하이퍼레저패브릭 심플 시뮬레이션을 만들어서 피어계열인증서/사용자계열인 증서 기반의 트랜잭션을 일으키고 오더러에서 확인하는 과정을 코드로써 살펴 볼 것 입니다.  

Go언어의 Crypto라이브러리 

go 언어가 지원하는 표준 라이브러리들 입니다. 이를 이용한 여러가지 암호화 관련 된 코드는 더 아래에서 살펴보죠. 

aes : Package aes implements AES encryption (formerly Rijndael), as defined in U.S. Federal Information Processing Standards Publication 197.
cipher:  Package cipher implements standard block cipher modes that can be wrapped around low-level block cipher implementations.
des: Package des implements the Data Encryption Standard (DES) and the Triple Data Encryption Algorithm (TDEA) as defined in U.S. Federal Information Processing Standards Publication 46-3.
dsa: Package dsa implements the Digital Signature Algorithm, as defined in FIPS 186-3.
ecdsa: Package ecdsa implements the Elliptic Curve Digital Signature Algorithm, as defined in FIPS 186-3.
elliptic: Package elliptic implements several standard elliptic curves over prime fields.
hmac: Package hmac implements the Keyed-Hash Message Authentication Code (HMAC) as defined in U.S. Federal Information Processing Standards Publication 198.
md5: Package md5 implements the MD5 hash algorithm as defined in RFC 1321.
rand: Package rand implements a cryptographically secure pseudorandom number generator.
rc4: Package rc4 implements RC4 encryption, as defined in Bruce Schneier’s Applied Cryptography.
rsa: Package rsa implements RSA encryption as specified in PKCS#1.
sha1: Package sha1 implements the SHA1 hash algorithm as defined in RFC 3174.
sha256: Package sha256 implements the SHA224 and SHA256 hash algorithms as defined in FIPS 180-4.
sha512: Package sha512 implements the SHA-384, SHA-512, SHA-512/224, and SHA-512/256 hash algorithms as defined in FIPS 180-4.
subtle: Package subtle implements functions that are often useful in cryptographic code but require careful thought to use correctly.
tls: Package tls partially implements TLS 1.2, as specified in RFC 5246.
x509: Package x509 parses X.509-encoded keys and certificates.
pkix: Package pkix contains shared, low level structures used for ASN.1 parsing and serialization of X.509 certificates, CRL and OCSP. 

X.509 인증서 구조 

자신의 공개키를 인증 받기 위해 CA의 비밀키를 이용해서 만든 CA서명증서입니다.검증은 CA셀프인증서를 통해서 할 수 있습니다. 구체적으로는 CA(or Intermediate CA) 공개키로 위의 그림에서 서명알고리즘으로 복호화 한것과 서명이 일치하면 OK란거죠. 서명알고리즘에는 RSA계열과 ECDSA계열이 있습니다.


Go언어를 이용한 암호화 코딩 예제 

0. HMAC 을 이용한 메세지 해싱 

HMAC:  hash message authentication code  즉 해싱에 의한 메세지 검증/인증으로 비밀키에 의해  -> 해싱은 되나  <- 반대 방향의 복구는 불가능한 , 즉 한방향으로만 작동하기 때문에 보통 확인 용도(보낸 내용이 무엇인지가 아니라, 보낸 내용이 보낼 때와 변경되진 않았는지 or 이미 저장되어 있는 정보와 일치하는지) 로 사용됩니다. 또한해싱의 특성상 원래의 것 보다 더 짧은 길이의 값이나 키로 변환해서 이득을 얻을 수 있는 많은 곳에 사용됩니다.

"crypto/hmac"
"crypto/sha256"
"crypto/sha512"


func main() {
message := "Today web engineering has modern apps ..."
salt := generateSalt()
fmt.Println("Message: " + message)
fmt.Println("\nSalt: " + salt)

hash := hmac.New(sha256.New, []byte(secretKey))
io.WriteString(hash, message+salt)
fmt.Printf("\nHMAC-Sha256: %x", hash.Sum(nil))

hash = hmac.New(sha512.New, []byte(secretKey))
io.WriteString(hash, message+salt)
fmt.Printf("\n\nHMAC-sha512: %x", hash.Sum(nil))
}


1. AES 대칭키를 이용한 암호화/복호화 

서로 공유하고 있는 동일한 키(대칭키라고 함)로 암호화하고 복호화 하는 방식에 관한 코드입니다.

"crypto/aes"


func encrypt(key []byte, text string) (string, error) {
block, err := aes.NewCipher(key)
cfb := cipher.NewCFBEncrypter(block, iv)
... }

func decrypt(key []byte, text string) (string, error) {
block, err := aes.NewCipher(key)
cfb := cipher.NewCFBDecrypter(block, iv)
...
}


2. RSA 개인키/공개키 생성

 RSA 기반의 개인키와 공개키를 생성합니다. 대칭키와 다르게 2가지의 키가 생성됩니다.


rsaPriKey, rsaPubKey := GenerateKeyPair(1024) // 개인키와 공개키 쌍 생성


3. PEM EXPORT / IMPORT 

 개인키,공개키 혹은 인증서등을 pem 파일로 export/import 할 수 있습니다.여기서는 확장자를 pem으로 하였지만 crt (인증서) , key (개인키) 등으로 할 수도 있습니다.
func main(){

   // GENERATE
   privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
   publicKey := &privateKey.PublicKey

   // WRITE to pem file 
   pemPrivateFile, err := os.Create("private_key.pem")
   if err != nil {
      fmt.Println(err)
      os.Exit(1)
   }

   var pemPrivateBlock = &pem.Block{
      Type:  "RSA PRIVATE KEY",
      Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
   }

   err = pem.Encode(pemPrivateFile, pemPrivateBlock)
   if err != nil {
      fmt.Println(err)
      os.Exit(1)
   }
   pemPrivateFile.Close()

   // READ From pem file 
   privateKeyFile, err := os.Open("private_key.pem")
   if err != nil {
      fmt.Println(err)
      os.Exit(1)
   }

   pemfileinfo, _ := privateKeyFile.Stat()
   size := pemfileinfo.Size()
   pembytes := make([]byte, size)
   buffer := bufio.NewReader(privateKeyFile)
   _, err = buffer.Read(pembytes)
   data, _ := pem.Decode([]byte(pembytes))
   privateKeyFile.Close()

   privateKeyImported, err := x509.ParsePKCS1PrivateKey(data.Bytes)
  
}


4. RSA 암호화/복호화

 hello를 공개키로 암호화하고, 개인키로 복호화 합니다. (키쌍을 암호화에 사용) 

////////// Encrypt / Decrypt ///////

rsaPriKey, rsaPubKey := GenerateKeyPair(1024)  // 개인키와 공개키 쌍 생성

encryptedMsg := EncryptWithPublicKey([] byte ("hello"), rsaPubKey2 ) // 공개키로 암호화
decryptedMsg := DecryptWithPrivateKey(encryptedMsg, rsaPriKey2) // 개인키로 복호화 


5. RSA 서명/확인

 hello를 개인키로 서명하고, 공개키로 확인 합니다. (키 쌍을 서명을 위해 사용 + 자기부인 방지)

////////// Sign / Verify  ////////////

alicePrivateKey, err := rsa.GenerateKey(rand.Reader, 2048) // 개인키와 공개키 쌍 생성 
alicePublicKey := alicePrivateKey.PublicKey

secretMessage := "Hello 8gwifi.org"

signature := SignPKCS1v15(secretMessage,*alicePrivateKey) // 개인키로 서명  
verif := VerifyPKCS1v15(signature, secretMessage,  alicePublicKey ) // 공개키로 확인 


6. ECDSA 개인키/공개키 생성 

 하이퍼레즈 패브릭이나 이더리움에서는 RSA 말고 ECDSA를 사용합니다.

pubkeyCurve := elliptic.P256() //see http://golang.org/pkg/crypto/elliptic/#P256

privatekey := new(ecdsa.PrivateKey)
privatekey, err := ecdsa.GenerateKey(pubkeyCurve, rand.Reader)

if err != nil {
   fmt.Println(err)
   os.Exit(1)
}

var pubkey ecdsa.PublicKey
pubkey = privatekey.PublicKey


7. ECDSA 서명/확인

RSA에서와 마찬가지로 서명/확인에 사용 할 수 있습니다.
pubkeyCurve := elliptic.P256() //see http://golang.org/pkg/crypto/elliptic/#P256

privatekey := new(ecdsa.PrivateKey)
privatekey, err := ecdsa.GenerateKey(pubkeyCurve, rand.Reader) 

var pubkey ecdsa.PublicKey
pubkey = privatekey.PublicKey

// Sign ecdsa style
var h hash.Hash
h = md5.New()
r := big.NewInt(0)
s := big.NewInt(0)

io.WriteString(h, "This is a message to be signed and verified by ECDSA!")
signhash := h.Sum(nil)

r, s, serr := ecdsa.Sign(rand.Reader, privatekey, signhash)

signature := r.Bytes()
signature = append(signature, s.Bytes()...)

// Verify
verifystatus := ecdsa.Verify(&pubkey, signhash, r, s)
fmt.Println(verifystatus) 


8. ECDH 로 (비밀키 전송없이) 비밀키 공유하기

간단히 말해서 나의 비밀키 * 상대방의 공개키 == 상대방의 비밀키 * 나의 공개키 라는 등식을 이용해서, 공통의 비밀키를 갖는 방식입니다. 이더리움에서는 피어끼리 접속 할 때마다 임시의 공개키/개인키를 만들어서 (참고로 임시가 아닌 static 공개키는 바로 자신의 주소죠) ECDH를 이용하여 만든 비밀키로 RLP인코딩한 패킷을 암호화하여 송/수신합니다. 
var privKey1, privKey2 crypto.PrivateKey
var pubKey1, pubKey2 crypto.PublicKey

var err error
var ok bool
var secret1, secret2 []byte

privKey1, pubKey1, err = e.GenerateKey(rand.Reader)
privKey2, pubKey2, err = e.GenerateKey(rand.Reader)


secret1, err = e.GenerateSharedSecret(privKey1, pubKey2)
secret2, err = e.GenerateSharedSecret(privKey2, pubKey1)

if !bytes.Equal(secret1, secret2) {
   log.Print("The two shared keys: %d, %d do not match", secret1, secret2)
}


9. PKI 시스템 (CA생성,서명하기,서명 확인하기) 


웹서비스 회사에서 https 서비스를 하기 위해 Verisign, Comodo, Thawte, GeoTrust, GlobalSign 같은 CA로 부터 자신의 공개키를 그들의 개인키로 서명한 인증서(. X.509)를 받아서 사용합니다. 이렇게 CA를 통하여 인증을 받은 공개키를 통해 신원을 검증하고 비밀키 교환을 하는 일련의 과정에 인프라를 PKI라고 합니다. 

하이퍼레저 패브릭 같은 콘소시엄형 블록체인은 각 조직에 대한 허가/인증이 중요한 기능이기에 이런 PKI를 시스템내에 직접 구현하여 활용합니다. 먼가 복잡해 보이지만  개인키,공개키 만드는 로직/인증서 만드는 로직/ 인증서 검증하는 로직만 있으면 만들 수 있습니다. 
- CA 자가서명 (CA는 루트이므로 자기 스스로 서명하게 됩니다.)
func main() {
   ca := &x509.Certificate{
      SerialNumber: big.NewInt(1653),
      Subject: pkix.Name{
         Organization:  []string{"ORGANIZATION_NAME"},
         Country:       []string{"COUNTRY_CODE"},
         Province:      []string{"PROVINCE"},
         Locality:      []string{"CITY"},
         StreetAddress: []string{"ADDRESS"},
         PostalCode:    []string{"POSTAL_CODE"},
      },
      NotBefore:             time.Now(),
      NotAfter:              time.Now().AddDate(10, 0, 0),
      IsCA:                  true,
      ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
      KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
      BasicConstraintsValid: true,
   }


   privateKey, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) // ecdsa.PrivateKey
   publicKey := &privateKey.PublicKey // ecdsa.PublicKey

   ca_b, err := x509.CreateCertificate(rand.Reader, ca, ca, publicKey, privateKey)

   if err != nil {
      log.Println("create ca failed", err)
      return
   }

   // Public key
   certOut, err := os.Create("ca.crt")
   pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: ca_b})
   certOut.Close()
   log.Print("written cert.pem\n")

   // Private key
   keyOut, err := os.OpenFile("ca.key", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
   x509Encoded, _ := x509.MarshalECPrivateKey(privateKey)
   pem.Encode(keyOut, &pem.Block{Type: "ECDSA PRIVATE KEY", Bytes: x509Encoded})
   keyOut.Close()
   log.Print("written key.pem\n")
}

- CA를 이용한 개인 인증서 생성 및 확인 (개인들은 자신의 공개키/개인키를 만든 후에 공개키를 CA의 개인키를 이용하여 서명하고 X.509구조의 인증서를 만들게 됩니다) 
package main

import (
   "bufio"
   "crypto/ecdsa"
   "crypto/elliptic"
   "crypto/rand"
   "crypto/tls"
   "crypto/x509"
   "crypto/x509/pkix"
   "encoding/pem"
   "fmt"
   "log"
   "math/big"
   "os"
   "time"
)

func main() {

   // Load CA
   catls, err := tls.LoadX509KeyPair("ca.crt", "ca.key")
   if err != nil {
      panic(err)
   }
   ca, err := x509.ParseCertificate(catls.Certificate[0])
   if err != nil {
      panic(err)
   }

   cert := &x509.Certificate{
      SerialNumber: big.NewInt(1653),
      Subject: pkix.Name{
         Organization:  []string{"ORGANIZATION_NAME"},
         Country:       []string{"COUNTRY_CODE"},
         Province:      []string{"PROVINCE"},
         Locality:      []string{"CITY"},
         StreetAddress: []string{"ADDRESS"},
         PostalCode:    []string{"POSTAL_CODE"},
      },
      NotBefore:             time.Now(),
      NotAfter:              time.Now().AddDate(10, 0, 0),
      IsCA:                  true,
      ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
      KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
      BasicConstraintsValid: true,
   }


   privateKey, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) // ecdsa.PrivateKey
   publicKey := &privateKey.PublicKey // ecdsa.PublicKey

   cert_b, err := x509.CreateCertificate(rand.Reader, ca, ca, publicKey, privateKey)

   if err != nil {
      log.Println("create ca failed", err)
      return
   }

   // Public key
   certOut, err := os.Create("bob.crt")
   pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: cert_b})
   certOut.Close()
   log.Print("written cert.pem\n")

   // Private key
   keyOut, err := os.OpenFile("bob.key", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
   x509Encoded, _ := x509.MarshalECPrivateKey(privateKey)
   pem.Encode(keyOut, &pem.Block{Type: "ECDSA PRIVATE KEY", Bytes: x509Encoded})
   keyOut.Close()
   log.Print("written key.pem\n")


   // get pubic key from certificate
   cert, _ = x509.ParseCertificate(cert_b)
   ecdsaPublicKey := cert.PublicKey.(*ecdsa.PublicKey)
   log.Print(ecdsaPublicKey)

   // get pubic key from certificate pem file
   certFile, err := os.Open("bob.crt")
   if err != nil {
      fmt.Println(err)
      os.Exit(1)
   }

   pemfileinfo, _ := certFile.Stat()
   size := pemfileinfo.Size()
   pembytes := make([]byte, size)
   buffer := bufio.NewReader(certFile)
   _, err = buffer.Read(pembytes)
   data, _ := pem.Decode([]byte(pembytes))
   certFile.Close()


   cert2, _ := x509.ParseCertificate(data.Bytes)
   ecdsaPublicKey2 := cert2.PublicKey.(*ecdsa.PublicKey)
   log.Print(ecdsaPublicKey2)

   // certificate validation
   checkSign:= cert2.CheckSignatureFrom(ca)
   if checkSign == nil {
      log.Print("check ok")
   }

}

위 예제들의 완전한 코드는 이런 예제를 이용하여 하이퍼레저 패브릭 MSP를 시뮬레이션 해보는 작업과 함께 다음 글에서 MSP 코드와 함께 Github에 올릴 예정입니다. 


다음 글: 400라인의 go코드로 구현한 하이퍼레저 패브릭 [4-1]-  MSP (2) (작성중..) 




레퍼런스:

https://fale.io/blog/2017/06/05/create-a-pki-in-golang/ 
https://8gwifi.org/docs/go-rsa.jsp 
https://github.com/hyperledger/fabric/blob/release-1.4/common/tools/cryptogen/main.go 
https://github.com/ethereum/go-ethereum/tree/master/p2p 
https://github.com/hyperledger/fabric/tree/release-1.4/msp 



CFT 는 분산시스템에서 노드가 비정상적인 충돌에 의해 문제가 생기더라도 나머지 시스템에서 서비스를 할 수 있게 하는 작동을 말한다면, BFT는 의도적 악의에 의한 문제까지도 해결하는 방식을 말한다. 

블록체인 시스템에선 둘 모두 합의라는 방식을 거치게 되는데, 비트코인의 경우는 일반적인 CFT, BFT 보다는 훨씬 더 극한노동(?) 들어가는 신뢰 작업이 추가되는데 바로 POW이며, 콘소시엄형 블록체인 시스템(이라고 하고 하이퍼레저 패브릭) 에서는 보통 조직들이 이미 신원확인등에 의한 허가를 받은 상태에서 참여하기 때문에 악의적인 행위를 안한다고 치고 서비스를 하기에, 서버가 맛가는 경우에 대해서만 방어하는 CFT 기반의 오더링 알고리즘이 우선되고 있다. 

CFT의 대표격이며, 하이퍼레저 패브릭 2.0 버전에서 등장할 가능성이 높은 RAFT와 BFT계열의 알고리즘을 Go언어로 심플하게 구현할 계획인데 아이디어와 결과는 새로운 포스트로 올릴 예정이다.

RAFT 참고 레퍼런스는 아래와 같다.
D. Ongaro et al.의 In Search of an Understandable Consensus Algorithm 
Hyperledger Fabric 2.0 RAFT Proposal 


예제로 이용된 재료는 이전에 만든  400라인의 go코드로 구현한 하이퍼레저 패브릭 [2]- 블록전파/Gossip 프로토콜 소스(앞으로는 gossip분산 서비스로 지칭)를 사용였는데 간단한 분산 네트워킹 예제이므로 도커/쿠버네이트 공부를 위한 좋은 재료가 될 것 입니다. 이 글은 그 예제의 연속성 상에서 기획된 글이며, 도커/쿠버네이트 내용을 한번 정도 읽어 봤다는 혹은 아래 참고 링크를 공부하면서 진행한다는 가정하에 실습을 위해 정리 한 포스트임을 알려드립니다. 각각의 기술에 대해 구체적으로 알고 싶은 분은 아래 레퍼런스를 참고 하시거나, 추가 구글링을 통해 확인 하십시요.

도커

아래 처럼 Dockerfile 을 만듭니다.

# Start from a Debian image with the latest version of Go installed
# and a workspace (GOPATH) configured at /go.
FROM golang

# Copy the local package files to the container's workspace.
ADD . /go/src/github.com/wowlsh93/hyperledger-fabric-400-gossip

# Build the gossip command inside the container.
# (You may fetch or manage dependencies here,
# either manually or with a tool like "godep".)
RUN go install github.com/wowlsh93/hyperledger-fabric-400-gossip/gossip

위의 도커파일을 가지고 gossip이라는 이름의 도커이미지를 만듭니다. 

docker build -t gossip .

기반 이미지는 golang 을 사용하며, 빌드시 현재 프로젝트(예제를 확인하시고 github 에서 가져오세요)를 컨네이너의 /go/src/github.com/wowlsh93/hyperledger-fabric-400-gossip 위치에 복사해 놓습니다. 그리고 소스를 인스톨하여 go/src/gossip 에 실행파일을 위치 시켜 놓습니다.

docker images

만들어진 이미지를 확인 하는 명령어는 위와 같습니다. 

docker run -p 28000:28000 --name gossip1 --rm gossip /go/bin/gossip -name 127.0.0.1:28000 -ip 127.0.0.1 -port 28000 -leader

실행은 run 명령어를 사용하며 옵션이 의미하는 바는  

-p 옵션으로 외부포트:내부포트를 포워딩 하고
-name : 이름은 gossip1 으로 임의로 정합니다.
-rm :  옵션으로 컨테이너가 내려가면 자동으로 삭제되게 하고 
컨테이너 실행시  /go/bin/gossip -name 127.0.0.1:28000 -ip 127.0.0.1 -port 28000 -leader 명령을 실행해 리더피어 서비스를 시작해 줍니다.  

두번째 터미널을 열어서 리더피어 말고 일반 피어를 위해 컨테이너를 띄어 봅시다. 
docker run -p 28001:28001 --net host --name  gossip2 --rm 
gossip /go/bin/gossip -name 127.0.0.1:28001 -ip 127.0.0.1 -port 28001  -bootstrap 127.0.0.1:28000
근데 컨테이너를 띄우면 에러가 나는데  컨테이너간 네트워킹을 할 수 없기 때문이며 이를 해결하기 위해 네트워킹 설정을 해야 합니다. 

도커 네트워킹 

1) 같은 노드에 있는 컨테이너 실습 - host 방식 

도커를 처음 설치하면 none, host, bridge 세 가지 종류의 네트워크가 자동으로 설정되는데  none방식(-net=none) 으로 하면 격리된 네트워킹을 갖기 때문에 안되고,  -net 옵션으로 host를 주면 모든 컨테이너가 호스트 네트워크를 같이 사용하게 되기 때문에 컨테이너간에 네트워킹이 가능해 집니다.

docker run -p 28000:28000 --net host --name gossip1 --rm gossip /go/bin/gossip -name 127.0.0.1:28000 -ip 127.0.0.1 -port 28000 -leader

위 처럼 -net host 를 각 컨테이너를 시작 할 때 넣어 주면 됩니다. 동일한 네트워크에 있기 때문에 포트만으로도 통신이 되는거죠.


2) 같은 노드에 있는 컨테이너 실습 - bridge 방식 

브리지방식은 도커의 디폴트 방식이기에 -net 옵션 없으면 자동으로 선택되는데 브리지 방식으로는 각각의 컨테이너가 다른 사설IP 를 갖게 되므로 포트만을 구분하는 예제(Gossip 서비스)에서는 안되는게 당연하며 추가정보가 필요합니다. 

docker network create --driver bridge gossip-network

일단 bridge형식의 네트워크를 하나 추가로 만듭니다. 이름은 gossip-network 로 하구요.

docker network ls

NETWORK ID NAME DRIVER SCOPE d87103af938c bridge bridge local 659368a11889 composer_default bridge local 17b35b1ad8c5 gossip-network bridge local 541d637a227c host host local de0e0715ded6 net_basic bridge local 6b6f56779a9a none null local

docker network ls 명령어로 gossip-network 가 만들어진것을 확인합니다.

docker run -it --network=gossip-network --name gossip1 gossip /go/bin/gossip -name gossip1:28000 -ip gossip1 -port 28000 -leader
docker run -it --network=gossip-network --name gossip2 gossip /go/bin/gossip -name gossip2:28001 -ip gossip2 -port 28001 -bootstrap gossip1:28000
docker run -it --network=gossip-network --name gossip3 gossip /go/bin/gossip -name gossip2:28002 -ip gossip3 -port 28002 -bootstrap

이제 각 도커 컨테이너를 시작시 옵션으로 -network=gossip-network 로 네트워크 설정을 하고, 불편한 IP주소 대신 gossip1~3처럼 컨네이너 명으로 컨테이너 끼리 통신 하게 할 수 있습니다. 컨테이너 끼리 NAT 테이블 안에서 브리지로 통신하기 때문에 굳이 -p 옵션으로 포트포워딩은 필요 없습니다. 참고로 다시 시작 할 때 컨네이터가 중단된 채 존재하기 때문에 안될 수 있는데 삭제 한 후에 해야합니다. 전체 컨테이너 삭제 명령어는 docker rm `docker ps -a -q` 입니다.


3) 같은 노드에 있는 컨테이너 실습 -  Docker Compose 

version: '2'
services:
leader:
image: gossip
container_name: gossip1
command: /go/bin/gossip -name gossip1:28000 -ip gossip1 -port 28000 -leader
restart:
on-failure


peer1:
image: gossip
container_name: gossip2
command: /go/bin/gossip -name gossip2:28001 -ip gossip2 -port 28001 -bootstrap gossip1:28000
restart:
on-failure
depends_on:
- leader
peer2:
image: gossip
container_name: gossip3
command: /go/bin/gossip -name gossip3:28002 -ip gossip3 -port 28002 -bootstrap gossip1:28000
restart:
on-failure
depends_on:
- leader

도커 컴포즈는 여러개의 컨테이너를 한방에 시작 시킬때 유용합니다. docker-compose.yaml  파일을 만들어 위의 내용을 작성합니다. 각 서비스를 구분했으며  옵션은 쉽게 이해 될 것입니다. 주의 할 점은 일반 피어는 리더피어가 생성된 후에 떠야하기때문에 depends_on옵션이 추가되었습니다. 

docker-compose -p gossipnetwork up -d

-p옵션으로 네트워크를 설정해주며,  -d 는 detached mode으로 로그가 안 보이기 때문에. -d 옵션을 빼거나 

docker logs -t -f gossip1

같은 명령어로 각 컨테이너들의 stdout 로그를 확인합니다.


4) 다른 노드에 있는 컨테이너 실습 - Overlay Network by etcd

일단 만들어진 이미지를 도커허브에 올려서 각 노드에서 편하게 사용 하려고 합니다.  참고

이제 어디서에나 도커만 있다면 이미지를 다운로드 (이미지 이름이 gossip 에서 wowlsh93/gossip:1 로 변경됨) 받아서 실행 할 수 있습니다.

docker pull wowlsh93/gossip:1

도커(컨테이너)의 장점이 gossip 서비스를 돌리기 위한 어떤 환경(go컴파일러등)이 갖추어져 있지 않더라도, 도커만 있는 곳이라면 실행 할 수 있어서 인프라 설치를 위한 공을 들일 필요가 없는거니까요.

이제 본격적으로 여러 노드에서 gossip분산 서비스를 gossip-network 를 만들어서 실행시켜 보시면 잘 될리가 없습니다. 각 네트워크는 각 노드 내부에서만 활동하기 때문인데요. 외부 네트워크주소를 입력하는 부분도 없잖아요. 당연한거죠. 다른 방법이 필요 합니다. 

여기서 다룰 오버레이 네트워크는 여러 머신에서 각각의 네트워크에서 돌아가는 서비스들이 하나의 네트워크에 있는 것 처럼 만들어 주는 방식(docker swarm, hadoop yarn, apache mesos, kubernetes 등) 으로 다양하게 이루어 질 수 있는데, 여기서는 Docker 자체에서 제공하는 방식으로 만들어 볼 예정입니다.

* 사실 우리 프로그램 경우 그냥 실행시 name 옵션에서 정확한 외부 ip 를 입력해주면 잘 동작합니다. 굳이 이딴거(오버레이/스웜/쿠버네티스) 사용 할 필요도 없긴합니다. 실제 하이퍼레저 패브릭도 마찬가지입니다. 도커,쿠버네티스 같은거 사용 안해도 돌아가는 건 상관이 없어요. 각 호스트에서 디펜던시를 따로 다 설치하는 귀찮음이 있긴 하지만 오히려 첨에 실습해보는데는 더 클리어 하구요. 다만 초기 실습이 아니라 일이 되면 여러모로 추상화/자동화가 편하겠지요. 

도커 없이 ip만 입력해 주면 됨) 
 gossip -name  192.168.0.5:28001 -ip  192.168.0.5 -port 28001  -bootstrap   192.168.0.2:28000 

도커만 사용해도 ip만 입력해 주면 됨) 
 docker run -p 28001:28001 --net host --name  gossip1 --rm wowlsh93/gossip:1 /go/bin/gossip -name   192.168.0.5::28001 -ip   192.168.0.5  -port 28001  -bootstrap  192.168.0.2:28000

                                          (이미지 출처: https://blog.naver.com/alice_k106/220772125819)

위 그림처럼 각각의 서버에 위치하더라도 하나의 네트워크처럼 작동하도록 하기 위해 이 글에서는  etcd (분산 key-value store로 zookeeper 같은 분산코디네이팅 역할을 함) 라는 것을 사용 할 건데요. 5개의 서버를 활용하여 3개에는 도커를 설치하여 우리가 만든 이미지를 다운로드 해주시고, 2개에는 etcd를 설치합니다.  저 같은 경우는 VirtualBox에 Host-only-adapter 방식으로 5개의 호스트 네트워크를 구성하였습니다. bridge 로 해도 되고 서로 ping 날려서 확인만 되면 됩니다. 

etcd 설치 - etcd node 1

ubuntu@docker-node1:~$ wget https://github.com/coreos/etcd/releases/download/v3.0.12/etcd-v3.0.12-linux-amd64.tar.gz
ubuntu@docker-node1:~$ tar zxvf etcd-v3.0.12-linux-amd64.tar.gz
ubuntu@docker-node1:~$ cd etcd-v3.0.12-linux-amd64

ubuntu@docker-node1:~$ nohup ./etcd --name docker-node1 --initial-advertise-peer-urls http://192.168.205.10:2380 \
--listen-peer-urls http://192.168.205.10:2380 \
--listen-client-urls http://192.168.205.10:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://192.168.205.10:2379 \
--initial-cluster-token etcd-cluster \
--initial-cluster docker-node1=http://192.168.205.10:2380,docker-node2=http://192.168.205.11:2380 \
--initial-cluster-state new&


etcd 설치 - etcd node 2

ubuntu@docker-node2:~$ wget https://github.com/coreos/etcd/releases/download/v3.0.12/etcd-v3.0.12-linux-amd64.tar.gz
ubuntu@docker-node2:~$ tar zxvf etcd-v3.0.12-linux-amd64.tar.gz
ubuntu@docker-node2:~$ cd etcd-v3.0.12-linux-amd64/

ubuntu@docker-node2:~$ nohup ./etcd --name docker-node2 --initial-advertise-peer-urls http://192.168.205.11:2380 \
--listen-peer-urls http://192.168.205.11:2380 \
--listen-client-urls http://192.168.205.11:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://192.168.205.11:2379 \
--initial-cluster-token etcd-cluster \
--initial-cluster docker-node1=http://192.168.205.10:2380,docker-node2=http://192.168.205.11:2380 \
--initial-cluster-state new&

ubuntu@docker-node2:~/etcd-v3.0.12-linux-amd64$ ./etcdctl cluster-health
member 21eca106efe4caee is healthy: got healthy result from http://192.168.205.10:2379
member 8614974c83d1cc6d is healthy: got healthy result from http://192.168.205.11:2379
cluster is healthy


ondocker-node1
if docker version >= 17.09

ubuntu@docker-node1:~$ sudo service docker stop ubuntu@docker-node1:~$ sudo /usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock --cluster-store=etcd://192.168.205.10:2379 --cluster-advertise=192.168.205.12:2375

On docker-node2

ubuntu@docker-node2:~$ sudo service docker stop ubuntu@docker-node2:~$ sudo /usr/bin/docker daemon -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock --cluster-store=etcd://192.168.205.10:2379 --cluster-advertise=192.168.205.13:2375

On docker-node3

ubuntu@docker-node2:~$ sudo service docker stop ubuntu@docker-node2:~$ sudo /usr/bin/docker daemon -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock --cluster-store=etcd://192.168.205.10:2379 --cluster-advertise=192.168.205.14:2375


모든 설치가 끝나고, 아무 도커호스트에서 아래처럼 오버레이 네트워크를 만듭니다.

docker network create -d overlay gossip-overlay

모든 노드에서 동일한 네트워크가 만들어 졌음을 확인합니다. 이제 해당 네트워크를 통해서 2)번의 명령어와 동일하게 실행 할 수 있습니다. 물론 네트워크이름은 바꿔야죠.

docker run -it --network=gossip-overlay --name gossip1 gossip /go/bin/gossip -name gossip1:28000 -ip gossip1 -port 28000 -leader
docker run -it --network=gossip-overlay --name gossip2 gossip /go/bin/gossip -name gossip2:28001 -ip gossip2 -port 28001 -bootstrap gossip1:28000
docker run -it --network=gossip-overlay --name gossip3 gossip /go/bin/gossip -name gossip2:28002 -ip gossip3 -port 28002 -bootstrap


5) 다른 노드에 있는 컨테이너 실습 - Docker Swarm

..진행중..


참고:

도커 / 도커허브
https://subicura.com/2017/02/10/docker-guide-for-beginners-create-image-and-deploy.html

도커 네트워크
https://mesosphere.com/blog/networking-docker-containers/

도커 컴포즈 네트워크
https://medium.com/@caysever/docker-compose-network-b86e424fad82

도커 오버레이 네트워크 by etcd
https://docker-k8s-lab.readthedocs.io/en/latest/docker/docker-etcd.html

쿠버네티스 개론 및 실습 동영상
https://www.youtube.com/watch?v=l42GttmnnZ4

쿠버네티스 설치
https://medium.com/@dirty49374/kubeadm%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-kubernetes-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0-ubuntu-18-04-61710f0b4db8



       go로 간략히 구현한 블록전파/Gossip Protocol

지난 글(400라인의 go코드로 구현한 하이퍼레저 패브릭)에 이어 이번에는 하이퍼레저 패브릭의 블록전파/가쉽프로토콜을 추가 구현 해 보는 시간을 갖겠습니다. 이번 설연휴동안 여유있게 코딩했는데 역시나 세세한 부분이 많이 생략되습니다만 중요맥락에 대해서는 대략 비슷하게 동작되게 하는 목표로 구현하였습니다. (더 줄일 수도 있었지만, 앞으로 추가될 기능들을 위한 기반으로 600라인이 넘어갔습니다.) 

1개의 리더피어와 여러개의 일반 Commit 피어가 작동하게 될 것이며, 각 피어는 포트로 구분 될 것입니다. 리더피어가 오더러에게 메세지를 10초에 한번씩 가져와서 몇개의 피어에만 전파시키면 결국은 모든 피어가 모두 동일한 데이터를 담고 있게 할 것입니다. 시작되는 피어는 부트스트랩노드에서 초기 피어정보를 가져올 것이며, 30초에 한번씩 주변 노드로부터 노드정보들을 가져와서 업데이트 할 것입니다. 중간에 몇몇개의 피어가 멈추어도 동작 할 것이며,  그 피어 중 하나를 다시 살리면, (혹은 새로운 피어가 들어가도) 그 피어는 그 동안의 모든 데이터를 이웃으로 부터 받게 될 것입니다. 리더피어가 멈추면, 리더를 선출하게 됩니다. 

400 라인 하이퍼레저 패브릭 구성 요소 복습 

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

심플하게 구현된 하이퍼레저 패브릭은 위의 구성요소로 되어 있으며, 6번 오더러(O)에서 리드피어로 블록을 주면, 리드피어는 블록을 저장하고 끝났습니다. 근데 원래는 리드피어(C)는 자신의 동료 Commit피어들에게 가쉽프로토콜로 블록을 전파하거든요. 그 부분이 이번에 새로 추가한 코드입니다. 

해당 부분을 위해 간략하게 정리한 아이디어는 아래와 같습니다. (참고로 완전하고 효율적으로 가쉽프로토콜을 작성하는것은 매우 어려운 도전적인 과제같습니다. 참고로 저는 잘 모름) 

블록전파/가쉽프로토콜 아이디어 정리

1. 암호화 핸드쉐이크 (TLS)  - 생략
2. 프로토콜 핸드쉐이크  - 생략
3. 스트리밍 인코딩/디코딩 - 생략
4. UDP 및 NAT Traversal - 퍼블릭 블록체인 아니면 필요가 없음. 
5. 서로의 이름만 확인 후 간략한 패킷정의로 TCP 스트리밍으로 통신 (RPC사용안함)

6. 부트스트랩 피어/리더 피어/ 오더러 설정 - 하드코딩 /  채널,조직 등 추상개념 제외 
7. 각 피어는 시작시부트스트랩 피어에 접속하여, 부트스트랩 피어들이 가지고 있는 다른 피어정보들을 가져와서 저장.
8. 3초에 한번씩 자신이 살아있다는 정보를 모든 피어에 알림.- 생략
9. 자신이 가지고 있는 피어에게 alive메세지가 5초안에 안날라오면, V에서 제거하여 H로 이동.- 생략
10. Alive메세지를 받으면 그게 내 V에 없는 피어라면 V에 넣는다. 있는데 seq,시작시간가 둘다 크면 업데이트. - 생략
11. 리더에게 alive메세지가 안오면, 피어들은 부트스트랩에 랜덤으로 리더로 선출해서 서로에게 알림. 모든 피어는 서로의 투표를 받아서, 가장 많이 투표된 피어를 선출하고, 그 피어는 스스로 리더피어가되서 오더러에서 데이터를 가져옴. - 생략

12. 리더피어는 5초에 한번씩 블록을 가져옴. (오더로에서 가져왔다고 시뮬레이션함.실제 연결안함)
13  부트스트랩으로 부터 초기 연결정보를 가져오고, 후에는 30초에 한번씩 주변피어정보를 수집함. 
14. 수집된 피어 중 자신이 가지고 있지 않은 피어에 즉시 연결 요청함. 
15. 블록 받으면 이웃노드들중 랜덤 3개에 전파 (Push모델) 
16. 한번 받은 블록을 기록해두고, 똑같은 것을 받으면 버림. (같은 블록 처리를 방지) 
17. 램덤 Push 모델로는 구멍이 생기기 마련, Pull로 자신의 구멍을 메우는 호출도 함. 생략
17. 새로 발견된 이웃이 블록을 적게 가지고 있으면 블록 전파  - 생략
18. 새로 접속한 피어는 주변에 alive메세지와 함께 블록 정보도 뭍혀서 날림. 주변 피어들은 자신들의 블록 id를 리턴해주고 랜덤선택하여 블록 요청 (가장 큰 블록 id를 가진 피어한테 요청하면 그 피어가 병목이 될 수도) - 생략
19. 자신의 블록id와 2이상 차이가 날 경우에만 블록 요청 - 생략

구성도

이런식으로 리더피어가 동채널/동조직 commit피어집단에 모두 전파하는게 아니며 


  이런식으로 주변 랜덤n개에만 전파하면 각각 그들도 주변에 전파합니다. 

Node : 서비스의 메인모듈이며, 리모트피어로 부터의 접속을 받아드리고, 접속을 수행하는 역할을 합니다. 최종연결된 피어에 대해 단독개체를 만들어서 PeerManager에게 전달합니다.
PeerManager: 리모트 피어들의 생존을 관리하며, 브로드케스트 같은 전체 피어에게 행하는 행위를 합니다.
Peer : 리모트 피어 각각과 연결된 개체입니다.

소스코드

https://github.com/wowlsh93/hyperledger-fabric-400-gossip

실행
터미널 7개 정도 띄운후 
터미널1:  go run peer.go -name 127.0.0.1:28000 -ip 127.0.0.1 -port 28000 -leader
터미널2:  go run peer.go -name 127.0.0.1:28001 -ip 127.0.0.1 -port 28001 -bootstrap 127.0.0.1:28000
터미널3:  go run peer.go -name 127.0.0.1:28002 -ip 127.0.0.1 -port 28002 -bootstrap 127.0.0.1:28000
터미널4:  go run peer.go -name 127.0.0.1:28003 -ip 127.0.0.1 -port 28003 -bootstrap 127.0.0.1:28000
터미널5:  go run peer.go -name 127.0.0.1:28004 -ip 127.0.0.1 -port 28004 -bootstrap 127.0.0.1:28000
터미널6:  go run peer.go -name 127.0.0.1:28005 -ip 127.0.0.1 -port 28005 -bootstrap 127.0.0.1:28000
터미널7:  go run peer.go -name 127.0.0.1:28006 -ip 127.0.0.1 -port 28006 -bootstrap 127.0.0.1:28000


* 리드피어는 오더러에서 블록을 가져 온다.


* 이웃피어로부터 정보를 수집한다. 

* 블록을 주변에 전파한다.  (전파하는 와중에도 동일한 블록이 들어 올 수 있으나, 이미 받은 블록이면 버림) 


- 다음 시리즈 

400라인의 go코드로 구현한 하이퍼레저 패브릭 [3]- 도커/쿠버네이트로 디플로이



읽을꺼리
https://www.ibm.com/developerworks/community/wikis/form/anonymous/api/wiki/0be57f75-2769-40b2-9ea4-99fbec0f9073/page/5145e8c8-4833-4e5b-b623-d73d579c562e/attachment/e978b030-1d7a-4f76-8ad6-ef3dde66c409/media/Fabric%E4%BB%A3%E7%A0%81%E8%A7%A3%E6%9E%90%E7%AC%AC%E4%BA%8C%E8%AE%B2.pdf
https://hyperledger-fabric.readthedocs.io/en/release-1.4/gossip.html
https://jira.hyperledger.org/browse/FAB-170
http://www.inf.u-szeged.hu/~jelasity/ddm/gossip.pdf
http://disi.unitn.it/~montreso/ds/papers/montresor17.pdf
https://munin.uit.no/bitstream/handle/10037/13115/thesis.pdf?sequence=2&isAllowed=y


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은 데이터를 주고 받으면서 계속 업데이트 되어 보안이 더 강화된다.


+ Recent posts