Skip to content

Commit

Permalink
trie: update mpt tutorial (#3056)
Browse files Browse the repository at this point in the history
* trie: update first module of examples

* trie: update module 2 of mpt examples

* trie: update module 3 of mpt examples

* trie: simplify imports of mpt examples

* trie: update module 4 of mpt examples

* trie: remove redundant comments from code examples

* Update packages/trie/examples/merkle_patricia_trees/README.md

Co-authored-by: Scorbajio <[email protected]>

* Update packages/trie/examples/merkle_patricia_trees/README.md

Co-authored-by: Scorbajio <[email protected]>

* Update packages/trie/examples/merkle_patricia_trees/README.md

Co-authored-by: Scorbajio <[email protected]>

* Update packages/trie/examples/merkle_patricia_trees/README.md

Co-authored-by: Scorbajio <[email protected]>

* trie: clarify sentence

---------

Co-authored-by: Scorbajio <[email protected]>
  • Loading branch information
gabrocheleau and scorbajio authored Sep 26, 2023
1 parent 7a0a37b commit 35dad1a
Show file tree
Hide file tree
Showing 13 changed files with 558 additions and 422 deletions.
650 changes: 354 additions & 296 deletions packages/trie/examples/merkle_patricia_trees/README.md

Large diffs are not rendered by default.

21 changes: 11 additions & 10 deletions packages/trie/examples/merkle_patricia_trees/example1a.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
/* Example 1a - Creating and Updating a Base Trie*/

const { Trie } = require('../../dist') // We import the library required to create a basic Merkle Patricia Tree
const { Trie } = require('../../dist/cjs') // We import the library required to create a basic Merkle Patricia Tree
const { bytesToHex, bytesToUtf8, utf8ToBytes } = require('@ethereumjs/util')

const trie = new Trie() // We create an empty Merkle Patricia Tree
console.log('Empty trie root (Bytes): ', trie.root()) // The trie root (32 bytes)
console.log('Empty trie root (Bytes): ', bytesToHex(trie.root())) // The trie root (32 bytes)

async function test() {
const key = Buffer.from('testKey')
const value = Buffer.from('testValue')
const key = utf8ToBytes('testKey')
const value = utf8ToBytes('testValue')
await trie.put(key, value) // We update (using "put") the trie with the key-value pair "testKey": "testValue"
const retrievedValue = await trie.get(key) // We retrieve (using "get") the value at key "testKey"
console.log('Value (Bytes): ', retrievedValue)
console.log('Value (String): ', retrievedValue.toString())
console.log('Updated trie root:', trie.root()) // The new trie root (32 bytes)
console.log('Value (Bytes): ', bytesToHex(retrievedValue))
console.log('Value (String): ', bytesToUtf8(retrievedValue))
console.log('Updated trie root:', bytesToHex(trie.root())) // The new trie root (32 bytes)
}

test()

/*
Results:
Empty trie root (Bytes): <Buffer 56 e8 1f 17 1b cc 55 a6 ff 83 45 e6 92 c0 f8 6e 5b 48 e0 1b 99 6c ad c0 01 62 2f b5 e3 63 b4 21>
Value (Bytes): <Buffer 74 65 73 74 56 61 6c 75 65>
Empty trie root (Bytes): 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421
Value (Bytes): 0x7465737456616c7565
Value (String): testValue
Updated trie root: <Buffer 8e 81 43 67 21 33 dd 5a b0 0d fc 4b 01 14 60 ea 2a 7b 00 d9 10 dc 42 78 94 2a e9 10 5c b6 20 74>
Updated trie root: 0x8e8143672133dd5ab00dfc4b011460ea2a7b00d910dc4278942ae9105cb62074
*/
23 changes: 12 additions & 11 deletions packages/trie/examples/merkle_patricia_trees/example1b.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
/* Example 1b - Manually Creating and Updating a Secure Trie*/

const { Trie } = require('../../dist') // We import the library required to create a basic Merkle Patricia Tree
const { Trie } = require('../../dist/cjs')
const { bytesToHex, bytesToUtf8, utf8ToBytes } = require('@ethereumjs/util')
const { keccak256 } = require('ethereum-cryptography/keccak')

const trie = new Trie() // We create an empty Merkle Patricia Tree
console.log('Empty trie root (Bytes): ', trie.root()) // The trie root (32 bytes)
const trie = new Trie()
console.log('Empty trie root (Bytes): ', bytesToHex(trie.root())) // The trie root (32 bytes)

async function test() {
await trie.put(keccak256(Buffer.from('testKey')), Buffer.from('testValue')) // We update (using "put") the trie with the key-value pair "testKey": "testValue"
const value = await trie.get(keccak256(Buffer.from('testKey'))) // We retrieve (using "get") the value at key "testKey"
console.log('Value (Bytes): ', value)
console.log('Value (String): ', value.toString())
console.log('Updated trie root:', trie.root()) // The new trie root (32 bytes)
await trie.put(keccak256(utf8ToBytes('testKey')), utf8ToBytes('testValue')) // We update (using "put") the trie with the key-value pair hash("testKey"): "testValue"
const value = await trie.get(keccak256(utf8ToBytes('testKey'))) // We retrieve (using "get") the value at hash("testKey")
console.log('Value (Bytes): ', bytesToHex(value))
console.log('Value (String): ', bytesToUtf8(value))
console.log('Updated trie root:', bytesToHex(trie.root())) // The new trie root (32 bytes)
}

test()

/*
Results:
Empty trie root (Bytes): <Buffer 56 e8 1f 17 1b cc 55 a6 ff 83 45 e6 92 c0 f8 6e 5b 48 e0 1b 99 6c ad c0 01 62 2f b5 e3 63 b4 21>
Value (Bytes): <Buffer 74 65 73 74 56 61 6c 75 65>
Empty trie root (Bytes): 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421
Value (Bytes): 0x7465737456616c7565
Value (String): testValue
Updated trie root: <Buffer be ad e9 13 ab 37 dc a0 dc a2 e4 29 24 b9 18 c2 a1 ca c4 57 83 3b d8 2b 9e 32 45 de cb 87 d0 fb>
Updated trie root: 0xbeade913ab37dca0dca2e42924b918c2a1cac457833bd82b9e3245decb87d0fb
*/
23 changes: 12 additions & 11 deletions packages/trie/examples/merkle_patricia_trees/example1c.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
/* Example 1c - Creating an empty Merkle Patricia Tree and updating it with a single key-value pair */

const { Trie } = require('../../dist') // We import the library required to create a basic Merkle Patricia Tree
const { Trie } = require('../../dist/cjs')
const { bytesToHex, bytesToUtf8, utf8ToBytes } = require('@ethereumjs/util')

const trie = new Trie({ useKeyHashing: true }) // We create an empty Merkle Patricia Tree
console.log('Empty trie root (Bytes): ', trie.root()) // The trie root (32 bytes)
const trie = new Trie({ useKeyHashing: true }) // We create an empty Merkle Patricia Tree with key hashing enabled
console.log('Empty trie root (Bytes): ', bytesToHex(trie.root())) // The trie root (32 bytes)

async function test() {
await trie.put(Buffer.from('testKey'), Buffer.from('testValue')) // We update (using "put") the trie with the key-value pair "testKey": "testValue"
const value = await trie.get(Buffer.from('testKey')) // We retrieve (using "get") the value at key "testKey"
console.log('Value (Bytes): ', value)
console.log('Value (String): ', value.toString())
console.log('Updated trie root:', trie.root()) // The new trie root (32 bytes)
await trie.put(utf8ToBytes('testKey'), utf8ToBytes('testValue')) // We update (using "put") the trie with the key-value pair "testKey": "testValue"
const value = await trie.get(utf8ToBytes('testKey')) // We retrieve (using "get") the value at key "testKey"
console.log('Value (Bytes): ', bytesToHex(value))
console.log('Value (String): ', bytesToUtf8(value))
console.log('Updated trie root:', bytesToHex(trie.root())) // The new trie root (32 bytes)
}

test()

/*
Results:
Empty trie root (Bytes): <Buffer 56 e8 1f 17 1b cc 55 a6 ff 83 45 e6 92 c0 f8 6e 5b 48 e0 1b 99 6c ad c0 01 62 2f b5 e3 63 b4 21>
Value (Bytes): <Buffer 74 65 73 74 56 61 6c 75 65>
Empty trie root (Bytes): 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421
Value (Bytes): 0x7465737456616c7565
Value (String): testValue
Updated trie root: <Buffer be ad e9 13 ab 37 dc a0 dc a2 e4 29 24 b9 18 c2 a1 ca c4 57 83 3b d8 2b 9e 32 45 de cb 87 d0 fb>
Updated trie root: 0xbeade913ab37dca0dca2e42924b918c2a1cac457833bd82b9e3245decb87d0fb
*/
23 changes: 12 additions & 11 deletions packages/trie/examples/merkle_patricia_trees/example1d.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,32 @@
/* Example 1d - Deleting a Key-Value Pair from a Trie*/

const { Trie } = require('../../dist') // We import the library required to create a basic Merkle Patricia Tree
const { Trie } = require('../../dist/cjs')
const { bytesToHex, bytesToUtf8, utf8ToBytes } = require('@ethereumjs/util')

const trie = new Trie() // We create an empty Merkle Patricia Tree
console.log('Empty trie root: ', trie.root()) // The trie root
const trie = new Trie()
console.log('Empty trie root: ', bytesToHex(trie.root())) // The trie root

async function test() {
const key = Buffer.from('testKey')
const value = Buffer.from('testValue')
const key = utf8ToBytes('testKey')
const value = utf8ToBytes('testValue')
await trie.put(key, value) // We update (using "put") the trie with the key-value pair "testKey": "testValue"
const valuePre = await trie.get(key) // We retrieve (using "get") the value at key "testKey"
console.log('Value (String): ', valuePre.toString()) // We retrieve our value
console.log('Updated trie root:', trie.root()) // The new trie root
console.log('Value (String): ', bytesToUtf8(valuePre)) // We retrieve our value
console.log('Updated trie root:', bytesToHex(trie.root())) // The new trie root

await trie.del(key)
const valuePost = await trie.get(key) // We try to retrieve the value at (deleted) key "testKey"
console.log('Value at key "testKey": ', valuePost) // Key not found. Value is therefore null.
console.log('Trie root after deletion:', trie.root()) // Our trie root is back to its initial value
console.log('Trie root after deletion:', bytesToHex(trie.root())) // Our trie root is back to its initial value
}

test()

/*
Results:
Empty trie root: <Buffer 56 e8 1f 17 1b cc 55 a6 ff 83 45 e6 92 c0 f8 6e 5b 48 e0 1b 99 6c ad c0 01 62 2f b5 e3 63 b4 21>
Empty trie root: 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421
Value (String): testValue
Updated trie root: <Buffer 8e 81 43 67 21 33 dd 5a b0 0d fc 4b 01 14 60 ea 2a 7b 00 d9 10 dc 42 78 94 2a e9 10 5c b6 20 74>
Updated trie root: 0x8e8143672133dd5ab00dfc4b011460ea2a7b00d910dc4278942ae9105cb62074
Value at key "testKey": null
Trie root after deletion: <Buffer 56 e8 1f 17 1b cc 55 a6 ff 83 45 e6 92 c0 f8 6e 5b 48 e0 1b 99 6c ad c0 01 62 2f b5 e3 63 b4 21>
Trie root after deletion: 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421
*/
7 changes: 4 additions & 3 deletions packages/trie/examples/merkle_patricia_trees/example2a.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// Example 2a - Creating and looking up a null node

const { Trie } = require('../../dist') // We import the library required to create a basic Merkle Patricia Tree
const { Trie } = require('../../dist/cjs')
const { utf8ToBytes } = require('@ethereumjs/util')

const trie = new Trie() // We create an empty Merkle Patricia Tree
const trie = new Trie()

async function test() {
const node1 = await trie.findPath(Buffer.from('testKey')) // We attempt to retrieve the node using our key "testKey"
const node1 = await trie.findPath(utf8ToBytes('testKey')) // We attempt to retrieve the node using our key "testKey"
console.log('Node 1: ', node1.node) // null
}

Expand Down
47 changes: 33 additions & 14 deletions packages/trie/examples/merkle_patricia_trees/example2b.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,49 @@
// Example 2b - Creating and looking up a branch node

const { Trie } = require('../../dist') // We import the library required to create a basic Merkle Patricia Tree
const { Trie } = require('../../dist/cjs')
const { bytesToHex, bytesToUtf8, utf8ToBytes } = require('@ethereumjs/util')

const trie = new Trie() // We create an empty Merkle Patricia Tree
const trie = new Trie()

async function test() {
// Notice how similar the following keys are
console.log(Buffer.from('testKey'))
console.log(Buffer.from('testKey0'))
console.log(Buffer.from('testKeyA'))
console.log(bytesToHex(utf8ToBytes('testKey')))
console.log(bytesToHex(utf8ToBytes('testKey0')))
console.log(bytesToHex(utf8ToBytes('testKeyA')))

// Add a key-value pair to the trie for each of them
await trie.put(Buffer.from('testKey'), Buffer.from('testValue'))
await trie.put(Buffer.from('testKey0'), Buffer.from('testValue0'))
await trie.put(Buffer.from('testKeyA'), Buffer.from('testValueA'))
await trie.put(utf8ToBytes('testKey'), utf8ToBytes('testValue'))
await trie.put(utf8ToBytes('testKey0'), utf8ToBytes('testValue0'))
await trie.put(utf8ToBytes('testKeyA'), utf8ToBytes('testValueA'))

const node1 = await trie.findPath(Buffer.from('testKey')) // We retrieve the node at the "branching" off of the keys
const node1 = await trie.findPath(utf8ToBytes('testKey')) // We retrieve the node at the "branching" off of the keys
console.log('Node 1: ', node1.node) // A branch node! We can see that it contains 16 branches and a value.
console.log('Node 1 value: ', bytesToUtf8(node1.node._value)) // The branch node's value

console.log('Node 1 value: ', node1.node._value.toString()) // The branch node's value
console.log('Node 1 branches: ', node1.node._branches) // All of its branches are empty, except at index 4 (corresponding to hex value 3).
await trie.put(utf8ToBytes('testKey0'), utf8ToBytes('testValue0'))
await trie.put(utf8ToBytes('testKeyA'), utf8ToBytes('testValueA'))

console.log('Value of branch at index 3: ', node1.node._branches[3][1].toString())
console.log('Value of branch at index 4: ', node1.node._branches[4][1].toString())
console.log('Node 1 branches: ', node1.node._branches) // All of its branches are empty, except at index 4 (corresponding to hex value 3).

const node2 = await trie.findPath(Buffer.from('testKe')) // We retrieve the node at the "branching" off of the keys
console.log('Node 1 branch 3 hex value: ', bytesToHex(node1.node._branches[3][1]))
console.log('Node 1 branch 4 hex value: ', bytesToHex(node1.node._branches[4][1]))
console.log(
'Node 1 branch 3 (hex): path: ',
bytesToHex(node1.node._branches[3][0]),
' | value: ',
bytesToHex(node1.node._branches[3][1])
)
console.log(
'Node 1 branch 4 (hex): path: ',
bytesToHex(node1.node._branches[4][0]),
' | value:',
bytesToHex(node1.node._branches[4][1])
)

console.log('Value of branch at index 3: ', bytesToUtf8(node1.node._branches[3][1]))
console.log('Value of branch at index 4: ', bytesToUtf8(node1.node._branches[4][1]))

const node2 = await trie.findPath(utf8ToBytes('testKe')) // We retrieve the node at "testKe"
console.log('Node 2: ', node2.node)
}

Expand Down
15 changes: 8 additions & 7 deletions packages/trie/examples/merkle_patricia_trees/example2c.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
// Example 2c - Creating and looking up a leaf node

const { Trie } = require('../../dist') // We import the library required to create a basic Merkle Patricia Tree
const { Trie } = require('../../dist/cjs')
const { bytesToUtf8, utf8ToBytes } = require('@ethereumjs/util')

const trie = new Trie() // We create an empty Merkle Patricia Tree
const trie = new Trie()

async function test() {
await trie.put(Buffer.from('testKey'), Buffer.from('testValue'))
await trie.put(Buffer.from('testKey0'), Buffer.from('testValue0'))
await trie.put(utf8ToBytes('testKey'), utf8ToBytes('testValue'))
await trie.put(utf8ToBytes('testKey0'), utf8ToBytes('testValue0'))

const node1 = await trie.findPath(Buffer.from('testKey0')) // We retrieve one of the leaf nodes
console.log('Node 1: ', node1.node) // A leaf node! We can see that it contains 2 items: the encodedPath and the value
console.log('Node 1 value: ', node1.node._value.toString()) // The leaf node's value
const node1 = await trie.findPath(utf8ToBytes('testKey0')) // We retrieve one of the leaf nodes
console.log('Node 1: ', node1.node) // A leaf node! We can see that it contains 2 items: the nibbles and the value
console.log('Node 1 value: ', bytesToUtf8(node1.node._value)) // The leaf node's value
}

test()
23 changes: 12 additions & 11 deletions packages/trie/examples/merkle_patricia_trees/example2d.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
// Example 2d - Creating and looking up an extension node

const { Trie } = require('../../dist') // We import the library required to create a basic Merkle Patricia Tree
const { Trie } = require('../../dist/cjs')
const { bytesToHex, utf8ToBytes } = require('@ethereumjs/util')

const trie = new Trie() // We create an empty Merkle Patricia Tree
const trie = new Trie()

async function test() {
console.log(Buffer.from('testKey'))
console.log(Buffer.from('testKey0001'))
console.log(Buffer.from('testKey000A'))
console.log(bytesToHex(utf8ToBytes('testKey')))
console.log(bytesToHex(utf8ToBytes('testKey0001')))
console.log(bytesToHex(utf8ToBytes('testKey000A')))

await trie.put(Buffer.from('testKey'), Buffer.from('testValue'))
await trie.put(Buffer.from('testKey0001'), Buffer.from('testValue1'))
await trie.put(Buffer.from('testKey000A'), Buffer.from('testValueA'))
await trie.put(utf8ToBytes('testKey'), utf8ToBytes('testValue'))
await trie.put(utf8ToBytes('testKey0001'), utf8ToBytes('testValue1'))
await trie.put(utf8ToBytes('testKey000A'), utf8ToBytes('testValueA'))

const node1 = await trie.findPath(Buffer.from('testKey'))
const node1 = await trie.findPath(utf8ToBytes('testKey'))
console.log(node1.node) // The branch node
console.log('Node: ', node1.node._branches[3]) // The address of our child node. Let's look it up:

const node2 = await trie.lookupNode(Buffer.from(node1.node._branches[3]))
const node2 = await trie.lookupNode(node1.node._branches[3])
console.log(node2) // An extension node!

const node3 = await trie.lookupNode(Buffer.from(node2._value))
const node3 = await trie.lookupNode(node2._value)
console.log(node3)
}

Expand Down
32 changes: 23 additions & 9 deletions packages/trie/examples/merkle_patricia_trees/example3a.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
// Example 3a - Generating a hash

const { Trie } = require('../../dist') // We import the library required to create a basic Merkle Patricia Tree
const { Trie } = require('../../dist/cjs')
const rlp = require('@ethereumjs/rlp')
const { bytesToHex, utf8ToBytes } = require('@ethereumjs/util')
const { keccak256 } = require('ethereum-cryptography/keccak')
const trie = new Trie() // We create an empty Merkle Patricia Tree
const trie = new Trie()

async function test() {
// We populate the tree to create an extension node.
await trie.put(Buffer.from('testKey'), Buffer.from('testValue'))
await trie.put(Buffer.from('testKey0001'), Buffer.from('testValue1'))
await trie.put(Buffer.from('testKey000A'), Buffer.from('testValueA'))
await trie.put(utf8ToBytes('testKey'), utf8ToBytes('testValue'))
await trie.put(utf8ToBytes('testKey0001'), utf8ToBytes('testValue1'))
await trie.put(utf8ToBytes('testKey000A'), utf8ToBytes('testValueA'))

const node1 = await trie.findPath(Buffer.from('testKey'))
const node2 = await trie.lookupNode(Buffer.from(node1.node._branches[3]))
const node1 = await trie.findPath(utf8ToBytes('testKey'))
const node2 = await trie.lookupNode(node1.node._branches[3])
const node3 = await trie.lookupNode(node2._value)

console.log('Our computed hash: ', Buffer.from(keccak256(rlp.encode(node2.raw()))))
console.log('The extension node hash: ', node1.node._branches[3])
console.log('Extension node:', node2)
console.log('Branch node:', node3._branches)
console.log('Branch node hash:', bytesToHex(node2._value))
console.log(
'Branch node branch 4:',
'path: ',
bytesToHex(node3._branches[4][0]),
' | value: ',
bytesToHex(node3._branches[4][1])
)

console.log('Raw node:', bytesToHex(rlp.encode(node2.raw())))
console.log('Our computed hash: ', bytesToHex(keccak256(rlp.encode(node2.raw()))))
console.log('The extension node hash: ', bytesToHex(node1.node._branches[3]))
}

test()
34 changes: 17 additions & 17 deletions packages/trie/examples/merkle_patricia_trees/example3b.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
// Example 3b - Verification using a hash

const { Trie } = require('../../dist') // We import the library required to create a basic Merkle Patricia Tree
const { Trie } = require('../../dist/cjs')
const { bytesToHex, utf8ToBytes } = require('@ethereumjs/util')
const trie1 = new Trie()
const trie2 = new Trie()

async function test() {
// We populate the tree to create an extension node.
await trie1.put(utf8ToBytes('testKey'), utf8ToBytes('testValue'))
await trie1.put(utf8ToBytes('testKey0001'), utf8ToBytes('testValue1'))
await trie1.put(utf8ToBytes('testKey000A'), utf8ToBytes('testValueA'))

await trie1.put(Buffer.from('testKey'), Buffer.from('testValue'))
await trie1.put(Buffer.from('testKey0001'), Buffer.from('testValue1'))
await trie1.put(Buffer.from('testKey000A'), Buffer.from('testValueA'))
await trie2.put(utf8ToBytes('testKey'), utf8ToBytes('testValue'))
await trie2.put(utf8ToBytes('testKey0001'), utf8ToBytes('testValue1'))
await trie2.put(utf8ToBytes('testKey000z'), utf8ToBytes('testValuez'))

await trie2.put(Buffer.from('testKey'), Buffer.from('testValue'))
await trie2.put(Buffer.from('testKey0001'), Buffer.from('testValue1'))
await trie2.put(Buffer.from('testKey000z'), Buffer.from('testValuez'))
const temp1 = await trie1.findPath(utf8ToBytes('testKey'))
const temp2 = await trie2.findPath(utf8ToBytes('testKey'))

const temp1 = await trie1.findPath(Buffer.from('testKey'))
const temp2 = await trie2.findPath(Buffer.from('testKey'))
const node1 = await trie1.lookupNode(temp1.node._branches[3])
const node2 = await trie2.lookupNode(temp2.node._branches[3])

const node1 = await trie1.lookupNode(Buffer.from(temp1.node._branches[3]))
const node2 = await trie2.lookupNode(Buffer.from(temp2.node._branches[3]))
console.log('Branch node 1 hash: ', bytesToHex(node1._value))
console.log('Branch node 2 hash: ', bytesToHex(node2._value))

console.log('Branch node 1 hash: ', node1._value)
console.log('Branch node 2 hash: ', node2._value)

console.log('Root of trie 1: ', trie1.root())
console.log('Root of trie 2: ', trie2.root())
console.log('Root of trie 1: ', bytesToHex(trie1.root()))
console.log('Root of trie 2: ', bytesToHex(trie2.root()))
}

test()
Loading

0 comments on commit 35dad1a

Please sign in to comment.