From d74a73075dab77e168d86c291b7f4d5d91e2e2d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Levente=20T=C3=B3th?= Date: Sat, 14 Jan 2023 00:22:08 +0100 Subject: [PATCH 1/6] feat: signatures contract init --- contracts/signatures/PostageStampSig.sol | 30 +++++++++++ contracts/signatures/Signature.sol | 69 ++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 contracts/signatures/PostageStampSig.sol create mode 100644 contracts/signatures/Signature.sol diff --git a/contracts/signatures/PostageStampSig.sol b/contracts/signatures/PostageStampSig.sol new file mode 100644 index 0000000..1f72fba --- /dev/null +++ b/contracts/signatures/PostageStampSig.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./Signature.sol"; + +library PostageStampSig { + /** Hash of the message to sign */ + function getMessageHash( + bytes32 _chunkAddr, + bytes32 _batchId, + uint64 _index, + uint64 _timeStamp + ) public pure returns (bytes32) { + return keccak256(abi.encodePacked(_chunkAddr, _batchId, _index, _timeStamp)); + } + + function verify( + address _signer, // signer Ethereum address to check against + bytes memory _signature, + bytes32 _chunkAddr, + bytes32 _postageId, + uint64 _index, + uint64 _timeStamp + ) public pure returns (bool) { + bytes32 messageHash = getMessageHash(_chunkAddr, _postageId, _index, _timeStamp); + bytes32 ethMessageHash = Signature.getEthSignedMessageHash(messageHash); + + return Signature.recoverSigner(ethMessageHash, _signature) == _signer; + } +} diff --git a/contracts/signatures/Signature.sol b/contracts/signatures/Signature.sol new file mode 100644 index 0000000..f5fbcf8 --- /dev/null +++ b/contracts/signatures/Signature.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; +// based on: https://solidity-by-example.org/signature/ + +/* Signature Verification + +How to Sign and Verify +# Signing +1. Create message to sign +2. Hash the message +3. Sign the hash (off chain, keep your private key secret) + +# Verify +1. Recreate hash from the original message +2. Recover signer from signature and hash +3. Compare recovered signer to claimed signer +*/ + +library Signature { + /** Appends Ethereum Signed Message prefix to the message hash */ + function getEthSignedMessageHash( + bytes32 _messageHash + ) public pure returns (bytes32) { + /* + Signature is produced by signing a keccak256 hash with the following format: + "\x19Ethereum Signed Message\n" + len(msg) + msg + */ + return + keccak256( + abi.encodePacked("\x19Ethereum Signed Message:\n32", _messageHash) + ); + } + + function recoverSigner( + bytes32 _ethSignedMessageHash, // it has to be prefixed message: https://ethereum.stackexchange.com/questions/19582/does-ecrecover-in-solidity-expects-the-x19ethereum-signed-message-n-prefix/21037 + bytes memory _signature + ) public pure returns (address) { + (bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature); + + return ecrecover(_ethSignedMessageHash, v, r, s); + } + + function splitSignature( + bytes memory sig + ) public pure returns (bytes32 r_, bytes32 s_, uint8 v_) { + require(sig.length == 65, "invalid signature length"); + + assembly { + /* + verbose explanation: https://ethereum.stackexchange.com/questions/135591/split-signature-function-in-solidity-by-example-docs + First 32 bytes stores the length of the signature + + add(sig, 32) = pointer of sig + 32 + effectively, skips first 32 bytes of signature + + mload(p) loads next 32 bytes starting at the memory address p into memory + */ + + // first 32 bytes, after the length prefix + r_ := mload(add(sig, 32)) + // second 32 bytes + s_ := mload(add(sig, 64)) + // final byte (first byte of the next 32 bytes) + v_ := byte(0, mload(add(sig, 96))) + } + + // implicitly return (r, s, v) + } +} From 4e0e34bcdf3eab9ee19106ee089063d75c0c2cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Levente=20T=C3=B3th?= Date: Sat, 14 Jan 2023 00:22:47 +0100 Subject: [PATCH 2/6] test: postage stamp signature --- test/PostageStampSig.ts | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 test/PostageStampSig.ts diff --git a/test/PostageStampSig.ts b/test/PostageStampSig.ts new file mode 100644 index 0000000..0326ac1 --- /dev/null +++ b/test/PostageStampSig.ts @@ -0,0 +1,38 @@ +import { expect } from 'chai' +import { ethers } from 'hardhat' + +describe('VerifySignature', function () { + it('Check signature', async function () { + const accounts = await ethers.getSigners() + + const Signature = await ethers.getContractFactory('Signature') + const signature = await Signature.deploy() + await signature.deployed() + const PostageStampSig = await ethers.getContractFactory('PostageStampSig', { + libraries: { + Signature: signature.address, + }, + }) + const postageStampSig = await PostageStampSig.deploy() + await postageStampSig.deployed() + + const signer = accounts[0] + const chunkAddrHex = '0x98371fb1297da62c1355abd8f9a7e43dd20cd5ddb9db7ba7c2d99e35e7afb58f' + const batchIdHex = '0xba73d3e3dfe9bd95595c6fa67f682adf3fde207ab339cffd7cc24a1905389a9c' + const index = BigInt('1039382085632') + const ts = BigInt('1673624779490180673') + + const hash = await postageStampSig.getMessageHash(chunkAddrHex, batchIdHex, index, ts) + const sig = await signer.signMessage(ethers.utils.arrayify(hash)) + + const ethHash = await signature.getEthSignedMessageHash(hash) + + expect(signer.address).to.equal(await signature.recoverSigner(ethHash, sig)) + + // Correct signature and message returns true + expect(await postageStampSig.verify(signer.address, sig, chunkAddrHex, batchIdHex, index, ts)).to.equal(true) + + // Incorrect message returns false + expect(await postageStampSig.verify(signer.address, sig, chunkAddrHex, batchIdHex, '0', ts)).to.equal(false) + }) +}) From 0376b076fc0e22f2a3e5e441e06a92be3ef81ac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Levente=20T=C3=B3th?= Date: Mon, 16 Jan 2023 10:30:28 +0100 Subject: [PATCH 3/6] refactor: library to contract --- contracts/signatures/PostageStampSig.sol | 2 +- contracts/signatures/Signature.sol | 2 +- test/PostageStampSig.ts | 13 +++---------- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/contracts/signatures/PostageStampSig.sol b/contracts/signatures/PostageStampSig.sol index 1f72fba..dcc1d5b 100644 --- a/contracts/signatures/PostageStampSig.sol +++ b/contracts/signatures/PostageStampSig.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; import "./Signature.sol"; -library PostageStampSig { +contract PostageStampSig is Signature { /** Hash of the message to sign */ function getMessageHash( bytes32 _chunkAddr, diff --git a/contracts/signatures/Signature.sol b/contracts/signatures/Signature.sol index f5fbcf8..c6c1018 100644 --- a/contracts/signatures/Signature.sol +++ b/contracts/signatures/Signature.sol @@ -16,7 +16,7 @@ How to Sign and Verify 3. Compare recovered signer to claimed signer */ -library Signature { +contract Signature { /** Appends Ethereum Signed Message prefix to the message hash */ function getEthSignedMessageHash( bytes32 _messageHash diff --git a/test/PostageStampSig.ts b/test/PostageStampSig.ts index 0326ac1..07bd44b 100644 --- a/test/PostageStampSig.ts +++ b/test/PostageStampSig.ts @@ -5,14 +5,7 @@ describe('VerifySignature', function () { it('Check signature', async function () { const accounts = await ethers.getSigners() - const Signature = await ethers.getContractFactory('Signature') - const signature = await Signature.deploy() - await signature.deployed() - const PostageStampSig = await ethers.getContractFactory('PostageStampSig', { - libraries: { - Signature: signature.address, - }, - }) + const PostageStampSig = await ethers.getContractFactory('PostageStampSig') const postageStampSig = await PostageStampSig.deploy() await postageStampSig.deployed() @@ -25,9 +18,9 @@ describe('VerifySignature', function () { const hash = await postageStampSig.getMessageHash(chunkAddrHex, batchIdHex, index, ts) const sig = await signer.signMessage(ethers.utils.arrayify(hash)) - const ethHash = await signature.getEthSignedMessageHash(hash) + const ethHash = await postageStampSig.getEthSignedMessageHash(hash) - expect(signer.address).to.equal(await signature.recoverSigner(ethHash, sig)) + expect(signer.address).to.equal(await postageStampSig.recoverSigner(ethHash, sig)) // Correct signature and message returns true expect(await postageStampSig.verify(signer.address, sig, chunkAddrHex, batchIdHex, index, ts)).to.equal(true) From 5d975a5eeb8e79366ccbf75f6cf4fb58ddef2a6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Levente=20T=C3=B3th?= Date: Mon, 16 Jan 2023 12:09:44 +0100 Subject: [PATCH 4/6] refactor: renaming signature test --- test/{PostageStampSig.ts => Signatures.ts} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename test/{PostageStampSig.ts => Signatures.ts} (92%) diff --git a/test/PostageStampSig.ts b/test/Signatures.ts similarity index 92% rename from test/PostageStampSig.ts rename to test/Signatures.ts index 07bd44b..1b29c14 100644 --- a/test/PostageStampSig.ts +++ b/test/Signatures.ts @@ -1,8 +1,8 @@ import { expect } from 'chai' import { ethers } from 'hardhat' -describe('VerifySignature', function () { - it('Check signature', async function () { +describe('Signatues', function () { + it('should recover postage stamp signature', async function () { const accounts = await ethers.getSigners() const PostageStampSig = await ethers.getContractFactory('PostageStampSig') From 71b03bc5073f6e188e61e2e2a60302d2d2c8e996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Levente=20T=C3=B3th?= Date: Mon, 16 Jan 2023 12:14:24 +0100 Subject: [PATCH 5/6] feat: single owner chunk signature check --- contracts/signatures/SocSig.sol | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 contracts/signatures/SocSig.sol diff --git a/contracts/signatures/SocSig.sol b/contracts/signatures/SocSig.sol new file mode 100644 index 0000000..2ad8ff2 --- /dev/null +++ b/contracts/signatures/SocSig.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./Signature.sol"; + +contract SocSig is Signature { + /** Hash of the message to sign */ + function getMessageHash( + bytes32 _identifier, + bytes32 _chunkAddr + ) public pure returns (bytes32) { + return keccak256(abi.encodePacked(_identifier, _chunkAddr)); + } + + function verify( + address _signer, // signer Ethereum address to check against + bytes memory _signature, + bytes32 _identifier, + bytes32 _chunkAddr + ) public pure returns (bool) { + bytes32 messageHash = getMessageHash(_identifier, _chunkAddr); + bytes32 ethMessageHash = Signature.getEthSignedMessageHash(messageHash); + + return Signature.recoverSigner(ethMessageHash, _signature) == _signer; + } +} From 8c361a9a75dfff8052e8d6c920e6d67354e7e63d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Levente=20T=C3=B3th?= Date: Mon, 16 Jan 2023 12:15:22 +0100 Subject: [PATCH 6/6] test: single owner chunk signature recovery --- test/Signatures.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/Signatures.ts b/test/Signatures.ts index 1b29c14..ec9ce00 100644 --- a/test/Signatures.ts +++ b/test/Signatures.ts @@ -28,4 +28,36 @@ describe('Signatues', function () { // Incorrect message returns false expect(await postageStampSig.verify(signer.address, sig, chunkAddrHex, batchIdHex, '0', ts)).to.equal(false) }) + + it('should recover Single Owner Chunk signature', async function () { + const accounts = await ethers.getSigners() + + const SocSig = await ethers.getContractFactory('SocSig') + const socSig = await SocSig.deploy() + await socSig.deployed() + + const signer = accounts[0] + const topicHex = '0x0000000000000000000000000000000000000000000000000000000000000000' + const chunkAddrHex = '0x98371fb1297da62c1355abd8f9a7e43dd20cd5ddb9db7ba7c2d99e35e7afb58f' + + const hash = await socSig.getMessageHash(topicHex, chunkAddrHex) + const sig = await signer.signMessage(ethers.utils.arrayify(hash)) + + const ethHash = await socSig.getEthSignedMessageHash(hash) + + expect(signer.address).to.equal(await socSig.recoverSigner(ethHash, sig)) + + // Correct signature and message returns true + expect(await socSig.verify(signer.address, sig, topicHex, chunkAddrHex)).to.equal(true) + + // Incorrect message returns false + expect( + await socSig.verify( + signer.address, + sig, + '0x0000000000000000000000000000000000000000000000000000000000000001', + chunkAddrHex, + ), + ).to.equal(false) + }) })