Exclamate EOS! : ABI

Jeeyong Um
GameXCoin
Published in
12 min readMar 14, 2019

--

EOS 스마트 컨트랙트를 컴파일 하면 두 개의 파일이 생성된다. 하나는 WebAssembly용 바이너리인 WASM 파일이고 다른 하나는 ABI 파일이다. ABI는 Application Binary Interface 의 약자로, 컨트랙트 실행시 바이너리 형태로 교환하는 데이터의 직렬화/역직렬화(serialization/deserialzation) 규칙을 기술한다. 네트워크 상에서 교환하는 데이터는 가변 길이의 연속된 바이트이므로 ABI가 없거나 잘못되면 바이너리를 제대로 해석할 수 없게 되며 트랜잭션 실행에 실패하거나 잘못된 결과를 갖게 된다.

ABI 파일은 보통 컨트랙트 소스 파일에 포함된 힌트를 이용해 자동으로 생성한다. EOSIO.CDT 1.3 버전 이전에는 주석 형태의 힌트 /// @abi ... 를 사용했지만 1.3 버전 이후부터는 C++11 이후 표준에서 지원하는 속성(attributes) 기능을 이용하여 ABI 생성기(eosio-abigen)가 필요로 하는 정보를 표시하고 있다. EOSIO.CDT 1.5 버전 기준 제공되는 속성은 다음 4가지이다.

[[eosio::contract]]
[[eosio::action]]
[[eosio::table]]
[[eosio::ignore]]

참고

아직 정식 버전은 나오지 않았지만 EOSIO.CDT v1.6.0 Release Candidate의 변경 사항을 보면 [[eosio::on_notify]], [[eosio::wasm_entry]] [[eosio::wasm_import]] 등 3가지 속성이 추가될 것임을 예고하고 있다. 다만 이 속성들은 ABI 생성에 필요한 힌트가 아니라 wasm 실행 및 notification 핸들러 (require_recipient 통해 수신한 액션) 정의에 사용하는 힌트이다. 컨트랙트 컴파일러 코드를 분석하면 몇 가지 속성이 추가로 정의되어 있지만 CDT 상에서 이를 어떻게 활용할지는 공개되어 있지 않다.

ABI 파일에 대한 기본적인 설명은 개발자 포털 또는 KerbEOS팀의 개발자 포털 한글판에서 확인할 수 있으므로 이 글에서는 문서화가 잘 되어 있지 않은 몇 가지 사항에 대해서만 간단히 소개하기로 한다.

ABI version

최신 버전 CDT를 사용하여 ABI를 생성하면 상단에 "version": "eosio::abi/1.1" 와 같이 버전 정보가 포함되어 있는 것을 볼 수 있다. 이전 버전(1.4.0 이전)의 CDT로 컴파일하여 생성한 ABI 파일은 1.0 이었는데 eosio::binary_extensionstd::variant 타입 지원이 추가되면서 eosio::abi/1.0 버전과 호환성이 없어져 1.1 로 업데이트 되었다.

eosio::abi/1.1 을 지원하는 것은 EOSIO 기준 1.3.0, EOSIO.CDT 기준 1.4.0 이후이므로 ABI 배포에 문제가 있는 경우 /v1/chain/get_info 를 통해 노드(nodeos) 버전을 확인해야 한다.

built_in types

빌트인 타입은 별도의 타입 정의 없이 사용할 수 있는 내장 타입을 의미한다. 사용자 정의 타입은 빌트인 타입의 조합으로만 나타낼 수 있다. 예를 들어 새로운 회원을 등록하기 위한 newmember 액션을 다음과 같이 정의했다고 하자.

위 소스 파일로부터 생성한 ABI에는 다음 내용이 포함된다. (comment, version 정보 등은 생략)

newmember 액션의 전달인자인 n은 사용자 정의 타입인 member 지만 member 타입은 다시 빌트인 타입인 64비트 정수(uint64) 와 문자열(string)로 이루어져 있음을 알 수 있다. 사용자 정의 타입을 중첩하여 사용할 수 있지만 끝까지 따라가면 빌트인 타입으로 환원된다.

지원하는 빌트인 타입의 목록은 아래 코드에서 확인할 수 있다.

optional type

optional 타입은 ABI 1.0 부터 지원한다. 컨트랙트 코드 상에서 대응되는 타입은 std::optional 로 액션의 전달인자를 std::optional 로 지정하는 경우 ABI 에는 "타입명 + ?" 형태로 표현된다.

optional 타입은 설정된 타입의 값 또는 null 을 전달할 수 있고, 값이 전달되었는지 여부는 has_value() 메소드를 사용하여 확인할 수 있다. (bool 타입으로의 묵시적 타입 캐스팅도 지원하므로 바로 변수명을 넣어 검사해도 된다)

$ cleos push action test printopt '[1234]' -p test@active
executed transaction: 08f7f22f3050a06311e05ae13efc8324f64e58c55cafe1cc91c62eba604aaaa4 104 bytes 100 us
# test <= test::printopt {"n":1234}
>> 1234
$ cleos push action test printopt '[null]' -p test@active
executed transaction: 5fd088d1da4b4ff231e9ba3a8e20652b16cc4fd5ee8f8a908cc2b915116f544a 96 bytes 100 us
# test <= test::printopt {"n":null}
>> parameter doesn't have value

binary_extension type

binary_extension 타입은 ABI 1.1 부터 지원한다. 컨트랙트 코드 상에서 대응되는 타입은 eosio::binary_extension 으로 액션의 전달인자를 eosio::binary_extension 으로 지정하는 경우 ABI 에서 "타입명 + $" 형태로 표현된다.

EOSIO는 역직렬화 과정 중 제공된 바이너리와 ABI 파일에서 정의한 타입이 일치하는지 확인하기 위해 데이터 사이즈를 검사하는 단계가 포함되어 있다. 예를 들어 다중 서명 (multi-sig) 기능을 제공하는 eosio.msig 컨트랙트의 approve 액션을 살펴보자. 이 액션은 원래 다음과 같은 함수 시그니처를 갖고 있었다.

따라서 approve 액션의 전달인자로 사용할 바이너리는 name(8) + name(8) + permission_level(16) = 32 bytes 로 정확히 32 바이트 크기가 아니면 바이너리를 읽을 수 없다는 에러를 내면서 트랜잭션 실행에 실패하게 된다.

그런데 EOSIO Contracts v1.5.0 에서 approve의 함수 시그니처가 다음과 같이 변경되었다.

만약 binary_extension 타입이 없었다면 전달인자의 바이너리에 eosio::checksum256 타입의 값이 반드시 포함되어야 하므로 64 바이트 크기(eosio::checksum256은 256비트이므로 32바이트)를 가져야 하며, 만약 이전 버전에서 보내던 32 바이트 크기의 바이너리를 보내면 트랜잭션은 실패한다.

그러나 binary_extension 타입으로 정의된 인자를 마지막에 추가하는 경우, 바이너리 크기 검사를 위한 사이즈 계산에는 이 값을 포함하지 않지만 데이터가 존재할 때는 정상적으로 역직렬화 하므로 이전 버전의 바이너리와 호환성을 유지하면서 새로운 기능을 제공할 수 있게 된다.

optional 타입과 비슷하지만 다른 점은 값을 보내고 싶지 않을 때, null 을 보내는 것이 아니라 아예 값을 포함하지 않는다는 것이다.

// proposal_hash를 생략하는 경우
$ cleos push action eosio.msig approve '["alice", "paymenow", {"actor": "alice", "permission": "active"}]' -p alice@active
// proposal_hash를 포함하는 경우
$ cleos push action eosio.msig approve '["alice", "paymenow", {"actor": "alice", "permission": "active"}, "1AABD1EB819D775ED6CC642B39381938D4DA13048FC9A2F942A53798FD2867F5"]' -p alice@active

참고

역직렬화 과정 중 사이즈 검사는 eosio::multi_index 도 동일하게 수행한다. 멀티 인덱스 테이블에 새로운 필드를 추가하면 기존에 저장했던 행(row)을 읽는 과정에서 에러가 나는데, 이는 ABI에서 정의한 타입의 크기와 멀티 인덱스 테이블이 불러온 바이너리의 크기가 다르기 때문이다. 따라서 개발자 포털에서는 테이블 구조를 변경하려면 기존에 추가했던 행을 모두 삭제해야 한다고 설명한다. eosio.system 컨트랙트도 전역 환경 변수를 위한 eosio_global_state 싱글턴(행이 1개인 multi_index의 특수 타입, eosio::singleton)에 변수를 추가하지 않고 eosio_global_state2, eosio_global_state3를 계속 추가한 것을 볼 수 있다.

그러나 binary_extension 타입을 이용하면 기존 테이블의 행을 삭제하지 않고도 새로운 필드를 추가할 수 있다. (사실 당연히 가능해야 하는 것은 multi_index 테이블이 내부적으로 사용하는 저수준 API인 DB API는 데이터 타입을 검사하는 과정 없이 메모리 주소와 바이너리 크기만 전달하여 저장하므로 해당 과정을 생략하면 변경 불가능할 이유가 없다) 테이블의 스키마가 고정되는 관계형 데이터베이스와 달리 EOSIO의 멀티 인덱스 테이블은 각 행에 key-value object 를 저장하므로 그 크기가 저마다 달라질 수 있다.

물론 이러한 테크닉은 멀티 인덱스 테이블의 동작 원리를 정확히 이해하고 있는 고급 사용자들만 사용하길 권한다. EOSIO 개발진은 일반 스마트 컨트랙트 개발자가 세부적인 동작 원리에 대해서는 신경쓰지 않고 컨트랙트의 비즈니스 로직에만 집중할 수 있도록 CDT(Contract Development Toolkit)를 개발하고 있다. 그러므로 공식 문서에서 소개하지 않은 방식으로 개발하는 경우 CDT가 버전업 되면서 언제든지 동작이 변경될 수 있고 이에 대한 책임은 전적으로 개발자 본인이 지게 된다. 예를 들어 EOSIO.CDT 1.6 버전 부터는 저수준 C API는 eosio-cpp 에서는 사용할 수 없고 eosio-cc 를 직접 사용하여 컴파일 하는 경우에만 사용 가능하도록 변경될 예정이다. 컨트랙트에서 C API를 직접 사용하거나, 방금 소개한 binary_extension 을 사용한 특수한 방식으로 개발하는 경우에는 동작 원리를 충분히 이해한 상태에서 꼭 필요한 경우만 제한적으로 사용해야 한다.

variant type

variant 타입은 ABI 1.1부터 지원한다. 컨트랙트 코드 상에서 대응되는 타입은 std::variant 로 액션의 전달인자를 std::variant 로 지정하는 경우 ABI 에는 "variant_ + 타입 + _ + 타입 + ..." 형태로 표현된다. 또한 "variants": [] 항목 안에 타입 정의가 포함된다. (단, EOSIO.CDT 1.5 버전에서는 variant 타입 ABI가 제대로 생성되지 않는 버그가 있다. 1.6 버전에서 해결되었다.)

variant 타입을 사용하여 생성한 ABI는 위와 같으며 값을 보낼 때는 타입을 명시해야 한다.

$ cleos push action test printvar '[["int64", 1234]]' -p test@active
executed transaction: 542799f44b3dd6f96404ba3bf2e7fd58aa1a6c49a3c347f4d200e6288f88f6ac 104 bytes 100 us
# test <= test::printvar {"var":["int64",1234]}
>> 1234
$ cleos push action test printvar '[["float64", 1234.00]]' -p test@active
executed transaction: 2b8e70fded44dd3ec3dbc6390feb71774526d26daae3524e3c6be2f1a5b4adc1 104 bytes 100 us
# test <= test::printvar {"var":["float64","1234.00000000000000000"]}
>> 1.234000000000000e+03

결론

EOSIO에서 ABI 파일의 역할이나 타입을 기술하는 기본적인 방식에 대해 다뤘다. 사실 공식 문서에서 충분한 설명이 이뤄졌어야 하는 부분인데 CDT 자체가 호환성을 깨뜨리는 수준의 변경이 잦은 단계라 문서화가 잘 이루어지지 않고 있다.

ABI는 자동으로 생성되는데 꼭 이렇게 자세히 알아야 하는가 생각할 수 있다. 이에 대해 공식 문서는 이렇게 설명한다.

ABI 파일은 eosio.cdt 에서 제공하는 eosio-cpp 유틸리티를 이용하여 생성할 수 있다. 하지만 ABI의 생성에 오작동이 생기거나 실패하게 하는 몇 가지 상황이 있다. 고급 C++ 패턴에서 실수를 하거나, 때때로 커스텀 타입이 ABI 생성에 문제를 일으키기도 한다. 따라서 개발자는 ABI 파일이 어떻게 작동하는지 반드시 이해해야 하고, 필요한 경우 디버깅하거나 수정할 수 있어야 한다.

ABI 파일이 자동으로 생성된다고 해서 꼭 생성된 파일만 사용해야 하는 것은 아니다. 컨트랙트를 공개할 때 수동으로 작성한 ABI 파일을 함께 배포하기도 하고, 자동으로 생성된 파일을 수정해야 하는 경우도 있다. 이번 글을 통해 EOSIO ABI의 모든 것을 이해하긴 어렵더라도 분석의 큰 틀을 잡는데 조금이나마 도움이 되었으면 한다.

--

--