├── .soliumignore ├── .gitattributes ├── .gitignore ├── .soliumrc.json ├── .solhint.json ├── hardhat.config.js ├── README.md ├── package.json ├── .github └── workflows │ └── ci.yaml ├── LICENSE ├── test ├── testcbor.js └── TestCBOR.sol └── contracts └── CBOR.sol /.soliumignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | artifacts 4 | cache 5 | -------------------------------------------------------------------------------- /.soliumrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solium:recommended", 3 | "plugins": [ 4 | "security" 5 | ], 6 | "rules": { 7 | "security/no-inline-assembly": "off", 8 | "quotes": ["error", "double"], 9 | "indentation": ["error", 4] 10 | } 11 | } -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": [], 4 | "rules": { 5 | "quotes": ["error", "double"], 6 | "avoid-suicide": "warn", 7 | "avoid-sha3": "warn", 8 | "avoid-low-level-calls": "warn", 9 | "compiler-version": ["off", "^0.4.24"], 10 | "avoid-throw": false, 11 | "var-name-mixedcase": false, 12 | "mark-callable-contracts": false 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("@nomiclabs/hardhat-truffle5"); 2 | require("hardhat-gas-reporter"); 3 | 4 | // You need to export an object to set up your config 5 | // Go to https://hardhat.org/config/ to learn more 6 | 7 | /** 8 | * @type import('hardhat/config').HardhatUserConfig 9 | */ 10 | module.exports = { 11 | solidity: { 12 | version: "0.8.7", 13 | settings: { 14 | optimizer: { 15 | enabled: true, 16 | runs: 200 17 | } 18 | } 19 | }, 20 | gasReporter: { 21 | currency: "USD", 22 | gasPrice: 1 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # solidity-cborutils [![GitHub workflow changelog](https://img.shields.io/github/workflow/status/smartcontractkit/solidity-cborutils/CI?style=flat-square&label=github-actions)](https://github.com/smartcontractkit/solidity-cborutils/actions?query=workflow%3ACI) 2 | Utilities for encoding [CBOR](http://cbor.io/) data in solidity 3 | 4 | ## Install 5 | 6 | ```bash 7 | $ git clone https://github.com/smartcontractkit/solidity-cborutils.git 8 | $ cd solidity-cborutils 9 | $ yarn install 10 | ``` 11 | ## Usage 12 | 13 | The Buffer library is not intended to be moved to storage. 14 | In order to persist a Buffer in storage from memory, 15 | you must manually copy each of its attributes to storage, 16 | and back out when moving it back to memory. 17 | 18 | ## Test 19 | 20 | ```bash 21 | $ yarn test 22 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@ensdomains/buffer": "0.1.0" 4 | }, 5 | "homepage": "https://github.com/smartcontractkit/solidity-cborutils", 6 | "license": "MIT", 7 | "name": "solidity-cborutils", 8 | "scripts": { 9 | "test": "hardhat test", 10 | "lint:sol": "solhint -f table **/*.sol" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/smartcontractkit/solidity-cborutils.git" 15 | }, 16 | "version": "2.0.0", 17 | "devDependencies": { 18 | "@nomiclabs/hardhat-truffle5": "^2.0.0", 19 | "@nomiclabs/hardhat-web3": "^2.0.0", 20 | "hardhat": "^2.0.6", 21 | "hardhat-gas-reporter": "^1.0.4", 22 | "cbor": ">=5.0.0", 23 | "solhint": "^3.3.6", 24 | "solium": "^1.2.5", 25 | "truffle": "^5", 26 | "truffle-assertions": "^0.9.2" 27 | }, 28 | "files": [ 29 | "contracts" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | pull_request: 9 | branches: 10 | - "*" 11 | 12 | jobs: 13 | build: 14 | name: Unit Tests 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Install Node.js 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: 10.x 24 | - run: npm install -g yarn 25 | - run: yarn install --frozen-lockfile 26 | - run: yarn test 27 | 28 | solidity-lint: 29 | name: Linting 30 | 31 | runs-on: ubuntu-latest 32 | 33 | steps: 34 | - uses: actions/checkout@v2 35 | - name: Install Node.js 36 | uses: actions/setup-node@v1 37 | with: 38 | node-version: 10.x 39 | - run: npm install -g yarn 40 | - run: yarn install --frozen-lockfile 41 | - run: yarn lint:sol 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 SmartContract ChainLink, Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/testcbor.js: -------------------------------------------------------------------------------- 1 | const TestCBOR = artifacts.require('./TestCBOR.sol'); 2 | const cbor = require('cbor'); 3 | const truffleAssert = require('truffle-assertions'); 4 | 5 | contract('CBOR', function(accounts) { 6 | it('returns valid CBOR-encoded data', async function() { 7 | var test = await TestCBOR.new(); 8 | var result = new Buffer.from((await test.getTestData()).slice(2), 'hex'); 9 | var decoded = await cbor.decodeFirst(result); 10 | assert.deepEqual(decoded, { 11 | 'key1': 'value1', 12 | 'long': 'This string is longer than 24 characters.', 13 | 'bytes': Buffer.from('Test'), 14 | 'true': true, 15 | 'false': false, 16 | 'null': null, 17 | 'undefined': undefined, 18 | 'array': [0, 1, 23, 24, 0x100, 0x10000, 0x100000000, -42] 19 | }); 20 | }); 21 | 22 | it('returns > 8 byte int as bytes', async function() { 23 | var test = await TestCBOR.new(); 24 | var result = cbor.decodeFirstSync(new Buffer.from((await test.getTestDataBigInt()).slice(2), 'hex')); 25 | expect(result.map((x) => x.toFixed())).to.deep.equal([ 26 | '-57896044618658097711785492504343953926634992332820282019728792003956564819968', 27 | '-57896044618658097711785492504343953926634992332820282019728792003956564819967', 28 | '57896044618658097711785492504343953926634992332820282019728792003956564819966', 29 | '57896044618658097711785492504343953926634992332820282019728792003956564819967', 30 | '-9223372036854775808', 31 | '-9223372036854775807', 32 | '9223372036854775806', 33 | '9223372036854775807' 34 | ]); 35 | }); 36 | 37 | it('returns > 8 byte uint as bytes', async function() { 38 | var test = await TestCBOR.new(); 39 | var result = cbor.decodeFirstSync(new Buffer.from((await test.getTestDataBigUint()).slice(2), 'hex')); 40 | expect(result.map((x) => x.toFixed())).to.deep.equal([ 41 | '0', 42 | '1', 43 | '115792089237316195423570985008687907853269984665640564039457584007913129639934', 44 | '115792089237316195423570985008687907853269984665640564039457584007913129639935' 45 | ]); 46 | }); 47 | 48 | function toHexString(byteArray) { 49 | return Array.from(byteArray, function(byte) { 50 | return ('0' + (byte & 0xFF).toString(16)).slice(-2); 51 | }).join('') 52 | } 53 | 54 | it('returns definite-length encoded array', async function() { 55 | var test = await TestCBOR.new(); 56 | var expected = []; 57 | for (let x = 0; x < 1024; x++) { 58 | expected.push(x.toFixed()) 59 | } 60 | var result = cbor.decodeFirstSync(new Buffer.from((await test.getTestDataDefiniteLengthArray()).slice(2), 'hex')); 61 | expect(result.map((x) => x.toFixed())).to.deep.equal(expected); 62 | }); 63 | 64 | it('returns definite-length encoded map', async function() { 65 | var test = await TestCBOR.new(); 66 | var result = cbor.decodeFirstSync(new Buffer.from((await test.getTestDataDefiniteLengthMap()).slice(2), 'hex')); 67 | expect(result).to.deep.equal({ 68 | "a": 100, 69 | "b": 200, 70 | "c": 300 71 | }); 72 | }); 73 | 74 | it('reverts for invalid CBOR', async function() { 75 | var test = await TestCBOR.new(); 76 | await truffleAssert.reverts( 77 | test.getTestDataInvalidCBOR(), 78 | "Invalid CBOR" 79 | ); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /test/TestCBOR.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >= 0.4.19 < 0.9.0; 3 | 4 | import "../contracts/CBOR.sol"; 5 | 6 | contract TestCBOR { 7 | using CBOR for CBOR.CBORBuffer; 8 | 9 | function getTestData() public pure returns(bytes memory) { 10 | CBOR.CBORBuffer memory buf = CBOR.create(64); 11 | 12 | // Maps 13 | buf.startMap(); 14 | 15 | // Short strings 16 | buf.writeKVString("key1", "value1"); 17 | 18 | // Longer strings 19 | buf.writeKVString("long", "This string is longer than 24 characters."); 20 | 21 | // Bytes 22 | buf.writeKVBytes("bytes", bytes("Test")); 23 | 24 | // Bools, null, undefined 25 | buf.writeKVBool("true", true); 26 | buf.writeKVBool("false", false); 27 | buf.writeKVNull("null"); 28 | buf.writeKVUndefined("undefined"); 29 | 30 | // Arrays 31 | buf.writeKVArray("array"); 32 | buf.writeUInt64(0); 33 | buf.writeUInt64(1); 34 | buf.writeUInt64(23); 35 | buf.writeUInt64(24); 36 | 37 | // 2, 4, and 8 byte numbers. 38 | buf.writeUInt64(0x100); 39 | buf.writeUInt64(0x10000); 40 | buf.writeUInt64(0x100000000); 41 | 42 | // Negative numbers 43 | buf.writeInt64(-42); 44 | 45 | buf.endSequence(); 46 | buf.endSequence(); 47 | 48 | return buf.data(); 49 | } 50 | 51 | function getTestDataBigInt() public pure returns(bytes memory) { 52 | CBOR.CBORBuffer memory buf = CBOR.create(128); 53 | 54 | buf.startArray(); 55 | buf.writeInt256(type(int256).min); 56 | buf.writeInt256(type(int256).min+1); 57 | buf.writeInt256(type(int256).max-1); 58 | buf.writeInt256(type(int256).max); 59 | buf.writeInt64(type(int64).min); 60 | buf.writeInt64(type(int64).min+1); 61 | buf.writeInt64(type(int64).max-1); 62 | buf.writeInt64(type(int64).max); 63 | buf.endSequence(); 64 | 65 | return buf.data(); 66 | } 67 | 68 | function getTestDataBigUint() public pure returns(bytes memory) { 69 | CBOR.CBORBuffer memory buf = CBOR.create(128); 70 | 71 | buf.startArray(); 72 | buf.writeUInt256(type(uint256).min); 73 | buf.writeUInt256(type(uint256).min+1); 74 | buf.writeUInt256(type(uint256).max-1); 75 | buf.writeUInt256(type(uint256).max); 76 | buf.endSequence(); 77 | 78 | return buf.data(); 79 | } 80 | 81 | function getTestDataDefiniteLengthArray() public pure returns(bytes memory) { 82 | CBOR.CBORBuffer memory buf = CBOR.create(128); 83 | 84 | uint64 length = 1024; 85 | buf.startFixedArray(length); 86 | for (uint64 i = 0; i < length; i++) { 87 | buf.writeInt64(int64(i)); 88 | } 89 | 90 | return buf.data(); 91 | } 92 | 93 | function getTestDataDefiniteLengthMap() public pure returns(bytes memory) { 94 | CBOR.CBORBuffer memory buf = CBOR.create(128); 95 | 96 | buf.startFixedMap(3); 97 | buf.writeKVInt64("a", 100); 98 | buf.writeKVInt64("b", 200); 99 | buf.writeKVInt64("c", 300); 100 | 101 | return buf.data(); 102 | } 103 | 104 | function getTestDataInvalidCBOR() public pure returns(bytes memory) { 105 | CBOR.CBORBuffer memory buf = CBOR.create(128); 106 | 107 | buf.startArray(); 108 | buf.startArray(); 109 | buf.startArray(); 110 | buf.writeUInt256(type(uint256).min); 111 | buf.endSequence(); 112 | 113 | return buf.data(); // this would revert() 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /contracts/CBOR.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | import "@ensdomains/buffer/contracts/Buffer.sol"; 5 | 6 | /** 7 | * @dev A library for populating CBOR encoded payload in Solidity. 8 | * 9 | * https://datatracker.ietf.org/doc/html/rfc7049 10 | * 11 | * The library offers various write* and start* methods to encode values of different types. 12 | * The resulted buffer can be obtained with data() method. 13 | * Encoding of primitive types is staightforward, whereas encoding of sequences can result 14 | * in an invalid CBOR if start/write/end flow is violated. 15 | * For the purpose of gas saving, the library does not verify start/write/end flow internally, 16 | * except for nested start/end pairs. 17 | */ 18 | 19 | library CBOR { 20 | using Buffer for Buffer.buffer; 21 | 22 | struct CBORBuffer { 23 | Buffer.buffer buf; 24 | uint256 depth; 25 | } 26 | 27 | uint8 private constant MAJOR_TYPE_INT = 0; 28 | uint8 private constant MAJOR_TYPE_NEGATIVE_INT = 1; 29 | uint8 private constant MAJOR_TYPE_BYTES = 2; 30 | uint8 private constant MAJOR_TYPE_STRING = 3; 31 | uint8 private constant MAJOR_TYPE_ARRAY = 4; 32 | uint8 private constant MAJOR_TYPE_MAP = 5; 33 | uint8 private constant MAJOR_TYPE_TAG = 6; 34 | uint8 private constant MAJOR_TYPE_CONTENT_FREE = 7; 35 | 36 | uint8 private constant TAG_TYPE_BIGNUM = 2; 37 | uint8 private constant TAG_TYPE_NEGATIVE_BIGNUM = 3; 38 | 39 | uint8 private constant CBOR_FALSE = 20; 40 | uint8 private constant CBOR_TRUE = 21; 41 | uint8 private constant CBOR_NULL = 22; 42 | uint8 private constant CBOR_UNDEFINED = 23; 43 | 44 | function create(uint256 capacity) internal pure returns(CBORBuffer memory cbor) { 45 | Buffer.init(cbor.buf, capacity); 46 | cbor.depth = 0; 47 | return cbor; 48 | } 49 | 50 | function data(CBORBuffer memory buf) internal pure returns(bytes memory) { 51 | require(buf.depth == 0, "Invalid CBOR"); 52 | return buf.buf.buf; 53 | } 54 | 55 | function writeUInt256(CBORBuffer memory buf, uint256 value) internal pure { 56 | buf.buf.appendUint8(uint8((MAJOR_TYPE_TAG << 5) | TAG_TYPE_BIGNUM)); 57 | writeBytes(buf, abi.encode(value)); 58 | } 59 | 60 | function writeInt256(CBORBuffer memory buf, int256 value) internal pure { 61 | if (value < 0) { 62 | buf.buf.appendUint8( 63 | uint8((MAJOR_TYPE_TAG << 5) | TAG_TYPE_NEGATIVE_BIGNUM) 64 | ); 65 | writeBytes(buf, abi.encode(uint256(-1 - value))); 66 | } else { 67 | writeUInt256(buf, uint256(value)); 68 | } 69 | } 70 | 71 | function writeUInt64(CBORBuffer memory buf, uint64 value) internal pure { 72 | writeFixedNumeric(buf, MAJOR_TYPE_INT, value); 73 | } 74 | 75 | function writeInt64(CBORBuffer memory buf, int64 value) internal pure { 76 | if(value >= 0) { 77 | writeFixedNumeric(buf, MAJOR_TYPE_INT, uint64(value)); 78 | } else{ 79 | writeFixedNumeric(buf, MAJOR_TYPE_NEGATIVE_INT, uint64(-1 - value)); 80 | } 81 | } 82 | 83 | function writeBytes(CBORBuffer memory buf, bytes memory value) internal pure { 84 | writeFixedNumeric(buf, MAJOR_TYPE_BYTES, uint64(value.length)); 85 | buf.buf.append(value); 86 | } 87 | 88 | function writeString(CBORBuffer memory buf, string memory value) internal pure { 89 | writeFixedNumeric(buf, MAJOR_TYPE_STRING, uint64(bytes(value).length)); 90 | buf.buf.append(bytes(value)); 91 | } 92 | 93 | function writeBool(CBORBuffer memory buf, bool value) internal pure { 94 | writeContentFree(buf, value ? CBOR_TRUE : CBOR_FALSE); 95 | } 96 | 97 | function writeNull(CBORBuffer memory buf) internal pure { 98 | writeContentFree(buf, CBOR_NULL); 99 | } 100 | 101 | function writeUndefined(CBORBuffer memory buf) internal pure { 102 | writeContentFree(buf, CBOR_UNDEFINED); 103 | } 104 | 105 | function startArray(CBORBuffer memory buf) internal pure { 106 | writeIndefiniteLengthType(buf, MAJOR_TYPE_ARRAY); 107 | buf.depth += 1; 108 | } 109 | 110 | function startFixedArray(CBORBuffer memory buf, uint64 length) internal pure { 111 | writeDefiniteLengthType(buf, MAJOR_TYPE_ARRAY, length); 112 | } 113 | 114 | function startMap(CBORBuffer memory buf) internal pure { 115 | writeIndefiniteLengthType(buf, MAJOR_TYPE_MAP); 116 | buf.depth += 1; 117 | } 118 | 119 | function startFixedMap(CBORBuffer memory buf, uint64 length) internal pure { 120 | writeDefiniteLengthType(buf, MAJOR_TYPE_MAP, length); 121 | } 122 | 123 | function endSequence(CBORBuffer memory buf) internal pure { 124 | writeIndefiniteLengthType(buf, MAJOR_TYPE_CONTENT_FREE); 125 | buf.depth -= 1; 126 | } 127 | 128 | function writeKVString(CBORBuffer memory buf, string memory key, string memory value) internal pure { 129 | writeString(buf, key); 130 | writeString(buf, value); 131 | } 132 | 133 | function writeKVBytes(CBORBuffer memory buf, string memory key, bytes memory value) internal pure { 134 | writeString(buf, key); 135 | writeBytes(buf, value); 136 | } 137 | 138 | function writeKVUInt256(CBORBuffer memory buf, string memory key, uint256 value) internal pure { 139 | writeString(buf, key); 140 | writeUInt256(buf, value); 141 | } 142 | 143 | function writeKVInt256(CBORBuffer memory buf, string memory key, int256 value) internal pure { 144 | writeString(buf, key); 145 | writeInt256(buf, value); 146 | } 147 | 148 | function writeKVUInt64(CBORBuffer memory buf, string memory key, uint64 value) internal pure { 149 | writeString(buf, key); 150 | writeUInt64(buf, value); 151 | } 152 | 153 | function writeKVInt64(CBORBuffer memory buf, string memory key, int64 value) internal pure { 154 | writeString(buf, key); 155 | writeInt64(buf, value); 156 | } 157 | 158 | function writeKVBool(CBORBuffer memory buf, string memory key, bool value) internal pure { 159 | writeString(buf, key); 160 | writeBool(buf, value); 161 | } 162 | 163 | function writeKVNull(CBORBuffer memory buf, string memory key) internal pure { 164 | writeString(buf, key); 165 | writeNull(buf); 166 | } 167 | 168 | function writeKVUndefined(CBORBuffer memory buf, string memory key) internal pure { 169 | writeString(buf, key); 170 | writeUndefined(buf); 171 | } 172 | 173 | function writeKVMap(CBORBuffer memory buf, string memory key) internal pure { 174 | writeString(buf, key); 175 | startMap(buf); 176 | } 177 | 178 | function writeKVArray(CBORBuffer memory buf, string memory key) internal pure { 179 | writeString(buf, key); 180 | startArray(buf); 181 | } 182 | 183 | function writeFixedNumeric( 184 | CBORBuffer memory buf, 185 | uint8 major, 186 | uint64 value 187 | ) private pure { 188 | if (value <= 23) { 189 | buf.buf.appendUint8(uint8((major << 5) | value)); 190 | } else if (value <= 0xFF) { 191 | buf.buf.appendUint8(uint8((major << 5) | 24)); 192 | buf.buf.appendInt(value, 1); 193 | } else if (value <= 0xFFFF) { 194 | buf.buf.appendUint8(uint8((major << 5) | 25)); 195 | buf.buf.appendInt(value, 2); 196 | } else if (value <= 0xFFFFFFFF) { 197 | buf.buf.appendUint8(uint8((major << 5) | 26)); 198 | buf.buf.appendInt(value, 4); 199 | } else { 200 | buf.buf.appendUint8(uint8((major << 5) | 27)); 201 | buf.buf.appendInt(value, 8); 202 | } 203 | } 204 | 205 | function writeIndefiniteLengthType(CBORBuffer memory buf, uint8 major) 206 | private 207 | pure 208 | { 209 | buf.buf.appendUint8(uint8((major << 5) | 31)); 210 | } 211 | 212 | function writeDefiniteLengthType(CBORBuffer memory buf, uint8 major, uint64 length) 213 | private 214 | pure 215 | { 216 | writeFixedNumeric(buf, major, length); 217 | } 218 | 219 | function writeContentFree(CBORBuffer memory buf, uint8 value) private pure { 220 | buf.buf.appendUint8(uint8((MAJOR_TYPE_CONTENT_FREE << 5) | value)); 221 | } 222 | } --------------------------------------------------------------------------------