Skip to content

Commit

Permalink
Zellic 3.4: Bias in hashToField function
Browse files Browse the repository at this point in the history
The function hashToField is used to hash a uint256 value toFp. The
resulting value is later mapped to a point on an elliptic curve with the
function mapToG2. The message is first hashed with the keccak256
function and then transformed to a value in the finite field with the
function maskBits:

```solidity
// This matches mcl maskN, this only takes the 254 bits for the field, if it is
still greater than the field then take the 253 bits
function maskBits(uint256 input) internal pure returns (uint256) {
    uint256 mask = ~uint256(0) - 0xC0;
    if (byteSwap(input & mask) >= FIELD_MODULUS) {
        mask = ~uint256(0) - 0xE0;
    }
    return input & mask;
}
```

The two first bits of the value are set to zero, and then if the value
is still bigger than p, the next bit is masked to zero. It means that
the values between p and 2^254−1 are mapped to a value between p−2^253
and 2^253−1. Values between zero and p are left unchanged, resulting in
values in the range [p−2^253, 2^253−1] having twice the probability to
be chosen as output.

---

Our solution to this is to implement RFC9380. The implementation has
largely been taken from Hopr's crypto implementation of RFC9380 tweaked
for the BLS usecase.
  • Loading branch information
darcys22 authored and Doy-lee committed Jun 27, 2024
1 parent 9a907e9 commit 4f88995
Show file tree
Hide file tree
Showing 10 changed files with 496 additions and 92 deletions.
10 changes: 6 additions & 4 deletions contracts/ServiceNodeRewards.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ contract ServiceNodeRewards is Initializable, Ownable2StepUpgradeable, PausableU
bytes32 public rewardTag;
bytes32 public removalTag;
bytes32 public liquidateTag;
bytes32 public hashToG2Tag;

uint256 private _stakingRequirement;
uint256 private _maxContributors;
Expand Down Expand Up @@ -68,6 +69,7 @@ contract ServiceNodeRewards is Initializable, Ownable2StepUpgradeable, PausableU
rewardTag = buildTag("BLS_SIG_TRYANDINCREMENT_REWARD");
removalTag = buildTag("BLS_SIG_TRYANDINCREMENT_REMOVE");
liquidateTag = buildTag("BLS_SIG_TRYANDINCREMENT_LIQUIDATE");
hashToG2Tag = buildTag("BLS_SIG_HASH_TO_FIELD_TAG");
signatureExpiry = 10 minutes;

designatedToken = IERC20(token_);
Expand Down Expand Up @@ -209,7 +211,7 @@ contract ServiceNodeRewards is Initializable, Ownable2StepUpgradeable, PausableU
// NOTE: Validate signature
{
bytes memory encodedMessage = abi.encodePacked(rewardTag, recipientAddress, recipientRewards);
BN256G2.G2Point memory Hm = BN256G2.hashToG2(encodedMessage);
BN256G2.G2Point memory Hm = BN256G2.hashToG2(encodedMessage, hashToG2Tag);
validateSignatureOrRevert(ids, blsSignature, Hm);
}

Expand Down Expand Up @@ -345,7 +347,7 @@ contract ServiceNodeRewards is Initializable, Ownable2StepUpgradeable, PausableU
caller,
serviceNodePubkey
);
BN256G2.G2Point memory Hm = BN256G2.hashToG2(encodedMessage);
BN256G2.G2Point memory Hm = BN256G2.hashToG2(encodedMessage, hashToG2Tag);

BN256G2.G2Point memory signature = BN256G2.G2Point(
[blsSignature.sigs1, blsSignature.sigs0],
Expand Down Expand Up @@ -424,7 +426,7 @@ contract ServiceNodeRewards is Initializable, Ownable2StepUpgradeable, PausableU
// NOTE: Validate signature
{
bytes memory encodedMessage = abi.encodePacked(removalTag, blsPubkey.X, blsPubkey.Y, timestamp);
BN256G2.G2Point memory Hm = BN256G2.hashToG2(encodedMessage);
BN256G2.G2Point memory Hm = BN256G2.hashToG2(encodedMessage, hashToG2Tag);
validateSignatureOrRevert(ids, blsSignature, Hm);
}

Expand Down Expand Up @@ -502,7 +504,7 @@ contract ServiceNodeRewards is Initializable, Ownable2StepUpgradeable, PausableU
// NOTE: Validate signature
{
bytes memory encodedMessage = abi.encodePacked(liquidateTag, blsPubkey.X, blsPubkey.Y, timestamp);
BN256G2.G2Point memory Hm = BN256G2.hashToG2(encodedMessage);
BN256G2.G2Point memory Hm = BN256G2.hashToG2(encodedMessage, hashToG2Tag);
validateSignatureOrRevert(ids, blsSignature, Hm);
}

Expand Down
218 changes: 169 additions & 49 deletions contracts/libraries/BN256G2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ pragma solidity ^0.8.20;
* @dev Homepage: https://github.com/musalbas/solidity-BN256G2
*/

import "hardhat/console.sol";

library BN256G2 {
uint256 internal constant CURVE_ORDER_FACTOR = 4965661367192848881; // this is also knows as z, generates prime definine base field (FIELD MODULUS) and order of the curve for BN curves
uint256 internal constant FIELD_MODULUS = 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47;
Expand Down Expand Up @@ -575,29 +577,8 @@ library BN256G2 {
return (x1, FIELD_MODULUS - x2);
}

function memcpy(bytes memory dest, bytes memory src, uint size) private pure {
// Copy by word
uint offset = 32; // Advance past the length encoding of the array
uint wordCount = size / 32;
assembly {
let destPtr := add(dest, offset)
let srcPtr := add(src, offset)
for { let i := 0 } lt(i, wordCount) { i := add(i, 1) } {
mstore(destPtr, mload(srcPtr))
destPtr := add(destPtr, 32)
srcPtr := add(srcPtr, 32)
}
}

// Copy tail end (remaining bytes)
for (uint i = (wordCount * 32); i < size; i++) {
dest[i] = src[i];
}
}

// hashes to G2 using the try and increment method
//function mapToG2(uint256 h) internal view returns (G2Point memory) {
function mapToG2(bytes memory message) internal view returns (G2Point memory) {
// Hashes to G2 using the try and increment method
function mapToG2(bytes memory message, bytes32 hashToG2Tag) internal view returns (G2Point memory) {

// Define the G2Point coordinates
uint256 x1;
Expand All @@ -606,12 +587,18 @@ library BN256G2 {
uint256 y2 = 0;

bytes memory message_with_i = new bytes(message.length + 1 /*bytes*/);
memcpy(message_with_i, message, message.length);
for (uint index = 0; index < message.length; index++) {
message_with_i[index] = message[index];
}

for (uint8 increment = 0;; increment++) { // Iterate until we find a valid G2 point
message_with_i[message_with_i.length - 1] = bytes1(increment);
x1 = byteSwap(maskBits(uint256(convertArrayAsLE(keccak256(message_with_i)))));
x2 = 0;

// TODO: Has side-effects in devnet that causes the hashToField to
// generate the correct values. No-op on other networks.
console.logBytes(message_with_i);

(x1, x2) = hashToField(message_with_i, hashToG2Tag);

(uint256 yx, uint256 yy) = Get_yy_coordinate(x1, x2); // Try to get y^2
(uint256 sqrt_x, uint256 sqrt_y) = FQ2Sqrt(yx, yy); // Calculate square root
Expand All @@ -627,38 +614,171 @@ library BN256G2 {
return (G2Point([x2, x1], [y2, y1]));
}

function hashToG2(bytes memory message) internal view returns (G2Point memory) {
G2Point memory map = mapToG2(message);
function hashToG2(bytes memory message, bytes32 hashToG2Tag) internal view returns (G2Point memory) {
G2Point memory map = mapToG2(message, hashToG2Tag);
(uint256 x1, uint256 x2, uint256 y1, uint256 y2) = ECTwistMulByCofactor(map.X[1], map.X[0], map.Y[1], map.Y[0]);
return (G2Point([x2, x1], [y2, y1]));
}

function convertArrayAsLE(bytes32 src) internal pure returns (bytes32) {
bytes32 dst;
for (uint256 i = 0; i < 32; i++) {
// Considering each byte of bytes32
bytes1 s = src[i];
// Assuming the role of D is just to cast or store our byte in this context
dst |= bytes32(s) >> (i * 8);
uint256 private constant KECCAK256_BLOCKSIZE = 136;

/**
* Takes an arbitrary byte-string and a domain seperation tag (dst) and
* returns two elements of the field with prime `FIELD_MODULUS`. This
* implementation is taken from Hopr's crypto implementation and repurposed
* for a BN256 curve:
*
* github.com/hoprnet/hoprnet/blob/53e3f49855775af8e92b465306be144038167b63/ethereum/contracts/src/Crypto.sol
*
* @dev DSTs longer than 255 bytes are considered unsound.
* see https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-16.html#name-domain-separation
*
* @param message the message to hash
* @param dst domain separation tag, used to make protocol instantiations unique
*/
function hashToField(bytes memory message, bytes32 dst) public view returns (uint256 u0, uint256 u1) {
(bytes32 b1, bytes32 b2, bytes32 b3) = expandMessageXMDKeccak256(message, abi.encodePacked(dst));

// computes ([...b1[..], ...b2[0..16]] ^ 1) mod n
// solhint-disable-next-line no-inline-assembly
assembly {
let p := mload(0x40) // next free memory slot
mstore(p, 0x30) // Length of Base
mstore(add(p, 0x20), 0x20) // Length of Exponent
mstore(add(p, 0x40), 0x20) // Length of Modulus
mstore(add(p, 0x60), b1) // Base
mstore(add(p, 0x80), b2)
mstore(add(p, 0x90), 1) // Exponent
mstore(add(p, 0xb0), FIELD_MODULUS) // Modulus
if iszero(staticcall(not(0), 0x05, p, 0xD0, p, 0x20)) { revert(0, 0) }

u0 := mload(p)
}
return dst;
}

// This matches mcl maskN, this only takes the 254 bits for the field, if it is still greater than the field then take the 253 bits
function maskBits(uint256 input) internal pure returns (uint256) {
uint256 mask = ~uint256(0) - 0xC0;
if (byteSwap(input & mask) >= FIELD_MODULUS) {
mask = ~uint256(0) - 0xE0;
// computes ([...b2[16..32], ...b3[..]] ^ 1) mod n
// solhint-disable-next-line no-inline-assembly
assembly {
let p := mload(0x40)
mstore(p, 0x30) // Length of Base
mstore(add(p, 0x20), 0x20) // Length of Exponent
mstore(add(p, 0x50), b2)
mstore(add(p, 0x40), 0x20) // Length of Modulus
mstore(add(p, 0x70), b3) // Base
mstore(add(p, 0x90), 1) // Exponent
mstore(add(p, 0xb0), FIELD_MODULUS) // Modulus
if iszero(staticcall(not(0), 0x05, p, 0xD0, p, 0x20)) { revert(0, 0) }

u1 := mload(p)
}
return input & mask;
}

function byteSwap(uint256 value) internal pure returns (uint256) {
uint256 swapped = 0;
for (uint256 i = 0; i < 32; i++) {
uint256 byteValue = (value >> (i * 8)) & 0xFF;
swapped |= byteValue << (256 - 8 - (i * 8));
/**
* Expands an arbitrary byte-string to 96 bytes using the
* `expand_message_xmd` method described in
*
* https://www.rfc-editor.org/rfc/rfc9380.html#name-expand_message_xmd
*
* This implementation is taken from Hopr's crypto implementation:
*
* github.com/hoprnet/hoprnet/blob/53e3f49855775af8e92b465306be144038167b63/ethereum/contracts/src/Crypto.sol
*
* Used for hashToField functionality to generate points within
* FIELD_MODULUS such that bias of selecting such numbers is beneath 2^-128
* as recommended by RFC9380.
*
* @dev DSTs longer than 255 bytes are considered unsound.
* see https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-16.html#name-domain-separation
*
* @param message the message to hash
* @param dst domain separation tag, used to make protocol instantiations unique
*/
function expandMessageXMDKeccak256(
bytes memory message,
bytes memory dst
)
public
pure
returns (bytes32 b1, bytes32 b2, bytes32 b3)
{
// solhint-disable-next-line no-inline-assembly
bytes32 b0;
assembly {
if gt(mload(dst), 255) { revert(0, 0) }

//let b0
{
// create payload for b0 hash
let b0Payload := mload(0x40)

// payload[0..KECCAK256_BLOCKSIZE] = 0

let b0PayloadO := KECCAK256_BLOCKSIZE // leave first block empty
let msg_o := 0x20 // skip length prefix

// payload[KECCAK256_BLOCKSIZE..KECCAK256_BLOCKSIZE+message.len()] = message[0..message.len()]
for { let i := 0 } lt(i, mload(message)) { i := add(i, 0x20) } {
mstore(add(b0Payload, b0PayloadO), mload(add(message, msg_o)))
b0PayloadO := add(b0PayloadO, 0x20)
msg_o := add(msg_o, 0x20)
}

// payload[KECCAK256_BLOCKSIZE+message.len()+1..KECCAK256_BLOCKSIZE+message.len()+2] = 96
b0PayloadO := add(mload(message), 137)
mstore8(add(b0Payload, b0PayloadO), 0x60) // only support for 96 bytes output length

let dstO := 0x20
b0PayloadO := add(b0PayloadO, 2)

// payload[KECCAK256_BLOCKSIZE+message.len()+3..KECCAK256_BLOCKSIZE+message.len()+dst.len()]
// = dst[0..dst.len()]
for { let i := 0 } lt(i, mload(dst)) { i := add(i, 0x20) } {
mstore(add(b0Payload, b0PayloadO), mload(add(dst, dstO)))
b0PayloadO := add(b0PayloadO, 0x20)
dstO := add(dstO, 0x20)
}

// payload[KECCAK256_BLOCKSIZE+message.len()+dst.len()..KECCAK256_BLOCKSIZE+message.len()+dst.len()+1]
// = dst.len()
b0PayloadO := add(add(mload(message), mload(dst)), 139)
mstore8(add(b0Payload, b0PayloadO), mload(dst))

b0 := keccak256(b0Payload, add(140, add(mload(dst), mload(message))))
}

// create payload for b1, b2 ... hashes
let bIPayload := mload(0x40)
mstore(bIPayload, b0)
// payload[32..33] = 1
mstore8(add(bIPayload, 0x20), 1)

let payloadO := 0x21
let dstO := 0x20

// payload[33..33+dst.len()] = dst[0..dst.len()]
for { let i := 0 } lt(i, mload(dst)) { i := add(i, 0x20) } {
mstore(add(bIPayload, payloadO), mload(add(dst, dstO)))
payloadO := add(payloadO, 0x20)
dstO := add(dstO, 0x20)
}

// payload[65+dst.len()..66+dst.len()] = dst.len()
mstore8(add(bIPayload, add(0x21, mload(dst))), mload(dst))

b1 := keccak256(bIPayload, add(34, mload(dst)))

// payload[0..32] = b0 XOR b1
mstore(bIPayload, xor(b0, b1))
// payload[32..33] = 2
mstore8(add(bIPayload, 0x20), 2)

b2 := keccak256(bIPayload, add(34, mload(dst)))

// payload[0..32] = b0 XOR b2
mstore(bIPayload, xor(b0, b2))
// payload[32..33] = 2
mstore8(add(bIPayload, 0x20), 3)

b3 := keccak256(bIPayload, add(34, mload(dst)))
}
return swapped;
}
}
5 changes: 3 additions & 2 deletions contracts/test/BN256G2EchidnaTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,16 @@ contract BN256G2EchidnaTest {

bytes private message;
BN256G2.G2Point Hm;
bytes32 dummyTag;

constructor() {
message = bytes("1");
Hm = BN256G2.hashToG2(message);
Hm = BN256G2.hashToG2(message, dummyTag);
}

function setMessage(bytes calldata _message) public {
message = _message;
Hm = BN256G2.hashToG2(message);
Hm = BN256G2.hashToG2(message, dummyTag);
}

function echidna_always_hashable() public returns (bool) {
Expand Down
1 change: 1 addition & 0 deletions test/cpp/cmake/SourcesAndHeaders.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ set(test_sources
src/basic.cpp
src/basic_ethereum.cpp
src/rewards_contract.cpp
src/hash.cpp
)
24 changes: 24 additions & 0 deletions test/cpp/include/service_node_rewards/ec_utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,34 @@
#undef MCLBN_NO_AUTOLINK
#pragma GCC diagnostic pop

#include <span>

namespace utils
{
std::string BLSPublicKeyToHex(const bls::PublicKey& publicKey);
bls::PublicKey HexToBLSPublicKey(std::string_view hex);
std::string SignatureToHex(bls::Signature sig);
std::array<unsigned char, 32> HashModulus(std::string message);

/// Expand an arbitrary `msg` string into `out` bytes of entropy via the
/// method outlined in RFC9380 `expand_message_xmd` detailed here:
///
/// https://www.rfc-editor.org/rfc/rfc9380.html#name-expand_message_xmd
///
/// This implementation uses keccak256 as a seeding function to generate the
/// entropy.
///
/// An optional domain separation tag `dst` can be passed in as further
/// seeding information to disambiguate the generated bytes from different
/// domains given the same `msg`.
///
/// The buffer passed in as `out` must have a capacity that is
/// a multiple of 32 and have a capacity, 0 <= `outSize` <= 256, otherwise
/// the function asserts. `dst` must be <= 255 bytes otherwise the
/// function asserts.
///
/// This function was taken from herumi/bls's implementation and modified to
/// use keccak256 to align with our Solidity implementation of expand
/// message.
void ExpandMessageXMDKeccak256(std::span<uint8_t> out, std::span<const uint8_t> msg, std::span<const uint8_t> dst);
}
8 changes: 4 additions & 4 deletions test/cpp/include/service_node_rewards/service_node_list.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class ServiceNode {
uint64_t service_node_id = SERVICE_NODE_LIST_SENTINEL;
ServiceNode() = default;
ServiceNode(uint64_t _service_node_id);
bls::Signature blsSignHash(std::span<const char> bytes) const;
bls::Signature blsSignHash(std::span<const uint8_t> bytes, uint32_t chainID, std::string_view contractAddress) const;
std::string proofOfPossession(uint32_t chainID, const std::string& contractAddress, const std::string& senderEthAddress, const std::string& serviceNodePubkey);
std::string getPublicKeyHex() const;
bls::PublicKey getPublicKey() const;
Expand All @@ -46,12 +46,12 @@ class ServiceNodeList {
std::string getLatestNodePubkey();

std::string aggregatePubkeyHex();
std::string aggregateSignatures(const std::string& message);
std::string aggregateSignaturesFromIndices(const std::string& message, const std::vector<int64_t>& indices);
std::string aggregateSignatures(const std::string& message, uint32_t chainID, std::string_view contractAddress);
std::string aggregateSignaturesFromIndices(const std::string& message, const std::vector<int64_t>& indices, uint32_t chainID, std::string_view contractAddress);

std::tuple<std::string, uint64_t, std::string> liquidateNodeFromIndices(uint64_t nodeID, uint32_t chainID, const std::string& contractAddress, const std::vector<uint64_t>& indices);
std::tuple<std::string, uint64_t, std::string> removeNodeFromIndices(uint64_t nodeID, uint32_t chainID, const std::string& contractAddress, const std::vector<uint64_t>& indices);
std::string updateRewardsBalance(const std::string& address, const uint64_t amount, const uint32_t chainID, const std::string& contractAddress, const std::vector<uint64_t>& service_node_ids);
std::string updateRewardsBalance(const std::string& address, uint64_t amount, uint32_t chainID, const std::string& contractAddress, const std::vector<uint64_t>& service_node_ids);

std::vector<uint64_t> findNonSigners(const std::vector<uint64_t>& indices);
std::vector<uint64_t> randomSigners(const size_t numOfRandomIndices);
Expand Down
Loading

0 comments on commit 4f88995

Please sign in to comment.