Skip to content

Testing #1454

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 17 commits into from
91 changes: 91 additions & 0 deletions SSQ.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# SimpleSquanch (SSQ)

The squanchy alternative to SSQ inspired by the [Varint serializer of Protocol Buffer](https://developers.google.com/protocol-buffers/docs/encoding#varints) and @metachris's [minimalistic approach](https://github.com/metachris/binary-serializer#base-128-varints). It is also an alternative to [Solidity's encodePacked()](https://docs.soliditylang.org/en/latest/abi-spec.html?highlight=encode#non-standard-packed-mode) which requires fixed-length fields and at most 1 variable-length field to decode without ambiguity.

## What is it?

A serialization scheme supporting simple binary fields of variable length.


### Features
- Space-efficient
- Varints do not require a fixed length.


### How does it work?
The delimitation of the fields relies on the MSB of each byte which is used as a flag to signal whether there are more bytes to process.

For example:
- If the payload fits in 7 bits (=1 byte without MSB), then the MSB is set to 0
- If the payload needs 2 or more bytes, each byte's MSB is set to 0 except the last one (from right to left).


### Limitations
- No support for complex types such as arrays, mappings or nesting.
- No concern with data types (signed/unsigned integers, strings etc), everything is bytes.


### Differences

In SSQ, the MSB flag has a different meaning:
- In SSQ, the MSB flag means "there are more bytes after this one", assuming that they are processed from least to most significant (right to left).
- In Protobuf and Metachris serializer, it is assumed that they are processed from the most to least significant (left to right) instead.
- The rationale for SSQ is that it appeared to make an humble decoding implementation more straightforward.

#### Differences with Solidity abi.encodePacked()
- The only way to decode packed bytes is by knowing the fixed length of each field which is fine at one point in time, but over time the required length for a field may change. Dynamic fields lead to ambiguities.
- SSQ is not affected by the above limitation, it supports dynamic fields of arbitrary length.

#### Differences with ProtoBuf
- SSQ lacks 98% of most of ProtoBuf's features: no (proto) schema, no nesting, no optional or repeating field, no string...
- Protobuf encodes the Varint's LSB first.
- Optimized for speed, closer to the network wire order

#### Differences with Metachris binary serializer
- SSQ supports only Varints, not packages.


### Simple Specifications

Each byte uses 7 bits for the payload storage, the MSB being reserved as a "more bytes after this?" flag.
```
Bit Values: [ x | 64 | 32 | 16 | 8 | 4 | 2 | 1 ]
|
+ last-varint-byte indicator (0=last, 1=next-also-length-byte)
```

Therefore we can store:
- in 1 byte: any value between 0x00..0x7F
- in 2 bytes: any value between 0x00..0x3FFF
...

There is a loss of 1 bit of storage but it is compensated by not having to either:
- reserve additional space for a Length field (to allow for variable length)
- agree on a fixed length now while attempting to predict the future possible range of values for the field.


### Specifications as code

👉 [Collab Python notebook](https://colab.research.google.com/drive/1QmRpkwmUYXBH1RPu6TTX1ZmTuZ5c1EZu#scrollTo=wvQJVfJwqOvj)


### Encoding examples

```
input | [ varint byte 1 ] | [ varint byte 0 ] | [ output ]
---------------+----------------------+---------------------+--------------
1 = 0x0001 | | [ 0 0 0 0 0 0 0 1 ] | 0x0001
127 = 0x007F | | [ 0 1 1 1 1 1 1 1 ] | 0x007F
128 = 0x0080 | [ 0 0 0 0 0 0 0 1 ] | [ 1 0 0 0 0 0 0 0 ] | 0x0180
255 = 0x00FF | [ 0 0 0 0 0 0 0 1 ] | [ 1 1 1 1 1 1 1 1 ] | 0x01FF
256 = 0x0100 | [ 0 0 0 0 0 0 1 0 ] | [ 1 0 0 0 0 0 0 0 ] | 0x0280
16383 = 0x3FFF | [ 0 1 1 1 1 1 1 1 ] | [ 1 1 1 1 1 1 1 1 ] | 0x7FFF
...

input | [ varint byte 2 ] | [ varint byte 1 ] | [ varint byte 0 ] | [ output ]
---------------+----------------------+---------------------+---------------------+------------
16484 = 0x4000 | [ 0 0 0 0 0 0 0 1 ] | [ 1 0 0 0 0 0 0 0 ] | [ 1 0 0 0 0 0 0 0 ] | 0x018080

```


9 changes: 9 additions & 0 deletions contracts/deploy/00-home-chain-arbitration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,17 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment)
from: deployer,
log: true,
});

const SSQLibrary = await deploy("SSQ", {
from: deployer,
log: true,
});

const disputeKit = await deploy("DisputeKitClassic", {
from: deployer,
libraries: {
SSQ: SSQLibrary.address
},
args: [deployer, AddressZero, rng.address],
log: true,
});
Expand Down Expand Up @@ -58,6 +66,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment)
from: deployer,
libraries: {
SortitionSumTreeFactory: sortitionSumTreeLibrary.address,
SSQ: SSQLibrary.address
},
args: [deployer, pnk, AddressZero, disputeKit.address, false, minStake, alpha, feeForJuror, 3, [0, 0, 0, 0], 3],
log: true,
Expand Down
45 changes: 43 additions & 2 deletions contracts/src/arbitration/KlerosCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ pragma solidity ^0.8;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./IArbitrator.sol";
import "./IDisputeKit.sol";
import {SSQ} from "../libraries/SSQ.sol";
import {SortitionSumTreeFactory} from "../data-structures/SortitionSumTreeFactory.sol";

/**
* @title KlerosCore
* Core arbitrator contract for Kleros v2.
*/
contract KlerosCore is IArbitrator {
using SSQ for bytes32; // Use library functions for deserialization to reduce L1 calldata costs on Optimistic Rollups.
using SortitionSumTreeFactory for SortitionSumTreeFactory.SortitionSumTrees; // Use library functions for sortition sum trees.

// ************************************* //
Expand Down Expand Up @@ -341,6 +343,16 @@ contract KlerosCore is IArbitrator {
// * State Modifiers * //
// ************************************* //

/** @dev Sets the caller's stake in a subcourt by passing serialized args to reduce L1 calldata gas costs on optimistic rollups.
* @param _args The SSQ serialized arguments.
*/
function setStake(bytes32 _args) external{
uint256 subcourtID;
uint256 stake;
(subcourtID, _args) = _args.unsquanchUint256();
(stake, _args) = _args.unsquanchUint256();
require(setStakeForAccount(msg.sender, uint96(subcourtID), stake, 0), "Staking failed");
}
/** @dev Sets the caller's stake in a subcourt.
* @param _subcourtID The ID of the subcourt.
* @param _stake The new stake.
Expand All @@ -349,19 +361,48 @@ contract KlerosCore is IArbitrator {
require(setStakeForAccount(msg.sender, _subcourtID, _stake, 0), "Staking failed");
}

/** @dev Creates a dispute by calling _creatDispute() with serialized args to reduce L1 calldata costs with optimistic rollups.
* @param _args The SSQ serialized arguments passed to _createDispute(...)
* @return disputeID The ID of the created dispute.
*/
function createDispute(bytes32 _args)
external
payable
returns (uint256 disputeID)
{
uint numberOfChoices;
bytes memory extraData;
(numberOfChoices, _args) = _args.unsquanchUint256();
(extraData, _args) = _args.unsquanchBytesLeftPadded();
return _createDispute(numberOfChoices, extraData);
}

/** @dev Creates a dispute. Must be called by the arbitrable contract.
* @param _numberOfChoices Number of choices for the jurors to choose from.
* @param _extraData Additional info about the dispute. We use it to pass the ID of the dispute's subcourt (first 32 bytes),
* the minimum number of jurors required (next 32 bytes) and the ID of the specific dispute kit (last 32 bytes).
* @return disputeID The ID of the created dispute.
*/
function createDispute(uint256 _numberOfChoices, bytes memory _extraData)
function createDispute(uint256 _numberOfChoices, bytes calldata _extraData)
external
payable
override
payable
returns (uint256 disputeID)
{
require(msg.value >= arbitrationCost(_extraData), "Not enough ETH to cover arbitration cost.");
return _createDispute(_numberOfChoices, _extraData);
}

/** @dev Creates a dispute. Must be called by the arbitrable contract.
* @param _numberOfChoices Number of choices for the jurors to choose from.
* @param _extraData Additional info about the dispute. We use it to pass the ID of the dispute's subcourt (first 32 bytes),
* the minimum number of jurors required (next 32 bytes) and the ID of the specific dispute kit (last 32 bytes).
* @return disputeID The ID of the created dispute.
*/
function _createDispute(uint256 _numberOfChoices, bytes memory _extraData)
internal
returns (uint256 disputeID)
{
(uint96 subcourtID, , uint8 disputeKitID) = extraDataToSubcourtIDMinJurorsDisputeKit(_extraData);

uint256 bitToCheck = 1 << disputeKitID; // Get the bit that corresponds with dispute kit's ID.
Expand Down
Loading