퍼블릭 블록체인 (이더리움) 철학은 화폐의 이동에 대한 "신뢰 비용"을 줄이기 위해서라면
콘소시엄 블록체인 (하이퍼레저 패브릭) 철학은  조직간의  "신뢰 비용" 을 줄이기 위해서이다.(코인이 없으며 굳이 넣을 수도있겠지만 그게 의미 있는지에 대해 매우 회의적입니다. 퍼블릭체인과의 인터체이닝도 마찬가지 개념에 대한 변화가 심하며 앞으로도 계속 변화 할 것이기 때문에 쉽사리 규정짓기 힘들기도 합니다만..)

즉 하이퍼레저 패브릭을 공부하거나 먼가를 만드는 목적이 일반인들을 위해서 DApp을 만들려고 한다면 이상한거다. 또한 무슨 보안을 위해서라든지 위변조 방지를 위해서라든지 이런 구실을 만들어서 장부에 저장되는 어떤 기록에 대한 주체가 하나인 경우임에도 하이퍼레저패브릭로 해야 한다고 주장하지 말자. 보안,위변조방지,분산저장으로인한안전성등은 도구 혹은 부수효과 에 불과하다. 크나큰 낭비가 될 수 있다. 

하이퍼레저 패브릭을 적용하기 위해서는 먼저 해당 분야에 "조직" 이라는 네트워크가 존재해야하며, 그 "조직" 간에 어떠한 신뢰 비용이 생기는지 대략적으로 라도 파악 할 수 있어야 한다. (장부에 담겨질 내용은 이 다음 얘기) 그런 후에 하이퍼레저패브릭이라는 나름 거대한 인프라를 구축하고 관리하는데 드는 비용보다 그 비용(당장의 신뢰비용 + 미래의 기대비용) 이 대단히 크다고 생각 할 경우, 도입 할 여지가 생기게 된다. 

따라서 하이퍼레저패브릭을 하려는 업체나 개인은 그 조직들에 대한 도메인을 잘 알아야하며,(준비를 해야하며) 그 조직을 설득 할 수 있는 힘이 있어야 한다. 이 얘기는 반대로 조그마한 스타트업이나 개인이 공부하기에는 낭비가 될 수가 있다는 사실을 염두해 두어야 한다. (물론 내부 톱니바퀴를 이루는 p2p,보안,컨센서스,분산인프라 등에 대한 이론 공부는 피가되고 살이 될 수 있다. 문제는 트레이드 오프) 


        NAT-PNP 와 UPNP 를 이용한 홀펀칭 - (2) 


이번글에서는 이더리움에서 extip와 UPNP를 어떻게 사용하는지 배워보도록 하자. 

외부에서 192.168.10.11:80 서버와 통신하려면  그 사설 IP를 입력해바짜 아무 의미가 없다. 따라서 그 서버와 연결시켜주는 라우터를 통하게 되는데, 라우터의 공인IP인 193.24.171.247 로 보내되, 포트를 8028로 한다면 그것을 라우터는 192.168.10.11:80으로 포트포워딩해주는 것이다. 

즉 8028 포트는 내부의 192.168.10.12:80 IP/PORT와 매핑되는데 이것을 수동으로 미리 해 놓는다면 외부에서 접속하는데 아무 문제가 없을 것이다. 이렇게 수동으로 미리 매핑되어 있으면 extip를 그냥 사용하면 되고, 미리 정해져 있지 않아서 이더리움 프로그램에서 직접 매핑 설정을 해주려면 UPNP가 필요한 것이다. 

따라서 앞으로 살펴볼 코드를 예상해 볼 수 있는 것이, extip 는 특별히 하는것이 없을 것이며, 그냥 정해져있는 외부 IP를 사용만 할 것이며, (따라서 매핑 코드가 필요 없다) UPNP 는 위의 그림에서 라우터의 공인IP를 찾아서 외부IP로 설정하고, 특정 포트를 자신과 연결해주는 작업을 하는 코드가 있을 것이다. 

extip


// ExtIP assumes that the local machine is reachable on the given
// external IP address, and that any required ports were mapped manually.
// Mapping operations will not return an error but won't actually do anything.
func ExtIP(ip net.IP) Interface {
if ip == nil {
panic("IP must not be nil")
}
return extIP(ip)
}

type extIP net.IP

func (n extIP) ExternalIP() (net.IP, error) { return net.IP(n), nil }
func (n extIP) String() string { return fmt.Sprintf("ExtIP(%v)", net.IP(n)) }

// These do nothing.
func (extIP) AddMapping(string, int, int, string, time.Duration) error { return nil }
func (extIP) DeleteMapping(string, int, int) error { return nil }

역시나 AddMapping / DeleteMapping처럼 매핑관련 메소드는 아무것도 안하며, 공인(외부)IP를 리턴해주는 ExternalIP 메소드는 미리 라우터에서 설정을 하여 nat 옵션으로 넣어준 ip 값을 그대로 사용한다. 

다음으로는 어디에서 AddMapping와 ExternalIP가 사용하는지 살펴보자.

if srv.NAT != nil {
if !realaddr.IP.IsLoopback() {
go nat.Map(srv.NAT, srv.quit, "udp", realaddr.Port, realaddr.Port, "ethereum discovery")
}
if ext, err := srv.NAT.ExternalIP(); err == nil {
realaddr = &net.UDPAddr{IP: ext, Port: realaddr.Port}
}
}

위 코드는 Server.Start() 메소드 안에 있는데 NAT옵션이 있는 경우 

1. nat.Map 메소드를 이용해서 실제port (이더리움 기본 포트는 30303) 와 NAT 서비스의 port 를 동일한 숫자로 매핑시켜준다. 


func Map(m Interface, c chan struct{}, protocol string, extport, intport int, name string) {
log := log.New("proto", protocol, "extport", extport, "intport", intport, "interface", m)
refresh := time.NewTimer(mapUpdateInterval)
defer func() {
refresh.Stop()
log.Debug("Deleting port mapping")
m.DeleteMapping(protocol, extport, intport)
}()
if err := m.AddMapping(protocol, extport, intport, name, mapTimeout); err != nil {
log.Debug("Couldn't add port mapping", "err", err)
} else {
log.Info("Mapped network port")
}
for {
select {
case _, ok := <-c:
if !ok {
return
}
case <-refresh.C:
log.Trace("Refreshing port mapping")
if err := m.AddMapping(protocol, extport, intport, name, mapTimeout); err != nil {
log.Debug("Couldn't add port mapping", "err", err)
}
refresh.Reset(mapUpdateInterval)
}
}
}

UPNP나 NAT-PMP의 AddMapping 메소드를 호출하여 실제 nat 서버와 통신하여 설정해준다.extip는 위에서 언급한 것 처럼 아무것도 안하며 UPNP에서 어떻게 작동하는지에 대한 코드는 UPNP목차에서 살펴본다.

2. 외부ip를 가져와서 net.UDPAddr 를 만들어 줘서 realaddr 로 사용한다. 


// node table
if !srv.NoDiscovery {
cfg := discover.Config{
PrivateKey: srv.PrivateKey,
AnnounceAddr: realaddr,
NodeDBPath: srv.NodeDatabase,
NetRestrict: srv.NetRestrict,
Bootnodes: srv.BootstrapNodes,
Unhandled: unhandled,
}
ntab, err := discover.ListenUDP(conn, cfg)
if err != nil {
return err
}
srv.ntab = ntab
}

해당 addr 는 노드디스커버리를 위해 AnnounceAddr로 설정되는데 이것은 다른 이더리움 사용자 노드들이 자신의 노드디스커버리테이블에 넣을 주변노드정보에 사용 될 것이다. 


// ListenUDP returns a new table that listens for UDP packets on laddr.
func ListenUDP(c conn, cfg Config) (*Table, error) {
tab, _, err := newUDP(c, cfg)
if err != nil {
return nil, err
}
log.Info("UDP listener up", "self", tab.self)
return tab, nil
}

func newUDP(c conn, cfg Config) (*Table, *udp, error) {
udp := &udp{
conn: c,
priv: cfg.PrivateKey,
netrestrict: cfg.NetRestrict,
closing: make(chan struct{}),
gotreply: make(chan reply),
addpending: make(chan *pending),
}
realaddr := c.LocalAddr().(*net.UDPAddr)
if cfg.AnnounceAddr != nil {
realaddr = cfg.AnnounceAddr
}
// TODO: separate TCP port
udp.ourEndpoint = makeEndpoint(realaddr, uint16(realaddr.Port))
tab, err := newTable(udp, PubkeyID(&cfg.PrivateKey.PublicKey), realaddr, cfg.NodeDBPath, cfg.Bootnodes)
if err != nil {
return nil, nil, err
}
udp.Table = tab

go udp.loop()
go udp.readLoop(cfg.Unhandled)
return udp.Table, udp, nil
}

AnnounceAddr는 realaddr로 대입되며, Endpoing로 세팅되고, 


func newTable(t transport, ourID NodeID, ourAddr *net.UDPAddr, nodeDBPath string, bootnodes []*Node) (*Table, error) {
// If no node database was given, use an in-memory one
db, err := newNodeDB(nodeDBPath, Version, ourID)
if err != nil {
return nil, err
}
tab := &Table{
net: t,
db: db,
self: NewNode(ourID, ourAddr.IP, uint16(ourAddr.Port), uint16(ourAddr.Port)),
bonding: make(map[NodeID]*bondproc),
bondslots: make(chan struct{}, maxBondingPingPongs),
refreshReq: make(chan chan struct{}),
initDone: make(chan struct{}),
closeReq: make(chan struct{}),
closed: make(chan struct{}),
rand: mrand.New(mrand.NewSource(0)),
ips: netutil.DistinctNetSet{Subnet: tableSubnet, Limit: tableIPLimit},
}
if err := tab.setFallbackNodes(bootnodes); err != nil {
return nil, err
}
for i := 0; i < cap(tab.bondslots); i++ {
tab.bondslots <- struct{}{}
}
for i := range tab.buckets {
tab.buckets[i] = &bucket{
ips: netutil.DistinctNetSet{Subnet: bucketSubnet, Limit: bucketIPLimit},
}
}
tab.seedRand()
tab.loadSeedNodes(false)
// Start the background expiration goroutine after loading seeds so that the search for
// seed nodes also considers older nodes that would otherwise be removed by the
// expiration.
tab.db.ensureExpirer()
go tab.loop()
return tab, nil
}

  self:  NewNode(ourID, ourAddr.IP, uint16(ourAddr.Port), uint16(ourAddr.Port)) 로 노드디스커버리테이블에 세팅된다. Kademlia DHT알고리즘에 따라 해당 테이블에 대한 정보는 전세계에 얽혀있는 이더리움 네트워크간에 공유된다. 

UPNP

다시 언급하지만 UPNP의 핵심은 NAT서비스의 외부IP를 가져오고,포트를 로컬IP/PORT로 매핑하는 것이다.

먼저 nat 공통 인터페이스를 구현한 UPnP객체를 만들어 보자. 


// UPnP returns a port mapper that uses UPnP. It will attempt to
// discover the address of your router using UDP broadcasts.
func UPnP() Interface {
return startautodisc("UPnP", discoverUPnP)
}

type autodisc struct {
what string // type of interface being autodiscovered
once sync.Once
doit func() Interface

mu sync.Mutex
found Interface
}

func startautodisc(what string, doit func() Interface) Interface {
// TODO: monitor network configuration and rerun doit when it changes.
return &autodisc{what: what, doit: doit}
}

UPnP 생성 함수를 호출하면 내부적으로 autodisc 객체가 만들어 지는데  UPnP라는 문자열 이름과, 진짜 생성 함수가 매개변수로 들어가서 doit 변수에 대입된다. 이 doit (discoverUPnP) 을 통해 nat 인터페이스 구현체가 만들어져서 found에 최종대입되며, found를 통해서 외부IP를 가져온다든지, 포트매핑을 한다든지  할 것이다.

import (
.....
"github.com/huin/goupnp"
"github.com/huin/goupnp/dcps/internetgateway1"
"github.com/huin/goupnp/dcps/internetgateway2"
)

먼저 이더리움에서는 huin/goupnp 라이브러리를 사용하고 있다.

type upnp struct {
dev *goupnp.RootDevice
service string
client upnpClient
}

type upnpClient interface {
GetExternalIPAddress() (string, error)
AddPortMapping(string, uint16, string, uint16, string, bool, string, uint32) error
DeletePortMapping(string, uint16, string) error
GetNATRSIPStatus() (sip bool, nat bool, err error)
}

upnp 구조체에서 upnpClient 인터페이스를 보면 외부IP 주소를 가져오는 함수(GetExternalIPAddress) 와 포트를 매핑해주는 함수(AddPortMapping)가 있음을 알 수 있다.

upnpClient 를 통해서 값을 가져오고 매핑하는 코드를 보면 아래와 같다.

func (n *upnp) ExternalIP() (addr net.IP, err error) {
ipString, err := n.client.GetExternalIPAddress()
if err != nil {
return nil, err
}
ip := net.ParseIP(ipString)
if ip == nil {
return nil, errors.New("bad IP in response")
}
return ip, nil
}

func (n *upnp) AddMapping(protocol string, extport, intport int, desc string, lifetime time.Duration) error {
ip, err := n.internalAddress()
if err != nil {
return nil
}
protocol = strings.ToUpper(protocol)
lifetimeS := uint32(lifetime / time.Second)
n.DeleteMapping(protocol, extport, intport)
return n.client.AddPortMapping("", uint16(extport), protocol, uint16(intport), ip.String(), true, desc, lifetimeS)
}

이제 upnpClient를 세팅하고 upnp객체를 만드는 코드를 살펴보자.


// discoverUPnP searches for Internet Gateway Devices
// and returns the first one it can find on the local network.
func discoverUPnP() Interface {
found := make(chan *upnp, 2)
// IGDv1
go discover(found, internetgateway1.URN_WANConnectionDevice_1, func(dev *goupnp.RootDevice, sc goupnp.ServiceClient) *upnp {
switch sc.Service.ServiceType {
case internetgateway1.URN_WANIPConnection_1:
return &upnp{dev, "IGDv1-IP1", &internetgateway1.WANIPConnection1{ServiceClient: sc}}
case internetgateway1.URN_WANPPPConnection_1:
return &upnp{dev, "IGDv1-PPP1", &internetgateway1.WANPPPConnection1{ServiceClient: sc}}
}
return nil
})
// IGDv2
... 생략 ...

for i := 0; i < cap(found); i++ {
if c := <-found; c != nil {
return c
}
}
return nil
}

// finds devices matching the given target and calls matcher for all
// advertised services of each device. The first non-nil service found
// is sent into out. If no service matched, nil is sent.
func discover(out chan<- *upnp, target string, matcher func(*goupnp.RootDevice, goupnp.ServiceClient) *upnp) {
devs, err := goupnp.DiscoverDevices(target)
if err != nil {
out <- nil
return
}
found := false
for i := 0; i < len(devs) && !found; i++ {
if devs[i].Root == nil {
continue
}
devs[i].Root.Device.VisitServices(func(service *goupnp.Service) {
if found {
return
}
// check for a matching IGD service
sc := goupnp.ServiceClient{
SOAPClient: service.NewSOAPClient(),
RootDevice: devs[i].Root,
Location: devs[i].Location,
Service: service,
}
sc.SOAPClient.HTTPClient.Timeout = soapRequestTimeout
upnp := matcher(devs[i].Root, sc)
if upnp == nil {
return
}
// check whether port mapping is enabled
if _, nat, err := upnp.client.GetNATRSIPStatus(); err != nil || !nat {
return
}
out <- upnp
found = true
})
}
if !found {
out <- nil
}
}

꽤나 복잡해보이지만 목적은 단순하다. 아래의 구조체에 값을 채우는 것이다. 값을 채운후에 client를 통해 외부ip값도 가져올수 있고, 포트매핑도 할 수 있게 된다.

.type upnp struct {
   dev     *goupnp.RootDevice
   service string
   client  upnpClient
}

이렇게 만들어진 client upnpClient 를 통해서 외부ip값을 가져오고 매핑하는 코드를 보면 아래와 같다.

func (n *upnp) ExternalIP() (addr net.IP, err error) {
ipString, err := n.client.GetExternalIPAddress()
if err != nil {
return nil, err
}
ip := net.ParseIP(ipString)
if ip == nil {
return nil, errors.New("bad IP in response")
}
return ip, nil
}

func (n *upnp) AddMapping(protocol string, extport, intport int, desc string, lifetime time.Duration) error {
ip, err := n.internalAddress()
if err != nil {
return nil
}
protocol = strings.ToUpper(protocol)
lifetimeS := uint32(lifetime / time.Second)
n.DeleteMapping(protocol, extport, intport)
return n.client.AddPortMapping("", uint16(extport), protocol, uint16(intport), ip.String(), true, desc, lifetimeS)
}

 ipString, err := n.client.GetExternalIPAddress() 로 외부ip를 가져오며,
n.client.AddPortMapping("", uint16(extport), protocol, uint16(intport), ip.String(), true, desc, lifetimeS) 로 포트매핑을 해주고 있다.



1. [이더리움에서 배우는 Go언어]   chan chan 이란?
2. [이더리움에서 배우는 Go언어]   1급함수활용과 채널
3. 
[이더리움에서 배우는 Go언어]   nat 옵션 이야기 - (1)

nat 옵션 이야기 - (1)

이번 시리즈에서는 네트워크 인프라의 내부에서 사설IP로 동작하여 공인IP가 없는 노드끼리 어떻게 통신 할 수 있는지에 대해서 알아 본다. 첫번째 글에서는 간략한 정의를 내려보며  이더리움에서 사용되는 옵션들을 살펴볼 것이며 두번째 글에서는 extip와 upnp를 구체적으로 살펴보려 한다. (참고로 nat-pnp는 upnp와 비슷하므로 생략)


홀펀칭

홀펀칭(막힌 구멍을 뚫는) 필요한 이유는 피씨들이 모두 자신의 개인 고정 IP를 사용하는게 아니기 때문이다. 많은 경우 NAT라는 장비를 통해서 앞쪽에 대리자/게이트웨이를 두고, 내부 IP/PORT를 따로 가지고 있기 때문에, 클라이언트들 끼리 직접 연결은 불가능하며 외부와 통신하려면 내부아이디와 외부로 노출되는 IP/PORT간에 매핑등이 되어 뚤려 있어야 하는데 이렇게 뚤어 주는 것을 홀펀칭이라고 한다. 게임등에서 많이 사용되며 이더리움 또한 외부의 무작위 노드들과 서로 양방향 통신을 해야 하기 때문에 반드시 필요하다. 


NAT-PMP 와 UPNP

NAT-PMP 및 UPnP(Universal Plug And Play)는 특히 인터넷 애플리케이션이 홈 라우터 및 게이트웨이를 구성하여 수동 포트 전달 구성을 우회할 수 있도록 하는 기술이다. 동일한 개념이라고 보면 되며 NAT 포트 매핑의 자동화하는 서로 다른 구현 방식일 뿐이다.일반적으로 UPnP 프로토콜은 Windows/BSD/Linux 시스템에서 사용되고 NAT-PMP는 Apple 시스템에서 사용된다. 이러한 방법을 활용하려면 NAT-PMP 또는 UPnP 지원 하드웨어가 있어야 한다. 요즘은 많은 경우 지원한다. 

IpTime 에서 설정 모습

NAT 이란?

NAT동작방식은 위에 간단히 설명한 것처럼, 내부에서 외부로 패킷을 보낼 때, NAT장치는 위부IP와 매핑된 내부를 기록하고 있다가, 해당 외부IP를 통해서 패킷이 날라오면 그것을 해당 내부IP로 보내준다. NAT에 매핑기록이 없으면 보통 드롭된다. 이 얘기는 내부에서 보내야 매핑정보가 남는다는 것인데, 외부에서 내부의 프로그램으로 보낼 땐 어떻게 하냐? 그건 수동으로 매핑정보를 입력해야 한다. 즉 외부52번은 내부 129번으로 매핑한다라는 기록이 있으면, 129번으로 포워딩 해주면 된다. 이후 설명될 NAT-PMP, UPNP는 이 설정이자동으로 할당되며, (인바운드매핑)을 요청하여 기록되게 할 수 있다. 

NAT-PMP 란? 

NAT 포트 매핑 프로토콜 (NAT-PMP)은 사용자의 노력없이 NAT (Network Address Translation) 설정 및 포트 전달 구성을 자동으로 설정하기위한 네트워크 프로토콜입니다. 이 프로토콜은 NAT 게이트웨이의 외부 IPv4 주소를 자동으로 결정하고 응용 프로그램이 통신 매개 변수를 피어와 통신 할 수있는 수단을 제공합니다. NAT-PMP는 2005 년에 많은 NAT 라우터에서 구현 된보다 일반적인 ISO 표준 [2] 인터넷 게이트웨이 장치 프로토콜 대신 Apple에 의해 도입되었습니다. 이 프로토콜은 RFC 6886의 IETF (Internet Engineering Task Force)에 의해 RFC (정보 요청 요청)로 게시되었습니다. NAT-PMP는 UDP (User Datagram Protocol)를 통해 실행되며 포트 번호 5351을 사용합니다. 포트 전달은 일반적으로 STUN 방법을 사용하여 수행 할 수없는 활동을 허용하지 않으므로 내장 된 인증 메커니즘이 없습니다. STUN에 비해 NAT-PMP의 이점은 STUN 서버가 필요없고 NAT-PMP 매핑에 알려진 만료 시간이있어 응용 프로그램이 비효율적 인 연결 유지 패킷을 보내지 않도록 할 수 있다는 것입니다. NAT-PMP는 포트 제어 프로토콜 (PCP)의 전신입니다. [3]      - 위키발췌- 

UPNP란? 

유니버설 플러그 앤 플레이 (UPnP)는 UPnP 포럼이 공표한 컴퓨터 네트워크 프로토콜의 집합이다. UPnP의 목표는 장치들을 부드럽게 연결하고 가정 네트워크와 회사 환경의 기능(데이터 공유, 통신, 엔터테인먼트)을 단순화하는 것이다. UPnP는 개방된 인터넷 기반의 통신 표준 기반의 UPnP 장치 제어 프로토콜을 정의하고 출판함으로써 이를 달성한다. UPnP라는 용어는 컴퓨터에 직접 장치를 유동적으로 부착하는 기술인 플러그 앤 플레이에서 비롯한 것이다.  UPNP의 기본 프로토콜이 (SSDP)이 UDP 베이스이다.

이러한 NAT-PNP,UPNP에는 보안문제가 있는데, 이것은 여기서 다루는 범위 밖이라 생략한다.

이더리움에서의 홀펀칭 소스 위치 [소스]

이더리움에서는 노드디스커버리 즉 주변에 접속할 대상을 물색하는데 UDP를 사용하며 이때 홀펀칭이 사용되어 이후 프로토콜에 의한 데이터교환에서는 TCP를 사용한다. 이더리움 코어에서는 p2p 폴더 아래의 nat 폴더에 관련된 기능들을 제공하는 소스가 있다. 참고로 TCP,UDP 모두 디폴트 포트로 30303을 사용한다. 30301은 단지 bootnode에 의해서만 기본 UDP 디스커버리 포트로 사용된다.

이더리움에서 NAT 옵션 설정 

--nat value   NAT port mapping mechanism (any|none|upnp|pmp|extip:<IP>) (default: "any")

extip:77.12.33.4: 주어진 IP로 고정 매핑 되었다고 설정함. port는 수동매핑되있어야하고~
any   : 자동으로 먼저 탐지된 메커니즘을 사용. (디폴트)
upnp : 유니버셜 플러그앤 플레이 프로토콜 사용 
pmp  : 자동으로 감지된 게이트웨이 어드레스를 이용하여 
NAT-PMP 를 사용. 
pmp:192.168.0.1 : 주어진 게이트웨어 어드레스를 이용하여  NAT-PMP를 사용. 
none 또는 off : 설정된게 없다는 의미로. 스스로가 외부접근가능한 IP면 이렇게 하면 된다. 참고로 nat옵션자체를 설정안한것과 같다. 내부적으론 NAT변수에 둘다 nil을 대입함. 

NATFlag = cli.StringFlag{
Name: "nat",
Usage: "NAT port mapping mechanism (any|none|upnp|pmp|extip:<IP>)",
Value: "any",
}

// setNAT creates a port mapper from command line flags.
func setNAT(ctx *cli.Context, cfg *p2p.Config) {
if ctx.GlobalIsSet(NATFlag.Name) {
natif, err := nat.Parse(ctx.GlobalString(NATFlag.Name))
if err != nil {
Fatalf("Option %s: %v", NATFlag.Name, err)
}
cfg.NAT = natif
}
}

GlobalIsSet 함수를 통해 해당 옵션이 커맨드라인에서 세팅되었는지 확인하여 있으면 파싱

func Parse(spec string) (Interface, error) {
var (
parts = strings.SplitN(spec, ":", 2)
mech = strings.ToLower(parts[0])
ip net.IP
)
if len(parts) > 1 {
ip = net.ParseIP(parts[1])
if ip == nil {
return nil, errors.New("invalid IP address")
}
}
switch mech {
case "", "none", "off":
return nil, nil
case "any", "auto", "on":
return Any(), nil
case "extip", "ip":
if ip == nil {
return nil, errors.New("missing IP address")
}
return ExtIP(ip), nil
case "upnp":
return UPnP(), nil
case "pmp", "natpmp", "nat-pmp":
return PMP(ip), nil
default:
return nil, fmt.Errorf("unknown mechanism %q", parts[0])
}
}

커맨드라인에서 읽은 옵션을 그것과 매칭되는 nat.Interface 상속타입을 만들어 cfg.NAT 에 설정 한다.아래에 nat.Interface 를 보면 로컬포트와 인터넷에 접속되는 포트와의 매핑을 추가하는 메소드 (시간이 지나면 자동으로 해제된다) 와 외부 인터넷과 접점인 게이트웨이의 IP를 알 수 있는 메소드가 있다. Golang은 덕타이핑이 기본이므로 해당 메소드를 구현하기만 하면 자바나 C++상속코딩 절차가 필요없이 다형적으로 사용 할 수 있게 된다. 

type Interface interface {

AddMapping(protocol string, extport, intport int, name string, lifetime time.Duration) error
DeleteMapping(protocol string, extport, intport int) error

ExternalIP() (net.IP, error)
String() string
}


Any

func Any() Interface {
return startautodisc("UPnP or NAT-PMP", func() Interface {
found := make(chan Interface, 2)
go func() { found <- discoverUPnP() }()
go func() { found <- discoverPMP() }()
for i := 0; i < cap(found); i++ {
if c := <-found; c != nil {
return c
}
}
return nil
})
}

UPnP와 PMP 중 먼저 찾아지는것을 리턴한다. 2개의 버퍼를 가진 채널을 만들어서 for문을 capability (여기서는 2) 만큼 돌면서 먼저 보내지면서 nil 로 보내지지 않은 것을 리턴 해준다.

ExtIp


// ExtIP assumes that the local machine is reachable on the given
// external IP address, and that any required ports were mapped manually.
// Mapping operations will not return an error but won't actually do anything.
func ExtIP(ip net.IP) Interface {
if ip == nil {
panic("IP must not be nil")
}
return extIP(ip)
}

type extIP net.IP

func (n extIP) ExternalIP() (net.IP, error) { return net.IP(n), nil }
func (n extIP) String() string { return fmt.Sprintf("ExtIP(%v)", net.IP(n)) }

// These do nothing.
func (extIP) AddMapping(string, int, int, string, time.Duration) error { return nil }
func (extIP) DeleteMapping(string, int, int) error { return nil }

주어지는 외부IP를 가지고 extIP객체를 만들어 리턴. 외부IP로 고정되어있기 때문에 매핑과 관련된 함수가 의미없다.

UPnP

func UPnP() Interface {
return startautodisc("UPnP", discoverUPnP)
}

노드 디스커버리를 dscoverUPnP 를 가지고 시작. 

PMP


// PMP returns a port mapper that uses NAT-PMP. The provided gateway
// address should be the IP of your router. If the given gateway
// address is nil, PMP will attempt to auto-discover the router.
func PMP(gateway net.IP) Interface {
if gateway != nil {
return &pmp{gw: gateway, c: natpmp.NewClient(gateway)}
}
return startautodisc("NAT-PMP", discoverPMP)
}

노드 디스커버리를 dscoverPMP 를 가지고 시작. 

다음 글에서는 ExtIP 와 UPnP 에 대해서 정밀하게 관찰해보자.




1. [이더리움에서 배우는 Go언어]   chan chan 이란?
2. [이더리움에서 배우는 Go언어]   1급함수활용과 채널
3. 
[이더리움에서 배우는 Go언어]  nat 옵션 이야기 - (1)



 1급함수활용과 채널

지난 글에서는 채널에 채널을 보내는 chan chan 에 대해서 배워 보았는데요, 기억이 안난다면 이야기가 이렇습니다. 내가 (소비자) 먹을 준비됬으면 나의 주소와 함께 알려드릴께요. 당신은 (생산자) 만들면 내 주소로 피자를 보내주세요.
즉 생산자 주도적이 아니라, 소비자 주도적이다. 

이번 글에서도 채널과 관련된 이야기를 해 보겠습니다. 이번에는 채널에 함수를 보내는 방식입니다. 이야기는 이렇습니다.
내가 피자만드는 방식(함수)를 알려줄께요. 그 방식대로 피자를 만들어서 보내주세요. 주소는 피자만드는 방식(함수) 끝에 적혀져있답니다.

먼가 감이 잡히시나요? 안잡혀도 괜찮습니다. 이제부터 글보다 익숙한 코드로 배워 볼 것이니까요. 혹시나 채널과 고루틴 자체를 모르신다면 위에 링크 글이나 구글에서 검색해서 알아보시길 바랍니다. 약간의 이해는 필요합니다.

먼저 이더리움에서 어떻게 사용되었는지 코드를 보고 진도를 나가겠습니다.(쌍괄식?)

type peerOpFunc func(map[discover.NodeID]*Peer) .... srv.peerOp = make(chan peerOpFunc) .... // PeerCount returns the number of connected peers.
func (srv *Server) PeerCount() int {
var count int
select {
case srv.peerOp <- func(ps map[discover.NodeID]*Peer) { count = len(ps) }:
<-srv.peerOpDone
case <-srv.quit:
}
return count
}

간단히 소스를 설명하자면 먼저 NodeID와 Peer포인터를 쌍으로 갖는 맵을 매개변수로 하는 함수를 하나의 타입으로 선언했으며(peerOpFunc), 그것에 대한 채널(srv.peerOp)을 만들었습니다. 그리고 Peer의 갯수를 세는 함수에서 해당 채널에 직접 구현된 함수 func(ps map[discover.NodeID]*Peer) { count = len(ps) }:를 넘기고 있습니다. 

case op := <-srv.peerOp:
op(peers)
srv.peerOpDone <- struct{}{}

넘겨진 함수는 run이라는 함수내에서 받아서 위와 같이 처리(보내진 함수를 호출) 합니다. 이런 핑퐁관계는 파이썬,자바스크립트나 최근 코틀린에서도 지원하고 있는 코루틴과 비슷하며, 함수내에서 무한loop를 돌면서 다양한 이벤트를 기다렸다가 해당되는 이벤트에 맞게 처리되는 모습은 일견 액터패턴도 유추시킵니다. 액터패턴은 하나의 큐에서 넘어오는 일련의 명령(+데이터)들을 기다렸다가 처리한다 치면 위의 코드에서는 select를 통해서 펼쳐놓은 모습입니다. 어쨋거나 둘 다 직접 락을 거는 Mutex 식의 사용을 지양하고 쓰레드들의 동기 처리를 보다 분명하게 에러가 없게 처리하기 위한 방식인거죠. 

자 그럼 왜 이렇게 함수를 넘겨서 처리 하는 걸까요? 이제 부터 살펴보겠습니다. 


const (
OP_ADD = 1 << iota
OP_SUB
OP_MUL
)

type Calculator struct {
acc float64
}


func (c *Calculator) Do(op int, v float64) float64 {
switch op {
case OP_ADD:
c.acc += v
case OP_SUB:
c.acc -= v
case OP_MUL:
c.acc *= v
default:
panic("unhandled operation")
}
return c.acc
}

func main() {
var c Calculator
fmt.Println(c.Do(OP_ADD, 100)) // 100
fmt.Println(c.Do(OP_SUB, 50)) // 50
fmt.Println(c.Do(OP_MUL, 2)) // 100
}

위에 간단한 계산기의 코드가 있습니다.
메소드 Do에는 더하기,빼기,곱하기가 모두 정의 되어 있구요. 너무 많은 것을 하는것 처럼 보입니다.
추가적으로 제곱하기,평균내기 등이 들어간다면 더욱 더 하나의 함수가 복잡해 질 거 같습니다. 

개인적으로 코딩 할 때 간단히 2가지 법칙을 염두해 두는데요. ( SOLID,DRY법칙처럼 내가 만들었습니다 ㅎ)
1. 간단 책임의 원칙 :  함수(객체)는 최대한 심플하게 하자. 책임을 적게 지자.
2. 책임 전가의 원칙:  책임은 최대한 외부에 전가시키자. 주입받도록 하자.

근데 솔직히 자신이 모두 다 짜고,개발하며,한번 만든거 이상의 추가 업데이트가 있을 가능성이 적다? 하면 앞으로 나올 내용 처럼 생쇼 안부리고 직관적이게 코딩하는게 더 좋습니다. 마술을 부리거나 지나친 패턴사용으로 오버엔지니어링 되는게 더 문제겠지요.

Do는 무엇을 받아서 실행만 시키게 하면 안될 까요? 즉 책임을 적게 가지고 가자는 겁니다. 
Go에서의 방식은 아래와 같은데요. 


type Calculator struct {
acc float64
}

type opfunc func(float64, float64) float64

func (c *Calculator) Do(op opfunc, v float64) float64 {
c.acc = op(c.acc, v)
return c.acc
}

func Add(a, b float64) float64 { return a + b }
func Sub(a, b float64) float64 { return a - b }
func Mul(a, b float64) float64 { return a * b }

func main() {
var c Calculator
fmt.Println(c.Do(Add, 5)) // 5
fmt.Println(c.Do(Sub, 3)) // 2
fmt.Println(c.Do(Mul, 8)) // 16
}

이렇게 어떤 역할을 하는 함수 자체를 Do의 매개변수로 넣는 방식입니다.  즉 매개변수 2개를 받아서 float64타입을 리턴하는  함수타입이면 모두 ㅇㅋ 입니다.  이제 Do는 그냥 실행만 하면 땡이네요. 굉장히 간단해 졌으며, 추가적인 계산방식이 생기면 외부에서 만들어서 주입해주면 됩니다. 좀 더 유연해 졌다고 볼 수 있습니다. 


func Sqrt(n, _ float64) float64 {
return math.Sqrt(n)
}

func main() {
var c Calculator
c.Do(Add, 16)
c.Do(Sqrt, 0) // operand ignored
}

제곱(sqrt) 로직 추가시에는  쓸때없는 매개변수를 하나 더 넣어 줘야하는 상황이 됩니다. 이런 경우는 


func Add(n float64) func(float64) float64 {
return func(acc float64) float64 {
return acc + n
}
}

func Sqrt() func(float64) float64 {
return func(n float64) float64 {
return math.Sqrt(n)
}
}

func (c *Calculator) Do(op func(float64) float64) float64 {
c.acc = op(c.acc)
return c.acc
}
func main() {
var c Calculator
c.Do(Add(2))
c.Do(Sqrt()) // 1.41421356237
}

이런식으로 커링(부분함수)식으로 처리하면 됩니다. 공통 타입의 함수타입을 만들기 위해서 겉에 하나의 함수를 더 감싸는거죠. 겉 함수에서는 리턴 될 내부 함수에서 사용 될 매개변수를 미리 넘겨주게 됩니다. 
자 이제 1급함수의 역할을 2가지로 맛보기 했는데요 (매개변수로써의 함수, 리턴값으로써의 함수) 


이제 1급함수를 채널에서 사용해 보겠습니다.
먼저 채널을 어떤 경우에 사용하는지 잠시 살펴보겠습니다. 


type Mux struct {
mu sync.Mutex
conns map[net.Addr]net.Conn
}

func (m *Mux) Add(conn net.Conn) {
m.mu.Lock()
defer m.mu.Unlock()
m.conns[conn.RemoteAddr()] = conn
}

func (m *Mux) Remove(addr net.Addr) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.conns, addr)
}
}

conns 맵이 여러 쓰레드에서 한번에 사용되면, 접근 경쟁에 의한 버그가 발생할 위험이 생기겠지요?
따라서 소스에서 처럼 Mutex로 진입시 락을 걸어서 혼자 만 사용하겠다고 알려줘야 합니다.
이렇게 직접 락을 걸면서 코딩을 하면 동시성 프로그래밍에서 알기 힘든 버그를 만들수 있다고 구루들이 경고하잖아요.
그래서 다양한 동시성패턴들이 생겨났는데, Go언어에서는 자신(코루틴) 이 건드리고, 남(다른 코루틴)에게 패스하고 그 패스를 받은 사람이 또 건드리고, 패스하고 이런 전략을 사용합니다.직접 메모리를 공유하지말고 서로 패스를  통해서 공유하자는 겁니다. (무엇인가 액션을 던진다고 생각하면 액터패턴과도 비슷합니다. 간단히 말해 그걸 넣고 받는게 큐면 액터가 되는 것이고, 채널이면 고루틴이 되는 것이죠.)

아래 소스를 보겠습니다.


type Mux struct {
add chan net.Conn
remove chan net.Addr
sendMsg chan string
}

func (m *Mux) Add(conn net.Conn) {
m.add <- conn
}

func (m *Mux) Remove(addr net.Addr) {
m.remove <- addr
}

func (m *Mux) SendMsg(msg string) error {
m.sendMsg <- msg
return nil
}

func (m *Mux) loop() {
conns := make(map[net.Addr]net.Conn)
for {
select {
case conn := <-m.add:
m.conns[conn.RemoteAddr()] = conn
case addr := <-m.remove:
delete(m.conns, addr)
case msg := <-m.sendMsg:
for _, conn := range m.conns {
io.WriteString(conn, msg)
}
}
}
}

Add,Remove,SendMsg 각각의 채널을 만들고, 해당 함수에서는 해당 로직을 수행하길 바란다고 이벤트(+데이터) 를 패스하기만 합니다. 그러면 패스를 받은 loop메소드에서는 (이게 액터역할을 하지요) 어떤 후보 이벤트가 발생할 때까지 select 에서 대기하고 있다가 해당 이벤트가 발생하면 그것을 처리해 줍니다.

m.add <- conn

이것은 m.add채널에 conn이라는 이벤트(+데이터)를 패스하는 것이고

case conn := <-m.add:

이것은 m.add채널에서 conn이라는 이벤트(+데이터)를 받는 것이겠지요.

근데 처음에 Calculator 코드를 보면 Do안에서 모든것을 다 처리해서 Do는 그냥 로직을 외부에서 주입받는 방식으로 바꾼거 기억하시나요? 기억안나면 위로 올라가서 다시 확인하세요.
여기도 마찬가지로 loop함수에서 모든것을 다 하는게 싫습니다. loop함수도 그냥 주입받은 로직을 처리하고 싶어지는데요
이때 채널에 함수를 패스하게 됩니다. 아래 처럼 말이죠.

func (m *Mux) Add(conn net.Conn) {
m.ops <- func(m map[net.Addr]net.Conn) {
m[conn.RemoteAddr()] = conn
}
}

자 이제 왜 이더리움에서 저런식의 코드를 짯는지 이해가 되시나요? (올라가지 마시라고 아래 다시 적어봤습니다) 

// PeerCount returns the number of connected peers.
func (srv *Server) PeerCount() int {
var count int
select {
case srv.peerOp <- func(ps map[discover.NodeID]*Peer) { count = len(ps) }:
<-srv.peerOpDone
case <-srv.quit:
}
return count
}


func (srv *Server) run(dialstate dialer) {

    running:
for {
...
select {
    case <-srv.quit:
     break running
    case n := <-srv.addstatic:
    ...
     case n := <-srv.removestatic:
     ...
     case op := <-srv.peerOp:
     // This channel is used by Peers and PeerCount.
     op(peers)
    srv.peerOpDone <- struct{}{}
    ...
    }

}

위의 run 함수에서 모든것을 처리하기 싫어서 입니다. 로직은 외부에서 주입받고 싶은것이죠.
소스에 대한 설명을 하자면 PeerCount 함수에서 피어의 갯수를 세는 로직을 만들어서 다른 쓰레드에게  srv.peerOp <- func(ps map[discover.NodeID]*Peer) { count = len(ps) }: 이렇게 보내주고 기다리면, 다른 쓰레드에서는 op(peers)처럼 실행만 한뒤에 다 했다고 peerOpDone <- struct{}{} 이렇게 익명구조체 생성하여 채널로 보내서 알려줍니다. 그러면 PeerCount에서는 <-srv.peerOpDone 이렇게 기다리다가 종료하고 있습니다 . 

근데 안타깝게도 실제 이더리움소스에서 모든것을 주입받는 것은 아닙니다. run안에서는 그냥 이벤트만 호출받고 그에 해당하는 로직을 처리하거나 위임 처리 한 후에 또 다른 채널에 패스를 하기도 합니다. 실전은 항상 단순하진 않지요.

마지막으로 해당 run전체 함수를 보여드리면서 글을 끝마치겠습니다.


func (srv *Server) run(dialstate dialer) {
defer srv.loopWG.Done()
var (
peers = make(map[discover.NodeID]*Peer)
inboundCount = 0
trusted = make(map[discover.NodeID]bool, len(srv.TrustedNodes))
taskdone = make(chan task, maxActiveDialTasks)
runningTasks []task
queuedTasks []task // tasks that can't run yet
)
// Put trusted nodes into a map to speed up checks.
// Trusted peers are loaded on startup and cannot be
// modified while the server is running.
for _, n := range srv.TrustedNodes {
trusted[n.ID] = true
}

// removes t from runningTasks
delTask := func(t task) {
for i := range runningTasks {
if runningTasks[i] == t {
runningTasks = append(runningTasks[:i], runningTasks[i+1:]...)
break
}
}
}
// starts until max number of active tasks is satisfied
startTasks := func(ts []task) (rest []task) {
i := 0
for ; len(runningTasks) < maxActiveDialTasks && i < len(ts); i++ {
t := ts[i]
srv.log.Trace("New dial task", "task", t)
go func() { t.Do(srv); taskdone <- t }()
runningTasks = append(runningTasks, t)
}
return ts[i:]
}
scheduleTasks := func() {
// Start from queue first.
queuedTasks = append(queuedTasks[:0], startTasks(queuedTasks)...)
// Query dialer for new tasks and start as many as possible now.
if len(runningTasks) < maxActiveDialTasks {
nt := dialstate.newTasks(len(runningTasks)+len(queuedTasks), peers, time.Now())
queuedTasks = append(queuedTasks, startTasks(nt)...)
}
}

running:
for {
scheduleTasks()

select {
case <-srv.quit:
// The server was stopped. Run the cleanup logic.
break running
case n := <-srv.addstatic:
// This channel is used by AddPeer to add to the
// ephemeral static peer list. Add it to the dialer,
// it will keep the node connected.
srv.log.Trace("Adding static node", "node", n)
dialstate.addStatic(n)
case n := <-srv.removestatic:
// This channel is used by RemovePeer to send a
// disconnect request to a peer and begin the
// stop keeping the node connected
srv.log.Trace("Removing static node", "node", n)
dialstate.removeStatic(n)
if p, ok := peers[n.ID]; ok {
p.Disconnect(DiscRequested)
}
case op := <-srv.peerOp:
// This channel is used by Peers and PeerCount.
op(peers)
srv.peerOpDone <- struct{}{}
case t := <-taskdone:
// A task got done. Tell dialstate about it so it
// can update its state and remove it from the active
// tasks list.
srv.log.Trace("Dial task done", "task", t)
dialstate.taskDone(t, time.Now())
delTask(t)
case c := <-srv.posthandshake:
// A connection has passed the encryption handshake so
// the remote identity is known (but hasn't been verified yet).
if trusted[c.id] {
// Ensure that the trusted flag is set before checking against MaxPeers.
c.flags |= trustedConn
}
// TODO: track in-progress inbound node IDs (pre-Peer) to avoid dialing them.
select {
case c.cont <- srv.encHandshakeChecks(peers, inboundCount, c):
case <-srv.quit:
break running
}
case c := <-srv.addpeer:
// At this point the connection is past the protocol handshake.
// Its capabilities are known and the remote identity is verified.
err := srv.protoHandshakeChecks(peers, inboundCount, c)
if err == nil {
// The handshakes are done and it passed all checks.
p := newPeer(c, srv.Protocols)
// If message events are enabled, pass the peerFeed
// to the peer
if srv.EnableMsgEvents {
p.events = &srv.peerFeed
}
name := truncateName(c.name)
srv.log.Debug("Adding p2p peer", "name", name, "addr", c.fd.RemoteAddr(), "peers", len(peers)+1)
go srv.runPeer(p)
peers[c.id] = p
if p.Inbound() {
inboundCount++
}
}
// The dialer logic relies on the assumption that
// dial tasks complete after the peer has been added or
// discarded. Unblock the task last.
select {
case c.cont <- err:
case <-srv.quit:
break running
}
case pd := <-srv.delpeer:
// A peer disconnected.
d := common.PrettyDuration(mclock.Now() - pd.created)
pd.log.Debug("Removing p2p peer", "duration", d, "peers", len(peers)-1, "req", pd.requested, "err", pd.err)
delete(peers, pd.ID())
if pd.Inbound() {
inboundCount--
}
}
}

srv.log.Trace("P2P networking is spinning down")

// Terminate discovery. If there is a running lookup it will terminate soon.
if srv.ntab != nil {
srv.ntab.Close()
}
if srv.DiscV5 != nil {
srv.DiscV5.Close()
}
// Disconnect all peers.
for _, p := range peers {
p.Disconnect(DiscQuitting)
}
// Wait for peers to shut down. Pending connections and tasks are
// not handled here and will terminate soon-ish because srv.quit
// is closed.
for len(peers) > 0 {
p := <-srv.delpeer
p.log.Trace("<-delpeer (spindown)", "remainingTasks", len(runningTasks))
delete(peers, p.ID())
}
}

레퍼런스:
https://dave.cheney.net/2016/11/13/do-not-fear-first-class-functions
https://github.com/ethereum/go-ethereum/blob/master/p2p/server.go

IBM 클라우드(하이퍼레저)에서 와이프 카드로 날라온 50만원 상당의 결재 금액에 멘붕중이네요. 예전에 계정관련 카드문제가 있어서 굉장히 큰 곤란을 겪었는데... 내카드,와이프카드,회사1카드모두 안되서 결국회사2카드를 통해서 서류를 미국으로 직접 보내서 겨우 서비스 이용중이고 요금도 납부중인데.. 난데없이 아내 카드로 잠시 Start plan 을 시동만 걸어놨다가 락걸려서 (이 부분에 대한 기억이 가물가물..) 잊어버린 서비스가 살았는지 청구를 해 왔습니다. 계정/결제 관련 문제는 한국에서 절대 해결 불가능하다고 하여 미국과 직접 소통하라고 메일하나 던져준 한국IBM 사용자지원센터. 여기까진 ㅇㅋ 50십만원 상당의 금액은 제 실수 일지도 모르기 때문에 납부하려고 마음먹고 (AWS는 이런것도 잘 해결해 주는것으로 알고 있습니다.) 빨리 기억에서 사라진 이 서비스를 중지시켜야 해서 까먹은 해당 IBM Cloud ID가 무엇인지 알려달라는 메일에 3일동안 대답이 없는 IBM 클라우드 .... IBM ID 와 연계된 이메일 주소로는 카드청구를 하고서, 해당 IBM ID 는 알려주지도 않는...망할..이런 사용자경험으론 앞으로 한국에서 IBM클라우드를 사용하긴 힘들거 같습니다.

비용을 청구한 이메일/카드와 연계된 IBM ID가 없다고 메일이 왔네요. 해당 서비스를 사용하는 ID도 없는데 서비스에 대한 비용청구는 한다?? 무언가 과금을 일으키는 서비스를 삭제 할 IBM ID가 없다? 그래도 50십만원씩 계속 청구 할거다? 이런 날강도가 다 있나요 ㅎㅎ




초반에 IBM Cloud 계정가입에 문제가 생겨서 차질이 있었지만, 한달동안 하이퍼레저 프로젝트를 잘(?) 완료 하였다. 해당 프로젝트는 웹서비스단(React,Express,Mongo)와 블록체인단(Composer Rest Server, IBM Blockchain) 으로 이루어졌는데, 중간에서 Mongodb가 캐싱역할을 하며, 웹서비스단에서 블록체인에 대한 호출 즉 Composr API 호출을 위임하고 있는 형태. 따라서 Composer Rest 서버를 멀티유저(혹은 싱글서버를 카드별로 여러개) 가 아닌 단일유저가 사용하므로 신뢰의 분산에는 적합치 않은 모습..차후에 이걸 어떻게 분산시킬지.. 분산을 꼭 시켜야할지에 대한 고민과 동시에 하이퍼레저를 다루는 방식이 굉장히 다양할수 있으며, 버전에 따라서 예전 문서가 잘 적용이 안되는 경우도 많아서 나름 고전했던거 같다. 마지막으로 IBM의 구성원들이 얼마나 노력을 많이 하고 있는지에 대해 정말 많이 느꼈다. 그들이 만들어 나가는 기술블로그들은 정말 대단~이번에 레드햇도 인수했던데, 상위권으로 도약하길 바란다. 

p,s

프로젝트하면서 느낀게 중간에 미들웨어에서 다른 트랜잭션이 Composer 트랜잭션과 동시에 일어나는데, 이런 경우 어떻게 완결성을 맺을까 하는 점이었다. 돈이 오고가는 프로젝트가 아니라서 대충 얼버무렸지만.. 장부에는 letter of credit 이 완결되어 Close 시켰다고 트랜잭션이 일어났는데, 은행 트랜잭션에서 송금이 실패했다면?? 외부 트랜잭션과 All or Nothing 전략은 어떤식으로 할 수 있을까?? 생각해볼게 많은거 같다. hyperledger fabric에 rollback 기능이 없고.... 실제 사례에 대한 아티클을 찾아보아야겠다. (요즘 유행하는 마이크로 서비스 패턴들을 잘 버무려야함. SAGA패턴 같은) 

아래는 그러한 트랜잭션에서 고려할만한 전략을 정리한 글이다.

    1) 취소 전략: 비즈니스 트랜잭션의 오류 해결책으로 가장 간단한 전략이다. 즉 지금까지 진행된 개별 트랜잭션들을 모두 취소시킨다. 어떻게 보면 이 전략은 2단계 커밋의 롤백 전략과 유사하다. 그리고 별로 좋은 전략 같아 보이지도 않는다. 그러나 현실 세계에서는 그런대로 쓸만하다. 취소함으로 발생하는 손실이 거래를 복구함으로 얻는 이익보다 큰 경우 특히 유효하다. 애플리케이션 개발 시 실행(삽입, 갱신) 기능과 취소(삭제, 갱신)기능을 함께 고려하고, 트랜잭션 조정자는 오류를 감지한 후 각 참여 자원의 취소 기능을 요청한다. 2단계 커밋의 롤백과 다른 점은 하나 이상의 요청이 트랜잭션 중간에 개입될 수 있고 긴 시간의 트랜잭션에서 발생한 오류를 해결하는 전략으로, 이런 문제는 2단계 커밋의 롤백으로는 해결되지 않는다.

    2) 재시도 전략: 재시도를 통해 비즈니스 트랜잭션이 성공할 수 있는 업무인 경우 다시 동일한 비즈니스 기능을 시작한다. 예를 들어 외부 자원이 일시적으로 중지된 경우 재시도를 통해 극복될 수 있다. 업무 규칙을 위반한 비즈니스 기능의 요청인 경우 이 전략을 사용하면 안된다. 참여 시스템이 멱등 수신자(Idempotent Receiver)[EIP]가 된다면, 이 전략은 좀더 효과를 발휘한다. 멱등 수신자란 동일한 요구에 대해 수신자의 상태가 변하지 않는 수신자를 말한다. 즉 재시도 전략을 비즈니스 트랜잭션 오류의 전략으로 선택한 경우 각 참여 자원을 멱등 수신자로 만들어야 한다. 멱등 수신자 패턴에 대한 자세한 설명은 [EIP]나 [SDP]를 참조한다. 재시도에는 재시도 횟수를 지정할 수 있다. 재시도 횟수를 넘어선 오류인 경우 다른 해결 전략으로 오류 해결 방법을 변경해야 한다.

    3) 보상 전략: 이미 처리된 비즈니스 오류를 별도의 비즈니스 기능으로 보상하는 전략이다. 예를 들어 은행이 고객의 이체 처리 중 출금은 성공했으나 입금에 실패한 경우 출금 액만큼 은행이 고객에게 입금해주는 전략이다. 이를 위해서는 트랜잭션 오류 상태를 해결 상태로 전이할 수 있는 기능(메소드)을 참여 애플리케이션에 포함해야 한다. 비즈니스 트랜잭션 오류가 발생하면 비즈니스 트랜잭션 조정자는 보상 비즈니스 기능을 참여 시스템에 요청해 비즈니스 트랜잭션의 오류 상태를 정상 상태로 전이시킨다.

인용 및 참고 링크

1. 분산 비즈니스 트랜잭션과 오류 해결 방법
2. 스타벅스는 2단계 커밋을 사용하지 않는다.
3. 2단계 커밋과 3단계 커밋이란?
4. 3단계 커밋이란?




사족
하이퍼레저 컴포저는 분명히 하이퍼레저 패브릭 네트워크/어플리케이션을 만들기 쉽게 해주지만, 패브릭을 추상화 하는데 있어서 어려움도 많습니다. (패브릭에서 추가된 기술을 따라 잡지 못한다던가, 모습이 전혀 달라서 처음 진입하는 사람들에게 큰 혼동을 초래. 예를들어 컴포저의 Participant 가 fabirc의 무엇과 매칭되는가? 컴포저의 Card 개념은 fabric의 무엇인가? 컴포저로 멀티유져/조직을 다루는 방법등~) 현재 몇몇 문제 때문에 IBM에서 컴포저에 대한 지원을 줄였지만, 업데이트는 계속 될 것이라고 하네요. 


개인적으로는 컴포저의 역할이 매우 중요하리라 생각합니다. 이쪽이 훨씬 더 키워드들이 실세계와 매칭되며 간단하니까요~사람들이 사용하지 않는 기술,어려운 기술은 사장되기 마련입니다. 

(이 글을 쓴 2018년인데, 2020년이 시작되는 현재 컴포저는 하이퍼레저군에서 제외되었네요. 모든 경우에 대한 추상층을 쌓아 올리는데 실패했으며, 굉장히 어려워 보이긴 했습니다. 사용자가 패브릭을 이해하고 사용하기도 그 레이어 때문에 더 어려워서 쓸모가 없어졌습니다.)


관련
Developing multi-user application using the Hyperledger Composer REST Server (18/2월)
IBM Blockchain Starter Plan 에 비지니스 네트워크 디플로이 (18/4월)

Composer REST Server 를 Cloud Foundry application로 디플로이| (18/5월)
Passport-JWT Authentication for Hyperledger Composer Rest Server (18/5월)
Deploy a blockchain business network to the cloud using the IBM Blockchain Starter Plan (18/6월)

위 글이 쓰여질 때는 IBM Blockchain network 가 fabric 1.1 기준이었지만 현재 (2018년 10월6일) 는 1.2.1 로 업데이트 되었다. 따라서 문서에는 composer 를 0.19버전을 설치하라고 나왔지만  0.20 버전으로 올려줘야 한다.

fabric 1.1 => composer 0.19.x
fabric 1.2 => composer 0.20.x

Create a fair trade supply network with Hyperledger Composer and IBM Blockchain Starter Plan (18/9월)
Deploy a sample application to the IBM Blockchain Platform Starter Plan  (18/10월)


기타

- Setup Hyperledger Fabric in multiple physical machines (18년 9월)
- Publish a Business Network in Multi-host HyperLedger Fabric (18년9월)

Hyperledger composer 일반강좌

한글 : https://www.youtube.com/watch?v=smytS8dQCtk  (후반부 10여분)
영어 : https://www.youtube.com/watch?v=nS_MRqAeEbQ


+ Recent posts