Published on

EIP-712 Encoding

Authors
  • author image
    Name
    Dirga Yasa

Banner

Kenapa Hashing EIP-712

EIP-712 adalah standar untuk melakukan hashing dan signing untuk sebuah struktur object. Pada database seperti mysql biasanya kita menggunakan id integer autoincrement atau uuid pada setiap record yang tersimpan. Namun smart contract memiliki keterbatasan penyimpanan dan assign id manual untuk setiap record yang tersimpan akan menghabiskan banyak gas fee. Jadi kali ini kita akan membahas bagaimana hashing EIP-712 dapat digunakan sebagai id untuk record yang tersimpan.

Single Struct

Solidity

Hal pertama yang perlu dilakukan adalah membuat struct di solidity

solidity
struct Offer {
  address owner;
  string name;
  uint256 price;
}

Kemudian membuat typehash. Typehash single object dibuat dengan cara hashing representasi type dari struct. Berikut adalah representasi type dari struct tersebut

'Offer(address owner,string name,uint256 price)'

Jangan menambahkan spasi pada type

Kemudian di-hash menggunakan fungsi keccak256()

solidity
bytes32 constant OFFER_TYPEHASH = keccak256("Offer(address owner,string name,uint256 price)")

Kemudian lakukan encoding pada value yang akan di-hash

solidity
function hashOffer(Offer memory offer) external pure returns(bytes32) {
  bytes32 OFFER_TYPEHASH = keccak256(bytes('Offer(address owner,string name,uint256 price)'));

  bytes memory encoded = abi.encode(
    OFFER_TYPEHASH,
    offer.owner,
    keccak256(bytes(offer.name)),
    offer.price
  );

  return keccak256(encoded);
}

Proses encoding dilakukan menggunakan fungsi abi.encode(). Hal yang di-encode adalah typehash, kemudian data sesuai urutan pada struct. Untuk data dengan type string harus dilakukan hashing terlebih dahulu. abi.encode() akan menghasilkan data bytes yang artinya panjangnya dynamic. Setelah di-encode, lakukan hashing dari bytes hasil encode sehingga menghasilkan hash untuk object Offer

Javascript

Untuk melakukan verifikasi off-chain, kita juga perlu melakukan hal yang sama pada bahasa pemrograman yang beriteraksi dengan blockhain. Umumnya bahasa yang digunakan adalah javascript dengan library ethers

Hal pertama adalah membuat typehash. Typehash tetap menggunakan representasi yang sama yang digunakan sebelumnya

'Offer(address owner,string name,uint256 price)'
javascript
import ethers from 'ethers'

const typehash = ethers.keccak256(Buffer.from('Offer(address owner,string name,uint256 price)'))

Kemudian hash object.

javascript
import ethers from 'ethers'

const offer = {
  owner: '0x16f750B6bb0eeF814358773197812f2989efEEe2',
  name: 'Water Bottle',
  price: 1000
}

const hashedName = ethers.keccak256(Buffer.from(offer.name))

const encoded = coder.encode(
  ['bytes32', 'address', 'bytes32', 'uint256'],
  [typehash, offer.owner, hashedName, offer.price]
)

const hash = ethers.keccak256(Buffer.from(encoded.slice(2), 'hex'))

Ketika dijalankan akan menghasilkan hash 0x990fc339382823551c75f369768a7dae49725291532cb251e3cc64190f7e81f1.

Untuk data string, perlu untuk di-hash terlebih dahulu. coder.encode() memerlukan 2 parameter yaitu array of type dan array of value. Karena encode menghasilkan string hex, maka perlu sebelum di-hash, '0x' diawal perlu dihilangkan dan 'hex' param pada Buffer.from sebagai tanda bahwa string yang akan diubah menjadi buffer tersebut sudah berformat hex.

Nested Struct

Solidity

Sama seperti sebelumnya, buat struct solidity

solidity
struct Seller {
  address owner;
  string productName;
}

struct Buyer {
  address owner;
  uint256 price
}

struct Trade {
  Seller seller;
  Buyer buyer;
  uint256 timestamp;
}

Kemudian membuat typehash. Typehash pada nested object harus mencantumkan semua representasi type dan diurutkan sesuai abjad. Berikut adalah representasi type dari nested struct tersebut.

'Trade(Seller seller,Buyer buyer,uint256 timestamp)Buyer(address owner,uint256 price)Seller(address owner,string productName)'

Kemudian di-hash menggunakan fungsi keccak256()

solidity
bytes32 TRADE_TYPEHASH = keccak256(bytes('Trade(Seller seller,Buyer buyer,uint256 timestamp)Buyer(address owner,uint256 price)Seller(address owner,string productName)'));

Kemudian lakukan encoding pada value yang akan di-hash

solidity
function hashTrade(Trade memory trade) external pure returns(bytes32) {
  bytes32 TRADE_TYPEHASH = keccak256(bytes('Trade(Seller seller,Buyer buyer,uint256 timestamp)Buyer(address owner,uint256 price)Seller(address owner,string productName)'));

  bytes memory encodedSeller = abi.encode(
    trade.seller.owner,
    keccak256(bytes(trade.seller.productName))
  );

  bytes memory encodedBuyer = abi.encode(
    trade.buyer.owner,
    trade.buyer.price
  );

  bytes memory encodedTrade = abi.encode(
    TRADE_TYPEHASH,
    keccak256(encodedSeller),
    keccak256(encodedBuyer),
    trade.timestamp
  );

  return keccak256(encodedTrade);
}

Untuk nested object, lakukan encode dimulai dari object paling dasar. Kemudian hasil encode tersebut di-hash dan dimasukkan kedalam encode object yang lebih besar.

Javascript

Untuk nested object, prosesnya hampir mirip dengan single object. Hal pertama adalah membuat typehash. Typehash tetap menggunakan representasi yang sama yang digunakan sebelumnya

'Trade(Seller seller,Buyer buyer,uint256 timestamp)Buyer(address owner,string productName)Seller(address owner,uint256 price)'
javascript
import ethers from 'ethers'

const typehash = ethers.keccak256(Buffer.from('Trade(Seller seller,Buyer buyer,uint256 timestamp)Buyer(address owner,uint256 price)Seller(address owner,string productName)'))

Kemudian hash object

javascript
import ethers from 'ethers'

const coder = ethers.AbiCoder.defaultAbiCoder();
const trade = {
  seller: {
    owner: '0x437C9Fe9c8C85Dd704541384528559D43590efB6',
    productName: 'Water Bottle'
  },
  buyer: {
    owner: '0x459f3A0143A5798d511E0A644655F43AE1abD2f9',
    price: 1000
  },
  timestamp: 17370835505
}

// Encode Seller
const sellerHashedProductName = ethers.keccak256(Buffer.from(trade.seller.productName))
const sellerEncoded = coder.encode(
  ['address', 'bytes32'],
  [trade.seller.owner, sellerHashedProductName]
)
const sellerHash = ethers.keccak256(Buffer.from(sellerEncoded.slice(2), 'hex'))

// Encode Buyer
const buyerEncoded = coder.encode(
  ['address', 'uint256'],
  [trade.buyer.owner, trade.buyer.price]
)
const buyerHash = ethers.keccak256(Buffer.from(buyerEncoded.slice(2), 'hex'))

// Encode Trade
const tradeEncoded = coder.encode(
  ['bytes32', 'bytes32', 'bytes32', 'uint256'],
  [typehash, sellerHash, buyerHash, trade.timestamp]
)

const hash = ethers.keccak256(Buffer.from(tradeEncoded.slice(2), 'hex'))

Ketika dijalankan akan menghasilkan hash 0x6d0fdc0d54321da6983d7866f54dbfbe5c13c15a4b603729680118ea7110687a.

Jadi hashing struct/object menggunakan standar EIP-712 dapat digunakan untuk generate unique id untuk struct record yang tersimpan pada smart contract sehingga bisa menghemat storage dan gas fee untuk mapping sebuah id dengan sebuah struct record

Semoga bermanfaat 😃

Cheers.