From 970f2f701619150bec7aa9760ce38f783f9383bd Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Tue, 11 Mar 2025 16:46:25 +0200 Subject: [PATCH 1/8] forge install: exit-format 0.2.0 --- .gitmodules | 3 +++ nitro_rpc/lib/exit-format | 1 + 2 files changed, 4 insertions(+) create mode 160000 nitro_rpc/lib/exit-format diff --git a/.gitmodules b/.gitmodules index f2be8ec..28b42f5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "nitro_rpc/lib/forge-std"] path = nitro_rpc/lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "nitro_rpc/lib/exit-format"] + path = nitro_rpc/lib/exit-format + url = https://github.com/statechannels/exit-format diff --git a/nitro_rpc/lib/exit-format b/nitro_rpc/lib/exit-format new file mode 160000 index 0000000..08169bb --- /dev/null +++ b/nitro_rpc/lib/exit-format @@ -0,0 +1 @@ +Subproject commit 08169bb1e5e1dbfe1d8e131d8e088833f4f0052e From 7c64dde98900a766b3dbdd19cd88bfb634d684c7 Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Tue, 11 Mar 2025 17:20:33 +0200 Subject: [PATCH 2/8] feat: verify rpc payload --- nitro_rpc/src/NitroRPC.sol | 66 ++++++++- nitro_rpc/src/TempINitroTypes.sol | 36 +++++ nitro_rpc/src/TempNitroUtils.sol | 225 ++++++++++++++++++++++++++++++ 3 files changed, 323 insertions(+), 4 deletions(-) create mode 100644 nitro_rpc/src/TempINitroTypes.sol create mode 100644 nitro_rpc/src/TempNitroUtils.sol diff --git a/nitro_rpc/src/NitroRPC.sol b/nitro_rpc/src/NitroRPC.sol index 8fa9fd7..68b256b 100644 --- a/nitro_rpc/src/NitroRPC.sol +++ b/nitro_rpc/src/NitroRPC.sol @@ -1,23 +1,81 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; + +import {INitroTypes} from "./TempINitroTypes.sol"; +import {NitroUtils} from "./TempNitroUtils.sol"; + /** * @title NitroRPC * @dev Solidity implementation of Nitro RPC protocol structures */ interface NitroRPC { - struct Payload { uint256 requestId; + uint256 timestamp; string method; bytes params; bytes result; - uint256 timestamp; } struct PayloadSigned { Payload rpcMessage; - INitroTypes.Signature clientSig - INitroTypes.Signature serverSig + Signature clientSig; + Signature serverSig; + } + + enum AllocationIndices { + Client, + Server + } + + /** + * @notice Encodes application-specific rules for a particular ForceMove-compliant state channel. + * @dev Encodes application-specific rules for a particular ForceMove-compliant state channel. + * @param fixedPart Fixed part of the state channel. + * @param proof Array of recovered variable parts which constitutes a support proof for the candidate. + * @param candidate Recovered variable part the proof was supplied for. + */ + function stateIsSupported( + FixedPart calldata fixedPart, + RecoveredVariablePart[] calldata proof, + RecoveredVariablePart calldata candidate + ) external pure override returns (bool, string memory) { + require(fixedPart.participants.length == uint256(AllocationIndices.Server) + 1, "bad number of participants"); + + PayloadSigned memory payloadSigned = abi.decode(candidate.variablePart.appData, (PayloadSigned)); + requireValidPayload(payloadSigned); + + return (true, ""); + } + + function requireValidPayload(PayloadSigned memory payloadSigned) internal pure { + require(recoverPayloadSigner(payloadSigned.rpcMessage, payloadSigned.clientSig) == fixedPart.participants[AllocationIndices.Client], "bad client signature"); + require(recoverPayloadSigner(payloadSigned.rpcMessage, payloadSigned.serverSig) == fixedPart.participants[AllocationIndices.Server], "bad server signature"); + + // TODO: verify timestamp and requestId + } + + // This pure internal function recovers the signer address from the payload and its signature. + function recoverPayloadSigner(Payload memory payload, Signature memory signature) internal pure returns (address) { + // Encode and hash the payload data. + // Using abi.encode ensures proper padding and decoding, avoiding potential ambiguities with dynamic types. + bytes32 messageHash = keccak256( + abi.encode( + payload.requestId, + payload.timestamp, + payload.method, + payload.params, + payload.result + ) + ); + + // Apply the Ethereum Signed Message prefix. + bytes32 ethSignedMessageHash = keccak256( + abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash) + ); + + // Recover the signer address using ecrecover. + return ecrecover(ethSignedMessageHash, signature.v, signature.r, signature.s); } } diff --git a/nitro_rpc/src/TempINitroTypes.sol b/nitro_rpc/src/TempINitroTypes.sol new file mode 100644 index 0000000..1f6b222 --- /dev/null +++ b/nitro_rpc/src/TempINitroTypes.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ExitFormat as Outcome} from "../lib/exit-format/contracts/ExitFormat.sol"; + +interface INitroTypes { + struct Signature { + uint8 v; + bytes32 r; + bytes32 s; + } + + struct FixedPart { + address[] participants; + uint64 channelNonce; + address appDefinition; + uint48 challengeDuration; + } + + struct VariablePart { + Outcome.SingleAssetExit[] outcome; + bytes appData; + uint48 turnNum; + bool isFinal; + } + + struct SignedVariablePart { + VariablePart variablePart; + Signature[] sigs; + } + + struct RecoveredVariablePart { + VariablePart variablePart; + uint256 signedBy; // bitmask + } +} diff --git a/nitro_rpc/src/TempNitroUtils.sol b/nitro_rpc/src/TempNitroUtils.sol new file mode 100644 index 0000000..54185ed --- /dev/null +++ b/nitro_rpc/src/TempNitroUtils.sol @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {INitroTypes} from "./TempINitroTypes.sol"; +import {ExitFormat as Outcome} from "../lib/exit-format/contracts/ExitFormat.sol"; + +library NitroUtils { + // ***************** + // Signature methods: + // ***************** + + /** + * @notice Require supplied stateHash is signed by signer. + * @dev Require supplied stateHash is signed by signer. + * @param stateHash State hash to check. + * @param sig Signed state signature. + * @param signer Address which must have signed the state. + * @return true if signer with sig has signed stateHash. + */ + function isSignedBy(bytes32 stateHash, INitroTypes.Signature memory sig, address signer) + internal + pure + returns (bool) + { + return signer == NitroUtils.recoverSigner(stateHash, sig); + } + + /** + * @notice Check if supplied participantIndex bit is set to 1 in signedBy bit mask. + * @dev Check if supplied partitipationIndex bit is set to 1 in signedBy bit mask. + * @param signedBy Bit mask field to check. + * @param participantIndex Bit to check. + * @return true if supplied partitipationIndex bit is set to 1 in signedBy bit mask. + */ + function isClaimedSignedBy(uint256 signedBy, uint8 participantIndex) internal pure returns (bool) { + return ((signedBy >> participantIndex) % 2 == 1); + } + + /** + * @notice Check if supplied participantIndex is the only bit set to 1 in signedBy bit mask. + * @dev Check if supplied participantIndex is the only bit set to 1 in signedBy bit mask. + * @param signedBy Bit mask field to check. + * @param participantIndex Bit to check. + * @return true if supplied partitipationIndex bit is the only bit set to 1 in signedBy bit mask. + */ + function isClaimedSignedOnlyBy(uint256 signedBy, uint8 participantIndex) internal pure returns (bool) { + return (signedBy == (2 ** participantIndex)); + } + + /** + * @notice Given a digest and ethereum digital signature, recover the signer. + * @dev Given a digest and digital signature, recover the signer. + * @param _d message digest. + * @param sig ethereum digital signature. + * @return signer + */ + function recoverSigner(bytes32 _d, INitroTypes.Signature memory sig) internal pure returns (address) { + bytes32 prefixedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", _d)); + address a = ecrecover(prefixedHash, sig.v, sig.r, sig.s); + require(a != address(0), "Invalid signature"); + return (a); + } + + /** + * @notice Count number of bits set to '1', specifying the number of participants which have signed the state. + * @dev Count number of bits set to '1', specifying the number of participants which have signed the state. + * @param signedBy Bit mask field specifying which participants have signed the state. + * @return amount of signers, which have signed the state. + */ + function getClaimedSignersNum(uint256 signedBy) internal pure returns (uint8) { + uint8 amount = 0; + + for (; signedBy > 0; amount++) { + // for reference: Kernighan's Bit Counting Algorithm + signedBy &= signedBy - 1; + } + + return amount; + } + + /** + * @notice Determine indices of participants who have signed the state. + * @dev Determine indices of participants who have signed the state. + * @param signedBy Bit mask field specifying which participants have signed the state. + * @return signerIndices + */ + function getClaimedSignersIndices(uint256 signedBy) internal pure returns (uint8[] memory) { + uint8[] memory signerIndices = new uint8[](getClaimedSignersNum(signedBy)); + uint8 signerNum = 0; + uint8 acceptedSigners = 0; + + for (; signedBy > 0; signerNum++) { + if (signedBy % 2 == 1) { + signerIndices[acceptedSigners] = signerNum; + acceptedSigners++; + } + signedBy >>= 1; + } + + return signerIndices; + } + + // ***************** + // ID methods: + // ***************** + + /** + * @notice Computes the unique id of a channel. + * @dev Computes the unique id of a channel. + * @param fixedPart Part of the state that does not change + * @return channelId + */ + function getChannelId(INitroTypes.FixedPart memory fixedPart) internal pure returns (bytes32 channelId) { + channelId = keccak256( + abi.encode( + fixedPart.participants, fixedPart.channelNonce, fixedPart.appDefinition, fixedPart.challengeDuration + ) + ); + } + + // ***************** + // Hash methods: + // ***************** + + /** + * @notice Computes the hash of the state corresponding to the input data. + * @dev Computes the hash of the state corresponding to the input data. + * @param turnNum Turn number + * @param isFinal Is the state final? + * @param channelId Unique identifier for the channel + * @param appData Application specific data. + * @param outcome Outcome structure. + * @return The stateHash + */ + function hashState( + bytes32 channelId, + bytes memory appData, + Outcome.SingleAssetExit[] memory outcome, + uint48 turnNum, + bool isFinal + ) internal pure returns (bytes32) { + return keccak256(abi.encode(channelId, appData, outcome, turnNum, isFinal)); + } + + /** + * @notice Computes the hash of the state corresponding to the input data. + * @dev Computes the hash of the state corresponding to the input data. + * @param fp The FixedPart of the state + * @param vp The VariablePart of the state + * @return The stateHash + */ + function hashState(INitroTypes.FixedPart memory fp, INitroTypes.VariablePart memory vp) + internal + pure + returns (bytes32) + { + return keccak256(abi.encode(getChannelId(fp), vp.appData, vp.outcome, vp.turnNum, vp.isFinal)); + } + + /** + * @notice Hashes the outcome structure. Internal helper. + * @dev Hashes the outcome structure. Internal helper. + * @param outcome Outcome structure to encode hash. + * @return bytes32 Hash of encoded outcome structure. + */ + function hashOutcome(Outcome.SingleAssetExit[] memory outcome) internal pure returns (bytes32) { + return keccak256(Outcome.encodeExit(outcome)); + } + + // ***************** + // Equality methods: + // ***************** + + /** + * @notice Check for equality of two byte strings + * @dev Check for equality of two byte strings + * @param _preBytes One bytes string + * @param _postBytes The other bytes string + * @return true if the bytes are identical, false otherwise. + */ + function bytesEqual(bytes memory _preBytes, bytes memory _postBytes) internal pure returns (bool) { + // copied from https://www.npmjs.com/package/solidity-bytes-utils/v/0.1.1 + bool success = true; + + /* solhint-disable no-inline-assembly */ + assembly { + let length := mload(_preBytes) + + // if lengths don't match the arrays are not equal + switch eq(length, mload(_postBytes)) + case 1 { + // cb is a circuit breaker in the for loop since there's + // no said feature for inline assembly loops + // cb = 1 - don't breaker + // cb = 0 - break + let cb := 1 + + let mc := add(_preBytes, 0x20) + let end := add(mc, length) + + for { let cc := add(_postBytes, 0x20) } + // the next line is the loop condition: + // while(uint256(mc < end) + cb == 2) + eq(add(lt(mc, end), cb), 2) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + // if any of these checks fails then arrays are not equal + if iszero(eq(mload(mc), mload(cc))) { + // unsuccess: + success := 0 + cb := 0 + } + } + } + default { + // unsuccess: + success := 0 + } + } + /* solhint-disable no-inline-assembly */ + + return success; + } +} From 60a43314d9597de9d078c2e9ee61d579e851fe1f Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Tue, 11 Mar 2025 17:25:39 +0200 Subject: [PATCH 3/8] forge install: nitro --- .gitmodules | 3 +++ nitro_rpc/lib/nitro | 1 + 2 files changed, 4 insertions(+) create mode 160000 nitro_rpc/lib/nitro diff --git a/.gitmodules b/.gitmodules index 28b42f5..d37a269 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "nitro_rpc/lib/exit-format"] path = nitro_rpc/lib/exit-format url = https://github.com/statechannels/exit-format +[submodule "nitro_rpc/lib/nitro"] + path = nitro_rpc/lib/nitro + url = https://github.com/erc7824/nitro diff --git a/nitro_rpc/lib/nitro b/nitro_rpc/lib/nitro new file mode 160000 index 0000000..cc2d456 --- /dev/null +++ b/nitro_rpc/lib/nitro @@ -0,0 +1 @@ +Subproject commit cc2d4564d88cf4289ca8f7b1dd23dbbae593f7e0 From 5020a315050f9a6348ca0eec7031a31e4a70d058 Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Tue, 11 Mar 2025 17:27:09 +0200 Subject: [PATCH 4/8] fix imports --- nitro_rpc/src/NitroRPC.sol | 4 +- nitro_rpc/src/TempINitroTypes.sol | 36 ----- nitro_rpc/src/TempNitroUtils.sol | 225 ------------------------------ 3 files changed, 2 insertions(+), 263 deletions(-) delete mode 100644 nitro_rpc/src/TempINitroTypes.sol delete mode 100644 nitro_rpc/src/TempNitroUtils.sol diff --git a/nitro_rpc/src/NitroRPC.sol b/nitro_rpc/src/NitroRPC.sol index 68b256b..ec04d46 100644 --- a/nitro_rpc/src/NitroRPC.sol +++ b/nitro_rpc/src/NitroRPC.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.17; -import {INitroTypes} from "./TempINitroTypes.sol"; -import {NitroUtils} from "./TempNitroUtils.sol"; +import {INitroTypes} from "../lib/nitro/src/interfaces/INitroTypes.sol"; +import {NitroUtils} from "../lib/nitro/src/libraries/NitroUtils.sol"; /** * @title NitroRPC diff --git a/nitro_rpc/src/TempINitroTypes.sol b/nitro_rpc/src/TempINitroTypes.sol deleted file mode 100644 index 1f6b222..0000000 --- a/nitro_rpc/src/TempINitroTypes.sol +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {ExitFormat as Outcome} from "../lib/exit-format/contracts/ExitFormat.sol"; - -interface INitroTypes { - struct Signature { - uint8 v; - bytes32 r; - bytes32 s; - } - - struct FixedPart { - address[] participants; - uint64 channelNonce; - address appDefinition; - uint48 challengeDuration; - } - - struct VariablePart { - Outcome.SingleAssetExit[] outcome; - bytes appData; - uint48 turnNum; - bool isFinal; - } - - struct SignedVariablePart { - VariablePart variablePart; - Signature[] sigs; - } - - struct RecoveredVariablePart { - VariablePart variablePart; - uint256 signedBy; // bitmask - } -} diff --git a/nitro_rpc/src/TempNitroUtils.sol b/nitro_rpc/src/TempNitroUtils.sol deleted file mode 100644 index 54185ed..0000000 --- a/nitro_rpc/src/TempNitroUtils.sol +++ /dev/null @@ -1,225 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {INitroTypes} from "./TempINitroTypes.sol"; -import {ExitFormat as Outcome} from "../lib/exit-format/contracts/ExitFormat.sol"; - -library NitroUtils { - // ***************** - // Signature methods: - // ***************** - - /** - * @notice Require supplied stateHash is signed by signer. - * @dev Require supplied stateHash is signed by signer. - * @param stateHash State hash to check. - * @param sig Signed state signature. - * @param signer Address which must have signed the state. - * @return true if signer with sig has signed stateHash. - */ - function isSignedBy(bytes32 stateHash, INitroTypes.Signature memory sig, address signer) - internal - pure - returns (bool) - { - return signer == NitroUtils.recoverSigner(stateHash, sig); - } - - /** - * @notice Check if supplied participantIndex bit is set to 1 in signedBy bit mask. - * @dev Check if supplied partitipationIndex bit is set to 1 in signedBy bit mask. - * @param signedBy Bit mask field to check. - * @param participantIndex Bit to check. - * @return true if supplied partitipationIndex bit is set to 1 in signedBy bit mask. - */ - function isClaimedSignedBy(uint256 signedBy, uint8 participantIndex) internal pure returns (bool) { - return ((signedBy >> participantIndex) % 2 == 1); - } - - /** - * @notice Check if supplied participantIndex is the only bit set to 1 in signedBy bit mask. - * @dev Check if supplied participantIndex is the only bit set to 1 in signedBy bit mask. - * @param signedBy Bit mask field to check. - * @param participantIndex Bit to check. - * @return true if supplied partitipationIndex bit is the only bit set to 1 in signedBy bit mask. - */ - function isClaimedSignedOnlyBy(uint256 signedBy, uint8 participantIndex) internal pure returns (bool) { - return (signedBy == (2 ** participantIndex)); - } - - /** - * @notice Given a digest and ethereum digital signature, recover the signer. - * @dev Given a digest and digital signature, recover the signer. - * @param _d message digest. - * @param sig ethereum digital signature. - * @return signer - */ - function recoverSigner(bytes32 _d, INitroTypes.Signature memory sig) internal pure returns (address) { - bytes32 prefixedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", _d)); - address a = ecrecover(prefixedHash, sig.v, sig.r, sig.s); - require(a != address(0), "Invalid signature"); - return (a); - } - - /** - * @notice Count number of bits set to '1', specifying the number of participants which have signed the state. - * @dev Count number of bits set to '1', specifying the number of participants which have signed the state. - * @param signedBy Bit mask field specifying which participants have signed the state. - * @return amount of signers, which have signed the state. - */ - function getClaimedSignersNum(uint256 signedBy) internal pure returns (uint8) { - uint8 amount = 0; - - for (; signedBy > 0; amount++) { - // for reference: Kernighan's Bit Counting Algorithm - signedBy &= signedBy - 1; - } - - return amount; - } - - /** - * @notice Determine indices of participants who have signed the state. - * @dev Determine indices of participants who have signed the state. - * @param signedBy Bit mask field specifying which participants have signed the state. - * @return signerIndices - */ - function getClaimedSignersIndices(uint256 signedBy) internal pure returns (uint8[] memory) { - uint8[] memory signerIndices = new uint8[](getClaimedSignersNum(signedBy)); - uint8 signerNum = 0; - uint8 acceptedSigners = 0; - - for (; signedBy > 0; signerNum++) { - if (signedBy % 2 == 1) { - signerIndices[acceptedSigners] = signerNum; - acceptedSigners++; - } - signedBy >>= 1; - } - - return signerIndices; - } - - // ***************** - // ID methods: - // ***************** - - /** - * @notice Computes the unique id of a channel. - * @dev Computes the unique id of a channel. - * @param fixedPart Part of the state that does not change - * @return channelId - */ - function getChannelId(INitroTypes.FixedPart memory fixedPart) internal pure returns (bytes32 channelId) { - channelId = keccak256( - abi.encode( - fixedPart.participants, fixedPart.channelNonce, fixedPart.appDefinition, fixedPart.challengeDuration - ) - ); - } - - // ***************** - // Hash methods: - // ***************** - - /** - * @notice Computes the hash of the state corresponding to the input data. - * @dev Computes the hash of the state corresponding to the input data. - * @param turnNum Turn number - * @param isFinal Is the state final? - * @param channelId Unique identifier for the channel - * @param appData Application specific data. - * @param outcome Outcome structure. - * @return The stateHash - */ - function hashState( - bytes32 channelId, - bytes memory appData, - Outcome.SingleAssetExit[] memory outcome, - uint48 turnNum, - bool isFinal - ) internal pure returns (bytes32) { - return keccak256(abi.encode(channelId, appData, outcome, turnNum, isFinal)); - } - - /** - * @notice Computes the hash of the state corresponding to the input data. - * @dev Computes the hash of the state corresponding to the input data. - * @param fp The FixedPart of the state - * @param vp The VariablePart of the state - * @return The stateHash - */ - function hashState(INitroTypes.FixedPart memory fp, INitroTypes.VariablePart memory vp) - internal - pure - returns (bytes32) - { - return keccak256(abi.encode(getChannelId(fp), vp.appData, vp.outcome, vp.turnNum, vp.isFinal)); - } - - /** - * @notice Hashes the outcome structure. Internal helper. - * @dev Hashes the outcome structure. Internal helper. - * @param outcome Outcome structure to encode hash. - * @return bytes32 Hash of encoded outcome structure. - */ - function hashOutcome(Outcome.SingleAssetExit[] memory outcome) internal pure returns (bytes32) { - return keccak256(Outcome.encodeExit(outcome)); - } - - // ***************** - // Equality methods: - // ***************** - - /** - * @notice Check for equality of two byte strings - * @dev Check for equality of two byte strings - * @param _preBytes One bytes string - * @param _postBytes The other bytes string - * @return true if the bytes are identical, false otherwise. - */ - function bytesEqual(bytes memory _preBytes, bytes memory _postBytes) internal pure returns (bool) { - // copied from https://www.npmjs.com/package/solidity-bytes-utils/v/0.1.1 - bool success = true; - - /* solhint-disable no-inline-assembly */ - assembly { - let length := mload(_preBytes) - - // if lengths don't match the arrays are not equal - switch eq(length, mload(_postBytes)) - case 1 { - // cb is a circuit breaker in the for loop since there's - // no said feature for inline assembly loops - // cb = 1 - don't breaker - // cb = 0 - break - let cb := 1 - - let mc := add(_preBytes, 0x20) - let end := add(mc, length) - - for { let cc := add(_postBytes, 0x20) } - // the next line is the loop condition: - // while(uint256(mc < end) + cb == 2) - eq(add(lt(mc, end), cb), 2) { - mc := add(mc, 0x20) - cc := add(cc, 0x20) - } { - // if any of these checks fails then arrays are not equal - if iszero(eq(mload(mc), mload(cc))) { - // unsuccess: - success := 0 - cb := 0 - } - } - } - default { - // unsuccess: - success := 0 - } - } - /* solhint-disable no-inline-assembly */ - - return success; - } -} From baae39407d2ed1b2a45ba594916c1b40f7a1edcb Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Tue, 11 Mar 2025 18:08:34 +0200 Subject: [PATCH 5/8] fix contract build --- .vscode/settings.json | 5 +++++ nitro_rpc/src/NitroRPC.sol | 27 ++++++++++++++------------- 2 files changed, 19 insertions(+), 13 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..57a1156 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "solidity.packageDefaultDependenciesContractsDirectory": "src", + "solidity.packageDefaultDependenciesDirectory": "lib" +} + \ No newline at end of file diff --git a/nitro_rpc/src/NitroRPC.sol b/nitro_rpc/src/NitroRPC.sol index ec04d46..6f4d2e3 100644 --- a/nitro_rpc/src/NitroRPC.sol +++ b/nitro_rpc/src/NitroRPC.sol @@ -2,14 +2,15 @@ pragma solidity ^0.8.17; -import {INitroTypes} from "../lib/nitro/src/interfaces/INitroTypes.sol"; -import {NitroUtils} from "../lib/nitro/src/libraries/NitroUtils.sol"; +import {INitroTypes} from "nitro/interfaces/INitroTypes.sol"; +import {NitroUtils} from "nitro/libraries/NitroUtils.sol"; +import {IForceMoveApp} from "nitro/interfaces/IForceMoveApp.sol"; /** * @title NitroRPC * @dev Solidity implementation of Nitro RPC protocol structures */ -interface NitroRPC { +contract NitroRPC is IForceMoveApp { struct Payload { uint256 requestId; uint256 timestamp; @@ -20,8 +21,8 @@ interface NitroRPC { struct PayloadSigned { Payload rpcMessage; - Signature clientSig; - Signature serverSig; + INitroTypes.Signature clientSig; + INitroTypes.Signature serverSig; } enum AllocationIndices { @@ -37,27 +38,27 @@ interface NitroRPC { * @param candidate Recovered variable part the proof was supplied for. */ function stateIsSupported( - FixedPart calldata fixedPart, - RecoveredVariablePart[] calldata proof, - RecoveredVariablePart calldata candidate + INitroTypes.FixedPart calldata fixedPart, + INitroTypes.RecoveredVariablePart[] calldata proof, + INitroTypes.RecoveredVariablePart calldata candidate ) external pure override returns (bool, string memory) { require(fixedPart.participants.length == uint256(AllocationIndices.Server) + 1, "bad number of participants"); PayloadSigned memory payloadSigned = abi.decode(candidate.variablePart.appData, (PayloadSigned)); - requireValidPayload(payloadSigned); + requireValidPayload(fixedPart, payloadSigned); return (true, ""); } - function requireValidPayload(PayloadSigned memory payloadSigned) internal pure { - require(recoverPayloadSigner(payloadSigned.rpcMessage, payloadSigned.clientSig) == fixedPart.participants[AllocationIndices.Client], "bad client signature"); - require(recoverPayloadSigner(payloadSigned.rpcMessage, payloadSigned.serverSig) == fixedPart.participants[AllocationIndices.Server], "bad server signature"); + function requireValidPayload(INitroTypes.FixedPart calldata fixedPart, PayloadSigned memory payloadSigned) internal pure { + require(recoverPayloadSigner(payloadSigned.rpcMessage, payloadSigned.clientSig) == fixedPart.participants[uint256(AllocationIndices.Client)], "bad client signature"); + require(recoverPayloadSigner(payloadSigned.rpcMessage, payloadSigned.serverSig) == fixedPart.participants[uint256(AllocationIndices.Server)], "bad server signature"); // TODO: verify timestamp and requestId } // This pure internal function recovers the signer address from the payload and its signature. - function recoverPayloadSigner(Payload memory payload, Signature memory signature) internal pure returns (address) { + function recoverPayloadSigner(Payload memory payload, INitroTypes.Signature memory signature) internal pure returns (address) { // Encode and hash the payload data. // Using abi.encode ensures proper padding and decoding, avoiding potential ambiguities with dynamic types. bytes32 messageHash = keccak256( From 21b556021a55466ff815a98ea1913e058fd5f83d Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Tue, 18 Mar 2025 15:09:48 +0200 Subject: [PATCH 6/8] feat: nitro-rpc proto and golang draft implementation (#2) * feat: nitro-rpc proto and golang draft implementation * feat: add compatibility tests for signing * forge install: openzeppelin-contracts v5.2.0 * import openzeppelin contracts * fix: packing and signing * add questions * Update README.md * Update README.md * Descriptiong NitroRPC convention * Fixed todolist * Update headers * nitro client api overview --------- Co-authored-by: Louis --- .gitmodules | 3 + README.md | 158 ++- nitro_client_api.md | 186 ++++ nitro_rpc/README.md | 24 + nitro_rpc/go.mod | 45 + nitro_rpc/go.sum | 202 ++++ nitro_rpc/lib/openzeppelin-contracts | 1 + nitro_rpc/proto/nitro_rpc.pb.go | 373 +++++++ {proto => nitro_rpc/proto}/nitro_rpc.proto | 7 +- nitro_rpc/proto/nitro_rpc.twirp.go | 1112 ++++++++++++++++++++ nitro_rpc/proto/tools.go | 8 + nitro_rpc/remappings.txt | 6 + nitro_rpc/src/NitroRPC.sol | 31 +- nitro_rpc/test/NitroRPCTest.t.sol | 64 ++ nitro_rpc/web/server.go | 56 + nitro_rpc/web/server_test.go | 58 + nitro_rpc/web/sign.go | 61 ++ nitro_rpc/web/sign_test.go | 35 + 18 files changed, 2394 insertions(+), 36 deletions(-) create mode 100644 nitro_client_api.md create mode 100644 nitro_rpc/go.mod create mode 100644 nitro_rpc/go.sum create mode 160000 nitro_rpc/lib/openzeppelin-contracts create mode 100644 nitro_rpc/proto/nitro_rpc.pb.go rename {proto => nitro_rpc/proto}/nitro_rpc.proto (91%) create mode 100644 nitro_rpc/proto/nitro_rpc.twirp.go create mode 100644 nitro_rpc/proto/tools.go create mode 100644 nitro_rpc/remappings.txt create mode 100644 nitro_rpc/test/NitroRPCTest.t.sol create mode 100644 nitro_rpc/web/server.go create mode 100644 nitro_rpc/web/server_test.go create mode 100644 nitro_rpc/web/sign.go create mode 100644 nitro_rpc/web/sign_test.go diff --git a/.gitmodules b/.gitmodules index d37a269..614c1c9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "nitro_rpc/lib/nitro"] path = nitro_rpc/lib/nitro url = https://github.com/erc7824/nitro +[submodule "nitro_rpc/lib/openzeppelin-contracts"] + path = nitro_rpc/lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/README.md b/README.md index da3ebca..2e38bf9 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,96 @@ Nitro Protocol Examples ## Nitro RPC -Those application leverage the NitroRPC Asynchronous protocol +Those application leverage the NitroRPC Asynchronous protocol, it describe a data format, that must be understood and readable by both backend, frontend and smart-contract NitroRPCApp (adjudication) + +Here is the format: + +### Solidity +```solidity +struct NitroRPC { + uint64 req_id; // Unique request ID (non-zero) + string method; // RPC method name (e.g., "substract") + string[] params; // Array of parameters for the RPC call + uint64 ts; // Milisecond unix timestamp provided by the server api +} + +NitroRPC memory rpc = NitroRPC({ + req_id: 123, + method: "subtract", + params: new string ts: 1710474422 +}); + +bytes memory encoded1 = ABI.encode(rpc); +bytes memory encoded2 = ABI.encode(rpc.req_id, rpc.method, rpc.params, rpc.ts); + +require(keccak256(encoded1) == keccak256(encoded2), "Mismatch in encoding!"); +``` + +### The RPCHash + +Client and server must sign the Nitro RPC Hash as followed + +### Solidity + +```solidity +rpc_hash = keccak256( + abi.encode( + rpc.req_id, + rpc.method, + rpc.params, + rpc.ts + ) +); + +# rpc_hash can be used to erecover the public key +``` + +### Go lang +```go +package main + +import ( + "fmt" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "log" +) + +type NitroRPC struct { + ReqID uint64 + Method string + Params []string + TS uint64 +} + +func main() { + rpc := NitroRPC{ + ReqID: 123, + Method: "subtract", + Params: []string{"param1", "param2"}, + TS: 1710474422, + } + + packedData, err := abi.Arguments{ + {Type: abi.UintTy}, + {Type: abi.StringTy}, + {Type: abi.StringSliceTy}, + {Type: abi.UintTy}, + }.Pack(rpc.ReqID, rpc.Method, rpc.Params, rpc.TS) + if err != nil { + log.Fatal("ABI encoding failed:", err) + } + + hash := crypto.Keccak256(packedData) + fmt.Println("Keccak256 Hash:", hexutil.Encode(hash)) +} +``` + +## NitroRPC Transport + +In NitroRPC the transport layer is agnostic to describe in any syntax as long as the NitroRPC Type, RPCHash and signature convention are valid. +You can use json-rpc, msgpack, protobug, gRPC or any custom marshalling and transport layer. ### NitroRPC Request @@ -58,13 +147,66 @@ CREATE TABLE rpc_states ( ``` #### TODO: -- Create AppData in solidity -- Create NitroApp to validate states including AppData -- Create Protobuf description of NitroRPC Request / Response -- Include NitroRPC types into gRPC and generate client and server -- Implement methods: `add(int), sub(int), mul(int), mod(int)` starting from `0` -- Create a simplified NitroCalc Service using sqlite for state (in go) -- Finally, we can import nitro pkg, and client or server can pack and sqlite state and call nitro of on-chain channel operations. +- [ ] Create AppData in solidity +- [ ] Create NitroApp to validate states including AppData +- [ ] Create Protobuf description of NitroRPC Request / Response +- [ ] Include NitroRPC types into gRPC and generate client and server +- [ ] Implement methods: `add(int), sub(int), mul(int), mod(int)` starting from `0` +- [ ] Create a simplified NitroCalc Service using sqlite for state (in go) +- [ ] Finally, we can import nitro pkg, and client or server can pack and sqlite state and call nitro of on-chain channel operations. + +### Questions + +#### Where state-management is happening? + + Like the TicTacToe example, the Logic and App State is managed by the Nitro.App, + But also the state is created in compliance by implementing correctly Nitro.App interface See ADR-002 + nitro package responsability is to unpack the state and submit it on-chain. + +#### Communication diagram between BusinessApp, NitroRPC and NitroCore + +NitroRPC is embeded into the BusinessApp and it's only a norm, expect for the smart-contract NitroRPCApp + +nitro go package is providing helpers and functions to abstract the blockchain level things, +it will take your Nitro.App state and execute the blockchain operation you request on the NitroAdjudicator (`prefunding, postfunding, finalize, checkpoint, challenge`) + +#### Who is responsible for the state signing? (One signer on Client, One signer on Server???) + +Client Nitro.App signs requests +Server Nitro.App signs reponses + +Channel service or nitro package does sign for you, the private key is obviously not part of the package. +But nitro pkg will help you sign and simplify all the nitro state creation. + +#### Do we have 2-step communication like? + - Client -> Server: Request + - Server -> Client: Response (Server signs?) + - Client -> Server: Acknowledge (Client signs?) + - Server -> Client: Acknowledge + +I would say 1-step is Request-Response pair. + +- Request is signed by client +- Response is signed by server + +anything else is an invalidate state (request signed without response, signature missing) + +#### Do we implement nitro specific methods (open, checkpoint, dispute, finalize) in the NitroRPC service? + +You only need to implement the transport, for example json-rpc handlers, or grpc, those specific method will be standardized +nitro pkg will provide the implementation of those methods, you just need to provide the correct prefunding postfunding state in nitro.App + +A nitro.App is a state container, which can hold 1 or many state indexed by TurnNum, serialized and passed to nitro pkg/svc for execution. + +#### Does NitroRPC server have BusinessApp logic? + +NitroRPC is just a convention, the Application has the business logic and implement an RPC protocol which comply with the state convention + +#### Does NitroRPC server stores rpc states? + +It's high recommended, in the event of answering to a challenge, but most of the time you need only the recent state, +but like I provided an SQL table, Application should keep track of state in some degree, it could be in memory and in a custom format +as long as it's able to form an RPCHash again. #### Markdown Table Example diff --git a/nitro_client_api.md b/nitro_client_api.md new file mode 100644 index 0000000..c36bf5e --- /dev/null +++ b/nitro_client_api.md @@ -0,0 +1,186 @@ +# Higher-Level NitroClient API + +## 1. Overview of the Solution + +The NitroCore package provides all low‑level functionality for state channels—including state construction, signing, and on‑chain enforcement—but its interface is complex. To make integration easier for most developers, we propose an additional abstraction layer—NitroClient—which exposes a high‑level HTTP (or JSON‑RPC) API. This API would handle the internal state management and on‑chain interactions while exposing simple, business‑oriented methods. The client (user) interacts only via high‑level commands (for example, openChannel, makeMove, and finalize) while the NitroClient internally constructs and signs state updates based on the NitroCore functions. + +A key requirement is that the client’s signature must be made over the state data (i.e. the state hash), ensuring that the state remains secure and verifiable. This is achieved by leveraging functions like `(c *Channel) SignAndAddState(s State, ls signer.Signer) (SignedState, error)` in NitroCore. + +--- + +## 2. High-Level API Design + +### Key Design Decisions + +- **Abstraction of Low-Level Details:** + Users are not required to manually manage state objects, turn numbers, or cryptographic signing. Instead, the API exposes simple endpoints that encapsulate these details. + +- **Language-Agnostic Communication:** + Exposing an HTTP or JSON‑RPC interface enables clients written in any language to interact with the service. + +- **Pluggable Business Logic:** + The API can support multiple applications (e.g. TicTacToe, betting, calc) by associating each channel with an `app_type` or by using schema‑driven validation. This dispatch layer routes high‑level commands to the correct business‑logic module. + +- **Internal Security and Signing:** + While the API abstracts state management, it still enforces that every state update is signed over its full hash. The client must provide a signature on the computed state hash, which is then verified and merged with the server’s signature to produce a full support proof. + +### Example Endpoints + +1. **POST /channel/open** + *Purpose:* Open a new channel. + *Request Example:* + ```json + { + "client_id": "user-123", + "counterparty": "server-abc", + "initial_funds": { "ETH": "1000000000000000000" }, + "client_signature": "0xabc..." + } + ``` + *Response Example:* + ```json + { + "channel_id": "0xdeadbeef...", + "state_hash": "0x1234abcd...", + "server_signature": "0xdef..." + } + ``` + +2. **POST /channel/move** + *Purpose:* Submit a state update (e.g. a move in a game). + *Request Example:* + ```json + { + "channel_id": "0xdeadbeef...", + "action": "move", + "params": { "x": 1, "y": 2 }, + "client_signature": "0xabc..." + } + ``` + *Response Example:* + ```json + { + "new_state_hash": "0x5678efgh...", + "server_signature": "0xdef..." + } + ``` + +3. **POST /channel/finalize** + *Purpose:* Finalize and close the channel. + *Request Example:* + ```json + { + "channel_id": "0xdeadbeef...", + "client_signature": "0xabc..." + } + ``` + *Response Example:* + ```json + { + "final_state_hash": "0x9abc...", + "server_signature": "0xdef..." + } + ``` + +4. **GET /channel/state/{channel_id}** + *Purpose:* Retrieve the current channel state (for debugging or verification). + +--- + +## 3. Communication Flow Examples + +### Client ↔ NitroClient API + +- **Opening a Channel:** + 1. The client sends a POST request to `/channel/open` with parameters such as client ID, counterparty info, and initial funds along with a client‑generated signature. + 2. The API creates an initial Nitro state using NitroCore, verifies the client’s signature (which is over the state hash), and then signs the state itself. + 3. The API returns a channel ID, state hash, and server signature. + +- **Making a Move (State Update):** + 1. The client sends a POST request to `/channel/move` with an `"action": "move"`, relevant parameters (for example, `{ "x": 1, "y": 2 }`), and a client signature. + 2. The server fetches the current channel state, dispatches the request to the appropriate business logic module (e.g. TicTacToe handler), and applies the move. + 3. The module computes the new state (incrementing turn number and updating app data) and calculates its hash. + 4. The server verifies the client’s signature over the new state hash, signs the state update using NitroCore’s `SignAndAddState`, and returns the new state hash and its own signature. + +- **Finalizing a Channel:** + 1. When ready to close the channel, the client sends a POST request to `/channel/finalize` with the channel ID and its signature. + 2. The server confirms that the state is final (or constructs a final state), signs it, and triggers the on‑chain finalization process via NitroCore. + 3. The response includes the final state hash and server signature. + +### Internal (NitroClient API ↔ NitroCore) + +- **State Construction and Signing:** + The API layer calls NitroCore functions such as: + ```go + currentState := channel.LatestSignedState().State + newState := currentState.Clone() + // Business logic: update newState.AppData, newState.Outcome, etc. + newState.TurnNum++ + // Verify client signature over newState.Hash() + clientSignedState, err := channel.SignAndAddState(newState, clientSigner) + // Server then signs the state update: + serverSig, _ := newState.Sign(serverSigner) + channel.AddStateWithSignature(newState, serverSig) + ``` + This ensures that the new state carries both the client’s and the server’s signatures, binding the state data cryptographically. + +--- + +## 4. Pros and Cons of the Proposed API + +### Pros + +- **User-Friendly:** + - Simplifies interaction by exposing high-level endpoints instead of requiring developers to manage Nitro states manually. + - Provides a consistent, language-agnostic interface (via HTTP/JSON‑RPC) that can be used by web, mobile, or desktop applications. + +- **Separation of Concerns:** + - NitroCore continues to focus on cryptographic signing, state validation, and on‑chain operations. + - NitroClient handles business logic, state dispatch, and coordination without exposing low‑level details. + +- **Centralized State Management & Auditing:** + - All state transitions are logged and stored centrally, which can simplify debugging and dispute resolution. + - The server can manage the entire channel lifecycle—from channel opening to finalization—ensuring consistency. + +- **Simplified On‑Chain Operations:** + - The API abstracts away gas management, transaction signing, and blockchain submission. + - Automated aggregation of client and server signatures ensures that every state update is secure and enforceable on‑chain. + +- **Flexibility Across Applications:** + - A pluggable or schema‑driven dispatch system allows the API to support different business logic modules (TicTacToe, betting, calculator, etc.) under a unified interface. + +### Cons + +- **Centralization and Trust Concerns:** + - The server becomes a single point of control; clients must trust that the server processes valid requests and does not act maliciously. + - Clients have limited direct access to raw states, reducing their ability to independently audit and challenge state transitions. + +- **Reduced Transparency:** + - Because the Nitro state is managed internally by the API, clients may have fewer tools to verify the underlying state if a dispute arises. + - Reliance on the server for state history and proof can be problematic in adversarial scenarios. + +- **Single Point of Failure and Scalability:** + - The centralized API must be highly available and secure; downtime or a breach could affect all channel operations. + - Scaling the API to handle many simultaneous channels and state updates might require significant infrastructure investment. + +- **Complexity in Generic Handling:** + - Designing a truly generic endpoint (e.g., `/channel/move`) requires a dispatch layer to route requests to the appropriate business logic. + - Managing dynamic schemas or multiple endpoints for different applications increases the overall complexity of the API. + +- **Security Risks in Key Management:** + - The server’s private keys must be managed with extreme care. A compromise of these keys could jeopardize the entire system. + - The verification logic must correctly enforce that client signatures are over the accurate state hash. + +- **Potential Latency:** + - The extra network hops (client → API → NitroCore → blockchain) could introduce delays, which may impact time-sensitive applications. + +--- + +## 5. Conclusion + +The proposed NitroClient API offers a significant usability improvement by abstracting the complex NitroCore state management into a simple, high-level HTTP interface. Users benefit from an intuitive, business-focused API while the heavy lifting—state creation, signing, and on‑chain interactions—is managed internally. + +However, this additional layer introduces trade‑offs in terms of centralization, trust, and potential complexity in handling different business logics. Security concerns regarding key management and state transparency must be addressed, and provisions such as audit modes or state proof access may help mitigate these risks. + +Overall, the abstraction layer is a promising solution to lower the barrier for developers while preserving the core security and functionality of the Nitro protocol, as long as careful attention is paid to balancing usability with trust and decentralization. + diff --git a/nitro_rpc/README.md b/nitro_rpc/README.md index 9265b45..1c00537 100644 --- a/nitro_rpc/README.md +++ b/nitro_rpc/README.md @@ -1,3 +1,27 @@ +# Nitro RPC + +## Protobuf & Twirp Setup + +This guide walks you through setting up `protoc`, adding the required plugins, and configuring your development environment to work seamlessly with Twirp and protobuf. +Twirp uses your protobuf service definitions to generate an HTTP/1.1 API. It’s lightweight and simpler than gRPC, which can be a good fit if you prefer HTTP/1.1 over HTTP/2. + +### Prerequisites + +Before generating the Go files from your protobuf definitions, ensure that you have installed the Protocol Buffers compiler (`protoc`) and the necessary Go plugins for Twirp and protobuf generation. Detailed installation instructions are available in the [Twirp Installation Documentation](https://twitchtv.github.io/twirp/docs/install.html). + +### Usage + +All protobuf definitions are maintained inside the `proto/` directory. You can generate the corresponding Go files using the following command: + +```bash +protoc -I proto \ + --go_out=proto --go_opt=paths=source_relative \ + --twirp_out=proto --twirp_opt=paths=source_relative \ + proto/nitro_rpc.proto +``` + +This command will create both the `.pb.go` and `.twirp.go` files in the `proto/` directory, keeping your service definitions and generated code neatly organized. + ## Foundry **Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** diff --git a/nitro_rpc/go.mod b/nitro_rpc/go.mod new file mode 100644 index 0000000..2f40381 --- /dev/null +++ b/nitro_rpc/go.mod @@ -0,0 +1,45 @@ +module github.com/erc7824/examples/nitro_rpc + +go 1.23.5 + +require ( + github.com/ethereum/go-ethereum v1.13.11 + github.com/layer-3/clearsync v0.0.129 + github.com/stretchr/testify v1.10.0 + github.com/twitchtv/twirp v8.1.3+incompatible + google.golang.org/protobuf v1.36.5 +) + +require ( + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/StackExchange/wmi v1.2.1 // indirect + github.com/bits-and-blooms/bitset v1.10.0 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect + github.com/consensys/bavard v0.1.13 // indirect + github.com/consensys/gnark-crypto v0.12.1 // indirect + github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/deckarep/golang-set/v2 v2.1.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/ethereum/c-kzg-4844 v0.4.0 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/holiman/uint256 v1.2.4 // indirect + github.com/mmcloughlin/addchain v0.4.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect + github.com/supranational/blst v0.3.11 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/sync v0.5.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/tools v0.15.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + rsc.io/tmplfunc v0.0.3 // indirect +) diff --git a/nitro_rpc/go.sum b/nitro_rpc/go.sum new file mode 100644 index 0000000..5faeba2 --- /dev/null +++ b/nitro_rpc/go.sum @@ -0,0 +1,202 @@ +github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= +github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/VictoriaMetrics/fastcache v1.12.1 h1:i0mICQuojGDL3KblA7wUNlY5lOK6a4bwt3uRKnkZU40= +github.com/VictoriaMetrics/fastcache v1.12.1/go.mod h1:tX04vaqcNoQeGLD+ra5pU5sWkuxnzWhEzLwhP9w653o= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.10.0 h1:ePXTeiPEazB5+opbv5fr8umg2R/1NlzgDsyepwsSr88= +github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/btcsuite/btcd v0.22.1 h1:CnwP9LM/M9xuRrGSCGeMVs9iv09uMqwsVX7EeIpgV2c= +github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= +github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= +github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/errors v1.9.1 h1:yFVvsI0VxmRShfawbt/laCIDy/mtTqqnvoNgiy5bEV8= +github.com/cockroachdb/errors v1.9.1/go.mod h1:2sxOtL2WIc096WSZqZ5h8fa17rdDq9HZOZLBCor4mBk= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v0.0.0-20230928194634-aa077af62593 h1:aPEJyR4rPBvDmeyi+l/FS/VtA00IWvjeFvjen1m1l1A= +github.com/cockroachdb/pebble v0.0.0-20230928194634-aa077af62593/go.mod h1:6hk1eMY/u5t+Cf18q5lFMUA1Rc+Sm5I6Ra1QuPyxXCo= +github.com/cockroachdb/redact v1.1.3 h1:AKZds10rFSIj7qADf0g46UixK8NNLwWTNdCIGS5wfSQ= +github.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= +github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= +github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= +github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233 h1:d28BXYi+wUpz1KBmiF9bWrjEMacUEREV6MBi2ODnrfQ= +github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs= +github.com/crate-crypto/go-kzg-4844 v0.7.0 h1:C0vgZRk4q4EZ/JgPfzuSoxdCq3C3mOZMBShovmncxvA= +github.com/crate-crypto/go-kzg-4844 v0.7.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.1.0 h1:g47V4Or+DUdzbs8FxCCmgb6VYd+ptPAngjM6dtGktsI= +github.com/deckarep/golang-set/v2 v2.1.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/ethereum/c-kzg-4844 v0.4.0 h1:3MS1s4JtA868KpJxroZoepdV0ZKBp3u/O5HcZ7R3nlY= +github.com/ethereum/c-kzg-4844 v0.4.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= +github.com/ethereum/go-ethereum v1.13.11 h1:b51Dsm+rEg7anFRUMGB8hODXHvNfcRKzz9vcj8wSdUs= +github.com/ethereum/go-ethereum v1.13.11/go.mod h1:gFtlVORuUcT+UUIcJ/veCNjkuOSujCi338uSHJrYAew= +github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c= +github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 h1:BAIP2GihuqhwdILrV+7GJel5lyPV3u1+PgzrWLc0TkE= +github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46/go.mod h1:QNpY22eby74jVhqH4WhDLDwxc/vqsern6pW+u2kbkpc= +github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0= +github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/holiman/billy v0.0.0-20230718173358-1c7e68d277a7 h1:3JQNjnMRil1yD0IfZKHF9GxxWKDJGj8I0IqOUol//sw= +github.com/holiman/billy v0.0.0-20230718173358-1c7e68d277a7/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU= +github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/layer-3/clearsync v0.0.129 h1:lB/KrxdHvWoG/0QJSwYXf4vGAgG4Z7NcYkrbpnCiIow= +github.com/layer-3/clearsync v0.0.129/go.mod h1:s/bT4t3kAK+ZrRtUclhY13kpyurOF71/lCdQ5BOwNBI= +github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= +github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= +github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= +github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= +github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= +github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU= +github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A= +github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= +github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= +golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= +rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= diff --git a/nitro_rpc/lib/openzeppelin-contracts b/nitro_rpc/lib/openzeppelin-contracts new file mode 160000 index 0000000..acd4ff7 --- /dev/null +++ b/nitro_rpc/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit acd4ff74de833399287ed6b31b4debf6b2b35527 diff --git a/nitro_rpc/proto/nitro_rpc.pb.go b/nitro_rpc/proto/nitro_rpc.pb.go new file mode 100644 index 0000000..a17fae6 --- /dev/null +++ b/nitro_rpc/proto/nitro_rpc.pb.go @@ -0,0 +1,373 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.5 +// protoc v5.29.3 +// source: nitro_rpc.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Request represents a signed RPC request message +type Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The RPC request payload + Req *RequestPayload `protobuf:"bytes,1,opt,name=req,proto3" json:"req,omitempty"` + // Signature of the payload by the client + Sig string `protobuf:"bytes,2,opt,name=sig,proto3" json:"sig,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Request) Reset() { + *x = Request{} + mi := &file_nitro_rpc_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Request) ProtoMessage() {} + +func (x *Request) ProtoReflect() protoreflect.Message { + mi := &file_nitro_rpc_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Request.ProtoReflect.Descriptor instead. +func (*Request) Descriptor() ([]byte, []int) { + return file_nitro_rpc_proto_rawDescGZIP(), []int{0} +} + +func (x *Request) GetReq() *RequestPayload { + if x != nil { + return x.Req + } + return nil +} + +func (x *Request) GetSig() string { + if x != nil { + return x.Sig + } + return "" +} + +// RequestPayload contains the actual RPC request data +type RequestPayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Unique request identifier + RequestId uint64 `protobuf:"varint,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + // Method name to invoke + Method string `protobuf:"bytes,2,opt,name=method,proto3" json:"method,omitempty"` + // Parameters for the method (encoded as necessary) + Params []byte `protobuf:"bytes,3,opt,name=params,proto3" json:"params,omitempty"` + // Timestamp in milliseconds (previously returned from server) + Timestamp uint64 `protobuf:"varint,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RequestPayload) Reset() { + *x = RequestPayload{} + mi := &file_nitro_rpc_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RequestPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RequestPayload) ProtoMessage() {} + +func (x *RequestPayload) ProtoReflect() protoreflect.Message { + mi := &file_nitro_rpc_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RequestPayload.ProtoReflect.Descriptor instead. +func (*RequestPayload) Descriptor() ([]byte, []int) { + return file_nitro_rpc_proto_rawDescGZIP(), []int{1} +} + +func (x *RequestPayload) GetRequestId() uint64 { + if x != nil { + return x.RequestId + } + return 0 +} + +func (x *RequestPayload) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *RequestPayload) GetParams() []byte { + if x != nil { + return x.Params + } + return nil +} + +func (x *RequestPayload) GetTimestamp() uint64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +// Response represents a signed RPC response message +type Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The RPC response payload + Res *ResponsePayload `protobuf:"bytes,1,opt,name=res,proto3" json:"res,omitempty"` + // Signature of the payload by the server + Sig string `protobuf:"bytes,2,opt,name=sig,proto3" json:"sig,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Response) Reset() { + *x = Response{} + mi := &file_nitro_rpc_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Response) ProtoMessage() {} + +func (x *Response) ProtoReflect() protoreflect.Message { + mi := &file_nitro_rpc_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Response.ProtoReflect.Descriptor instead. +func (*Response) Descriptor() ([]byte, []int) { + return file_nitro_rpc_proto_rawDescGZIP(), []int{2} +} + +func (x *Response) GetRes() *ResponsePayload { + if x != nil { + return x.Res + } + return nil +} + +func (x *Response) GetSig() string { + if x != nil { + return x.Sig + } + return "" +} + +// ResponsePayload contains the actual RPC response data +type ResponsePayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Matching request identifier from the request + RequestId uint64 `protobuf:"varint,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + // Method name that was invoked + Method string `protobuf:"bytes,2,opt,name=method,proto3" json:"method,omitempty"` + // Return values from the method (encoded as necessary) + Results []byte `protobuf:"bytes,3,opt,name=results,proto3" json:"results,omitempty"` + // Latest server timestamp in milliseconds + Timestamp uint64 `protobuf:"varint,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResponsePayload) Reset() { + *x = ResponsePayload{} + mi := &file_nitro_rpc_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResponsePayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResponsePayload) ProtoMessage() {} + +func (x *ResponsePayload) ProtoReflect() protoreflect.Message { + mi := &file_nitro_rpc_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResponsePayload.ProtoReflect.Descriptor instead. +func (*ResponsePayload) Descriptor() ([]byte, []int) { + return file_nitro_rpc_proto_rawDescGZIP(), []int{3} +} + +func (x *ResponsePayload) GetRequestId() uint64 { + if x != nil { + return x.RequestId + } + return 0 +} + +func (x *ResponsePayload) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *ResponsePayload) GetResults() []byte { + if x != nil { + return x.Results + } + return nil +} + +func (x *ResponsePayload) GetTimestamp() uint64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +var File_nitro_rpc_proto protoreflect.FileDescriptor + +var file_nitro_rpc_proto_rawDesc = string([]byte{ + 0x0a, 0x0f, 0x6e, 0x69, 0x74, 0x72, 0x6f, 0x5f, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x09, 0x6e, 0x69, 0x74, 0x72, 0x6f, 0x5f, 0x72, 0x70, 0x63, 0x22, 0x48, 0x0a, 0x07, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x03, 0x72, 0x65, 0x71, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6e, 0x69, 0x74, 0x72, 0x6f, 0x5f, 0x72, 0x70, 0x63, + 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x52, + 0x03, 0x72, 0x65, 0x71, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x73, 0x69, 0x67, 0x22, 0x7d, 0x0a, 0x0e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x72, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, + 0x16, 0x0a, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x4a, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x2c, 0x0a, 0x03, 0x72, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x6e, 0x69, 0x74, 0x72, 0x6f, 0x5f, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x03, 0x72, 0x65, 0x73, 0x12, + 0x10, 0x0a, 0x03, 0x73, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x73, 0x69, + 0x67, 0x22, 0x80, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x50, 0x61, + 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x18, 0x0a, 0x07, + 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x72, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x32, 0x3d, 0x0a, 0x08, 0x4e, 0x69, 0x74, 0x72, 0x6f, 0x52, 0x50, 0x43, + 0x12, 0x31, 0x0a, 0x04, 0x43, 0x61, 0x6c, 0x6c, 0x12, 0x12, 0x2e, 0x6e, 0x69, 0x74, 0x72, 0x6f, + 0x5f, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x6e, + 0x69, 0x74, 0x72, 0x6f, 0x5f, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x42, 0x33, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x65, 0x72, 0x63, 0x37, 0x38, 0x32, 0x34, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, + 0x65, 0x73, 0x2f, 0x6e, 0x69, 0x74, 0x72, 0x6f, 0x5f, 0x72, 0x70, 0x63, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x3b, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +}) + +var ( + file_nitro_rpc_proto_rawDescOnce sync.Once + file_nitro_rpc_proto_rawDescData []byte +) + +func file_nitro_rpc_proto_rawDescGZIP() []byte { + file_nitro_rpc_proto_rawDescOnce.Do(func() { + file_nitro_rpc_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_nitro_rpc_proto_rawDesc), len(file_nitro_rpc_proto_rawDesc))) + }) + return file_nitro_rpc_proto_rawDescData +} + +var file_nitro_rpc_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_nitro_rpc_proto_goTypes = []any{ + (*Request)(nil), // 0: nitro_rpc.Request + (*RequestPayload)(nil), // 1: nitro_rpc.RequestPayload + (*Response)(nil), // 2: nitro_rpc.Response + (*ResponsePayload)(nil), // 3: nitro_rpc.ResponsePayload +} +var file_nitro_rpc_proto_depIdxs = []int32{ + 1, // 0: nitro_rpc.Request.req:type_name -> nitro_rpc.RequestPayload + 3, // 1: nitro_rpc.Response.res:type_name -> nitro_rpc.ResponsePayload + 0, // 2: nitro_rpc.NitroRPC.Call:input_type -> nitro_rpc.Request + 2, // 3: nitro_rpc.NitroRPC.Call:output_type -> nitro_rpc.Response + 3, // [3:4] is the sub-list for method output_type + 2, // [2:3] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_nitro_rpc_proto_init() } +func file_nitro_rpc_proto_init() { + if File_nitro_rpc_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_nitro_rpc_proto_rawDesc), len(file_nitro_rpc_proto_rawDesc)), + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_nitro_rpc_proto_goTypes, + DependencyIndexes: file_nitro_rpc_proto_depIdxs, + MessageInfos: file_nitro_rpc_proto_msgTypes, + }.Build() + File_nitro_rpc_proto = out.File + file_nitro_rpc_proto_goTypes = nil + file_nitro_rpc_proto_depIdxs = nil +} diff --git a/proto/nitro_rpc.proto b/nitro_rpc/proto/nitro_rpc.proto similarity index 91% rename from proto/nitro_rpc.proto rename to nitro_rpc/proto/nitro_rpc.proto index 677ba04..18497cc 100644 --- a/proto/nitro_rpc.proto +++ b/nitro_rpc/proto/nitro_rpc.proto @@ -1,8 +1,7 @@ syntax = "proto3"; package nitro_rpc; - -option go_package = "github.com/erc7824/nitro-rpc"; +option go_package = "github.com/erc7824/examples/nitro_rpc/proto;proto"; // NitroRPC defines the protocol for asynchronous RPC communication with signatures service NitroRPC { @@ -28,7 +27,7 @@ message RequestPayload { string method = 2; // Parameters for the method (encoded as necessary) - repeated bytes params = 3; + bytes params = 3; // Timestamp in milliseconds (previously returned from server) uint64 timestamp = 4; @@ -52,7 +51,7 @@ message ResponsePayload { string method = 2; // Return values from the method (encoded as necessary) - repeated bytes results = 3; + bytes results = 3; // Latest server timestamp in milliseconds uint64 timestamp = 4; diff --git a/nitro_rpc/proto/nitro_rpc.twirp.go b/nitro_rpc/proto/nitro_rpc.twirp.go new file mode 100644 index 0000000..ca63e6c --- /dev/null +++ b/nitro_rpc/proto/nitro_rpc.twirp.go @@ -0,0 +1,1112 @@ +// Code generated by protoc-gen-twirp v8.1.3, DO NOT EDIT. +// source: nitro_rpc.proto + +package proto + +import context "context" +import fmt "fmt" +import http "net/http" +import io "io" +import json "encoding/json" +import strconv "strconv" +import strings "strings" + +import protojson "google.golang.org/protobuf/encoding/protojson" +import proto "google.golang.org/protobuf/proto" +import twirp "github.com/twitchtv/twirp" +import ctxsetters "github.com/twitchtv/twirp/ctxsetters" + +import bytes "bytes" +import errors "errors" +import path "path" +import url "net/url" + +// Version compatibility assertion. +// If the constant is not defined in the package, that likely means +// the package needs to be updated to work with this generated code. +// See https://twitchtv.github.io/twirp/docs/version_matrix.html +const _ = twirp.TwirpPackageMinVersion_8_1_0 + +// ================== +// NitroRPC Interface +// ================== + +// NitroRPC defines the protocol for asynchronous RPC communication with signatures +type NitroRPC interface { + // Call executes a remote procedure call + Call(context.Context, *Request) (*Response, error) +} + +// ======================== +// NitroRPC Protobuf Client +// ======================== + +type nitroRPCProtobufClient struct { + client HTTPClient + urls [1]string + interceptor twirp.Interceptor + opts twirp.ClientOptions +} + +// NewNitroRPCProtobufClient creates a Protobuf client that implements the NitroRPC interface. +// It communicates using Protobuf and can be configured with a custom HTTPClient. +func NewNitroRPCProtobufClient(baseURL string, client HTTPClient, opts ...twirp.ClientOption) NitroRPC { + if c, ok := client.(*http.Client); ok { + client = withoutRedirects(c) + } + + clientOpts := twirp.ClientOptions{} + for _, o := range opts { + o(&clientOpts) + } + + // Using ReadOpt allows backwards and forwards compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + + // Build method URLs: []/./ + serviceURL := sanitizeBaseURL(baseURL) + serviceURL += baseServicePath(pathPrefix, "nitro_rpc", "NitroRPC") + urls := [1]string{ + serviceURL + "Call", + } + + return &nitroRPCProtobufClient{ + client: client, + urls: urls, + interceptor: twirp.ChainInterceptors(clientOpts.Interceptors...), + opts: clientOpts, + } +} + +func (c *nitroRPCProtobufClient) Call(ctx context.Context, in *Request) (*Response, error) { + ctx = ctxsetters.WithPackageName(ctx, "nitro_rpc") + ctx = ctxsetters.WithServiceName(ctx, "NitroRPC") + ctx = ctxsetters.WithMethodName(ctx, "Call") + caller := c.callCall + if c.interceptor != nil { + caller = func(ctx context.Context, req *Request) (*Response, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*Request) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*Request) when calling interceptor") + } + return c.callCall(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*Response) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*Response) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *nitroRPCProtobufClient) callCall(ctx context.Context, in *Request) (*Response, error) { + out := new(Response) + ctx, err := doProtobufRequest(ctx, c.client, c.opts.Hooks, c.urls[0], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +// ==================== +// NitroRPC JSON Client +// ==================== + +type nitroRPCJSONClient struct { + client HTTPClient + urls [1]string + interceptor twirp.Interceptor + opts twirp.ClientOptions +} + +// NewNitroRPCJSONClient creates a JSON client that implements the NitroRPC interface. +// It communicates using JSON and can be configured with a custom HTTPClient. +func NewNitroRPCJSONClient(baseURL string, client HTTPClient, opts ...twirp.ClientOption) NitroRPC { + if c, ok := client.(*http.Client); ok { + client = withoutRedirects(c) + } + + clientOpts := twirp.ClientOptions{} + for _, o := range opts { + o(&clientOpts) + } + + // Using ReadOpt allows backwards and forwards compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + + // Build method URLs: []/./ + serviceURL := sanitizeBaseURL(baseURL) + serviceURL += baseServicePath(pathPrefix, "nitro_rpc", "NitroRPC") + urls := [1]string{ + serviceURL + "Call", + } + + return &nitroRPCJSONClient{ + client: client, + urls: urls, + interceptor: twirp.ChainInterceptors(clientOpts.Interceptors...), + opts: clientOpts, + } +} + +func (c *nitroRPCJSONClient) Call(ctx context.Context, in *Request) (*Response, error) { + ctx = ctxsetters.WithPackageName(ctx, "nitro_rpc") + ctx = ctxsetters.WithServiceName(ctx, "NitroRPC") + ctx = ctxsetters.WithMethodName(ctx, "Call") + caller := c.callCall + if c.interceptor != nil { + caller = func(ctx context.Context, req *Request) (*Response, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*Request) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*Request) when calling interceptor") + } + return c.callCall(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*Response) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*Response) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *nitroRPCJSONClient) callCall(ctx context.Context, in *Request) (*Response, error) { + out := new(Response) + ctx, err := doJSONRequest(ctx, c.client, c.opts.Hooks, c.urls[0], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +// ======================= +// NitroRPC Server Handler +// ======================= + +type nitroRPCServer struct { + NitroRPC + interceptor twirp.Interceptor + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response + jsonCamelCase bool // JSON fields are serialized as lowerCamelCase rather than keeping the original proto names +} + +// NewNitroRPCServer builds a TwirpServer that can be used as an http.Handler to handle +// HTTP requests that are routed to the right method in the provided svc implementation. +// The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). +func NewNitroRPCServer(svc NitroRPC, opts ...interface{}) TwirpServer { + serverOpts := newServerOpts(opts) + + // Using ReadOpt allows backwards and forwards compatibility with new options in the future + jsonSkipDefaults := false + _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + jsonCamelCase := false + _ = serverOpts.ReadOpt("jsonCamelCase", &jsonCamelCase) + var pathPrefix string + if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + + return &nitroRPCServer{ + NitroRPC: svc, + hooks: serverOpts.Hooks, + interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), + pathPrefix: pathPrefix, + jsonSkipDefaults: jsonSkipDefaults, + jsonCamelCase: jsonCamelCase, + } +} + +// writeError writes an HTTP response with a valid Twirp error format, and triggers hooks. +// If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) +func (s *nitroRPCServer) writeError(ctx context.Context, resp http.ResponseWriter, err error) { + writeError(ctx, resp, err, s.hooks) +} + +// handleRequestBodyError is used to handle error when the twirp server cannot read request +func (s *nitroRPCServer) handleRequestBodyError(ctx context.Context, resp http.ResponseWriter, msg string, err error) { + if context.Canceled == ctx.Err() { + s.writeError(ctx, resp, twirp.NewError(twirp.Canceled, "failed to read request: context canceled")) + return + } + if context.DeadlineExceeded == ctx.Err() { + s.writeError(ctx, resp, twirp.NewError(twirp.DeadlineExceeded, "failed to read request: deadline exceeded")) + return + } + s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err)) +} + +// NitroRPCPathPrefix is a convenience constant that may identify URL paths. +// Should be used with caution, it only matches routes generated by Twirp Go clients, +// with the default "/twirp" prefix and default CamelCase service and method names. +// More info: https://twitchtv.github.io/twirp/docs/routing.html +const NitroRPCPathPrefix = "/twirp/nitro_rpc.NitroRPC/" + +func (s *nitroRPCServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + ctx := req.Context() + ctx = ctxsetters.WithPackageName(ctx, "nitro_rpc") + ctx = ctxsetters.WithServiceName(ctx, "NitroRPC") + ctx = ctxsetters.WithResponseWriter(ctx, resp) + + var err error + ctx, err = callRequestReceived(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + if req.Method != "POST" { + msg := fmt.Sprintf("unsupported method %q (only POST is allowed)", req.Method) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } + + // Verify path format: []/./ + prefix, pkgService, method := parseTwirpPath(req.URL.Path) + if pkgService != "nitro_rpc.NitroRPC" { + msg := fmt.Sprintf("no handler for path %q", req.URL.Path) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } + if prefix != s.pathPrefix { + msg := fmt.Sprintf("invalid path prefix %q, expected %q, on path %q", prefix, s.pathPrefix, req.URL.Path) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } + + switch method { + case "Call": + s.serveCall(ctx, resp, req) + return + default: + msg := fmt.Sprintf("no handler for path %q", req.URL.Path) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } +} + +func (s *nitroRPCServer) serveCall(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + header := req.Header.Get("Content-Type") + i := strings.Index(header, ";") + if i == -1 { + i = len(header) + } + switch strings.TrimSpace(strings.ToLower(header[:i])) { + case "application/json": + s.serveCallJSON(ctx, resp, req) + case "application/protobuf": + s.serveCallProtobuf(ctx, resp, req) + default: + msg := fmt.Sprintf("unexpected Content-Type: %q", req.Header.Get("Content-Type")) + twerr := badRouteError(msg, req.Method, req.URL.Path) + s.writeError(ctx, resp, twerr) + } +} + +func (s *nitroRPCServer) serveCallJSON(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "Call") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + d := json.NewDecoder(req.Body) + rawReqBody := json.RawMessage{} + if err := d.Decode(&rawReqBody); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + reqContent := new(Request) + unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true} + if err = unmarshaler.Unmarshal(rawReqBody, reqContent); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + + handler := s.NitroRPC.Call + if s.interceptor != nil { + handler = func(ctx context.Context, req *Request) (*Response, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*Request) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*Request) when calling interceptor") + } + return s.NitroRPC.Call(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*Response) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*Response) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *Response + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *Response and nil error while calling Call. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + marshaler := &protojson.MarshalOptions{UseProtoNames: !s.jsonCamelCase, EmitUnpopulated: !s.jsonSkipDefaults} + respBytes, err := marshaler.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/json") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *nitroRPCServer) serveCallProtobuf(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "Call") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + buf, err := io.ReadAll(req.Body) + if err != nil { + s.handleRequestBodyError(ctx, resp, "failed to read request body", err) + return + } + reqContent := new(Request) + if err = proto.Unmarshal(buf, reqContent); err != nil { + s.writeError(ctx, resp, malformedRequestError("the protobuf request could not be decoded")) + return + } + + handler := s.NitroRPC.Call + if s.interceptor != nil { + handler = func(ctx context.Context, req *Request) (*Response, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*Request) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*Request) when calling interceptor") + } + return s.NitroRPC.Call(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*Response) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*Response) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *Response + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *Response and nil error while calling Call. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + respBytes, err := proto.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal proto response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/protobuf") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *nitroRPCServer) ServiceDescriptor() ([]byte, int) { + return twirpFileDescriptor0, 0 +} + +func (s *nitroRPCServer) ProtocGenTwirpVersion() string { + return "v8.1.3" +} + +// PathPrefix returns the base service path, in the form: "//./" +// that is everything in a Twirp route except for the . This can be used for routing, +// for example to identify the requests that are targeted to this service in a mux. +func (s *nitroRPCServer) PathPrefix() string { + return baseServicePath(s.pathPrefix, "nitro_rpc", "NitroRPC") +} + +// ===== +// Utils +// ===== + +// HTTPClient is the interface used by generated clients to send HTTP requests. +// It is fulfilled by *(net/http).Client, which is sufficient for most users. +// Users can provide their own implementation for special retry policies. +// +// HTTPClient implementations should not follow redirects. Redirects are +// automatically disabled if *(net/http).Client is passed to client +// constructors. See the withoutRedirects function in this file for more +// details. +type HTTPClient interface { + Do(req *http.Request) (*http.Response, error) +} + +// TwirpServer is the interface generated server structs will support: they're +// HTTP handlers with additional methods for accessing metadata about the +// service. Those accessors are a low-level API for building reflection tools. +// Most people can think of TwirpServers as just http.Handlers. +type TwirpServer interface { + http.Handler + + // ServiceDescriptor returns gzipped bytes describing the .proto file that + // this service was generated from. Once unzipped, the bytes can be + // unmarshalled as a + // google.golang.org/protobuf/types/descriptorpb.FileDescriptorProto. + // + // The returned integer is the index of this particular service within that + // FileDescriptorProto's 'Service' slice of ServiceDescriptorProtos. This is a + // low-level field, expected to be used for reflection. + ServiceDescriptor() ([]byte, int) + + // ProtocGenTwirpVersion is the semantic version string of the version of + // twirp used to generate this file. + ProtocGenTwirpVersion() string + + // PathPrefix returns the HTTP URL path prefix for all methods handled by this + // service. This can be used with an HTTP mux to route Twirp requests. + // The path prefix is in the form: "//./" + // that is, everything in a Twirp route except for the at the end. + PathPrefix() string +} + +func newServerOpts(opts []interface{}) *twirp.ServerOptions { + serverOpts := &twirp.ServerOptions{} + for _, opt := range opts { + switch o := opt.(type) { + case twirp.ServerOption: + o(serverOpts) + case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument + twirp.WithServerHooks(o)(serverOpts) + case nil: // backwards compatibility, allow nil value for the argument + continue + default: + panic(fmt.Sprintf("Invalid option type %T, please use a twirp.ServerOption", o)) + } + } + return serverOpts +} + +// WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta). +// Useful outside of the Twirp server (e.g. http middleware), but does not trigger hooks. +// If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) +func WriteError(resp http.ResponseWriter, err error) { + writeError(context.Background(), resp, err, nil) +} + +// writeError writes Twirp errors in the response and triggers hooks. +func writeError(ctx context.Context, resp http.ResponseWriter, err error, hooks *twirp.ServerHooks) { + // Convert to a twirp.Error. Non-twirp errors are converted to internal errors. + var twerr twirp.Error + if !errors.As(err, &twerr) { + twerr = twirp.InternalErrorWith(err) + } + + statusCode := twirp.ServerHTTPStatusFromErrorCode(twerr.Code()) + ctx = ctxsetters.WithStatusCode(ctx, statusCode) + ctx = callError(ctx, hooks, twerr) + + respBody := marshalErrorToJSON(twerr) + + resp.Header().Set("Content-Type", "application/json") // Error responses are always JSON + resp.Header().Set("Content-Length", strconv.Itoa(len(respBody))) + resp.WriteHeader(statusCode) // set HTTP status code and send response + + _, writeErr := resp.Write(respBody) + if writeErr != nil { + // We have three options here. We could log the error, call the Error + // hook, or just silently ignore the error. + // + // Logging is unacceptable because we don't have a user-controlled + // logger; writing out to stderr without permission is too rude. + // + // Calling the Error hook would confuse users: it would mean the Error + // hook got called twice for one request, which is likely to lead to + // duplicated log messages and metrics, no matter how well we document + // the behavior. + // + // Silently ignoring the error is our least-bad option. It's highly + // likely that the connection is broken and the original 'err' says + // so anyway. + _ = writeErr + } + + callResponseSent(ctx, hooks) +} + +// sanitizeBaseURL parses the the baseURL, and adds the "http" scheme if needed. +// If the URL is unparsable, the baseURL is returned unchanged. +func sanitizeBaseURL(baseURL string) string { + u, err := url.Parse(baseURL) + if err != nil { + return baseURL // invalid URL will fail later when making requests + } + if u.Scheme == "" { + u.Scheme = "http" + } + return u.String() +} + +// baseServicePath composes the path prefix for the service (without ). +// e.g.: baseServicePath("/twirp", "my.pkg", "MyService") +// +// returns => "/twirp/my.pkg.MyService/" +// +// e.g.: baseServicePath("", "", "MyService") +// +// returns => "/MyService/" +func baseServicePath(prefix, pkg, service string) string { + fullServiceName := service + if pkg != "" { + fullServiceName = pkg + "." + service + } + return path.Join("/", prefix, fullServiceName) + "/" +} + +// parseTwirpPath extracts path components form a valid Twirp route. +// Expected format: "[]/./" +// e.g.: prefix, pkgService, method := parseTwirpPath("/twirp/pkg.Svc/MakeHat") +func parseTwirpPath(path string) (string, string, string) { + parts := strings.Split(path, "/") + if len(parts) < 2 { + return "", "", "" + } + method := parts[len(parts)-1] + pkgService := parts[len(parts)-2] + prefix := strings.Join(parts[0:len(parts)-2], "/") + return prefix, pkgService, method +} + +// getCustomHTTPReqHeaders retrieves a copy of any headers that are set in +// a context through the twirp.WithHTTPRequestHeaders function. +// If there are no headers set, or if they have the wrong type, nil is returned. +func getCustomHTTPReqHeaders(ctx context.Context) http.Header { + header, ok := twirp.HTTPRequestHeaders(ctx) + if !ok || header == nil { + return nil + } + copied := make(http.Header) + for k, vv := range header { + if vv == nil { + copied[k] = nil + continue + } + copied[k] = make([]string, len(vv)) + copy(copied[k], vv) + } + return copied +} + +// newRequest makes an http.Request from a client, adding common headers. +func newRequest(ctx context.Context, url string, reqBody io.Reader, contentType string) (*http.Request, error) { + req, err := http.NewRequest("POST", url, reqBody) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if customHeader := getCustomHTTPReqHeaders(ctx); customHeader != nil { + req.Header = customHeader + } + req.Header.Set("Accept", contentType) + req.Header.Set("Content-Type", contentType) + req.Header.Set("Twirp-Version", "v8.1.3") + return req, nil +} + +// JSON serialization for errors +type twerrJSON struct { + Code string `json:"code"` + Msg string `json:"msg"` + Meta map[string]string `json:"meta,omitempty"` +} + +// marshalErrorToJSON returns JSON from a twirp.Error, that can be used as HTTP error response body. +// If serialization fails, it will use a descriptive Internal error instead. +func marshalErrorToJSON(twerr twirp.Error) []byte { + // make sure that msg is not too large + msg := twerr.Msg() + if len(msg) > 1e6 { + msg = msg[:1e6] + } + + tj := twerrJSON{ + Code: string(twerr.Code()), + Msg: msg, + Meta: twerr.MetaMap(), + } + + buf, err := json.Marshal(&tj) + if err != nil { + buf = []byte("{\"type\": \"" + twirp.Internal + "\", \"msg\": \"There was an error but it could not be serialized into JSON\"}") // fallback + } + + return buf +} + +// errorFromResponse builds a twirp.Error from a non-200 HTTP response. +// If the response has a valid serialized Twirp error, then it's returned. +// If not, the response status code is used to generate a similar twirp +// error. See twirpErrorFromIntermediary for more info on intermediary errors. +func errorFromResponse(resp *http.Response) twirp.Error { + statusCode := resp.StatusCode + statusText := http.StatusText(statusCode) + + if isHTTPRedirect(statusCode) { + // Unexpected redirect: it must be an error from an intermediary. + // Twirp clients don't follow redirects automatically, Twirp only handles + // POST requests, redirects should only happen on GET and HEAD requests. + location := resp.Header.Get("Location") + msg := fmt.Sprintf("unexpected HTTP status code %d %q received, Location=%q", statusCode, statusText, location) + return twirpErrorFromIntermediary(statusCode, msg, location) + } + + respBodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return wrapInternal(err, "failed to read server error response body") + } + + var tj twerrJSON + dec := json.NewDecoder(bytes.NewReader(respBodyBytes)) + dec.DisallowUnknownFields() + if err := dec.Decode(&tj); err != nil || tj.Code == "" { + // Invalid JSON response; it must be an error from an intermediary. + msg := fmt.Sprintf("Error from intermediary with HTTP status code %d %q", statusCode, statusText) + return twirpErrorFromIntermediary(statusCode, msg, string(respBodyBytes)) + } + + errorCode := twirp.ErrorCode(tj.Code) + if !twirp.IsValidErrorCode(errorCode) { + msg := "invalid type returned from server error response: " + tj.Code + return twirp.InternalError(msg).WithMeta("body", string(respBodyBytes)) + } + + twerr := twirp.NewError(errorCode, tj.Msg) + for k, v := range tj.Meta { + twerr = twerr.WithMeta(k, v) + } + return twerr +} + +// twirpErrorFromIntermediary maps HTTP errors from non-twirp sources to twirp errors. +// The mapping is similar to gRPC: https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md. +// Returned twirp Errors have some additional metadata for inspection. +func twirpErrorFromIntermediary(status int, msg string, bodyOrLocation string) twirp.Error { + var code twirp.ErrorCode + if isHTTPRedirect(status) { // 3xx + code = twirp.Internal + } else { + switch status { + case 400: // Bad Request + code = twirp.Internal + case 401: // Unauthorized + code = twirp.Unauthenticated + case 403: // Forbidden + code = twirp.PermissionDenied + case 404: // Not Found + code = twirp.BadRoute + case 429: // Too Many Requests + code = twirp.ResourceExhausted + case 502, 503, 504: // Bad Gateway, Service Unavailable, Gateway Timeout + code = twirp.Unavailable + default: // All other codes + code = twirp.Unknown + } + } + + twerr := twirp.NewError(code, msg) + twerr = twerr.WithMeta("http_error_from_intermediary", "true") // to easily know if this error was from intermediary + twerr = twerr.WithMeta("status_code", strconv.Itoa(status)) + if isHTTPRedirect(status) { + twerr = twerr.WithMeta("location", bodyOrLocation) + } else { + twerr = twerr.WithMeta("body", bodyOrLocation) + } + return twerr +} + +func isHTTPRedirect(status int) bool { + return status >= 300 && status <= 399 +} + +// wrapInternal wraps an error with a prefix as an Internal error. +// The original error cause is accessible by github.com/pkg/errors.Cause. +func wrapInternal(err error, prefix string) twirp.Error { + return twirp.InternalErrorWith(&wrappedError{prefix: prefix, cause: err}) +} + +type wrappedError struct { + prefix string + cause error +} + +func (e *wrappedError) Error() string { return e.prefix + ": " + e.cause.Error() } +func (e *wrappedError) Unwrap() error { return e.cause } // for go1.13 + errors.Is/As +func (e *wrappedError) Cause() error { return e.cause } // for github.com/pkg/errors + +// ensurePanicResponses makes sure that rpc methods causing a panic still result in a Twirp Internal +// error response (status 500), and error hooks are properly called with the panic wrapped as an error. +// The panic is re-raised so it can be handled normally with middleware. +func ensurePanicResponses(ctx context.Context, resp http.ResponseWriter, hooks *twirp.ServerHooks) { + if r := recover(); r != nil { + // Wrap the panic as an error so it can be passed to error hooks. + // The original error is accessible from error hooks, but not visible in the response. + err := errFromPanic(r) + twerr := &internalWithCause{msg: "Internal service panic", cause: err} + // Actually write the error + writeError(ctx, resp, twerr, hooks) + // If possible, flush the error to the wire. + f, ok := resp.(http.Flusher) + if ok { + f.Flush() + } + + panic(r) + } +} + +// errFromPanic returns the typed error if the recovered panic is an error, otherwise formats as error. +func errFromPanic(p interface{}) error { + if err, ok := p.(error); ok { + return err + } + return fmt.Errorf("panic: %v", p) +} + +// internalWithCause is a Twirp Internal error wrapping an original error cause, +// but the original error message is not exposed on Msg(). The original error +// can be checked with go1.13+ errors.Is/As, and also by (github.com/pkg/errors).Unwrap +type internalWithCause struct { + msg string + cause error +} + +func (e *internalWithCause) Unwrap() error { return e.cause } // for go1.13 + errors.Is/As +func (e *internalWithCause) Cause() error { return e.cause } // for github.com/pkg/errors +func (e *internalWithCause) Error() string { return e.msg + ": " + e.cause.Error() } +func (e *internalWithCause) Code() twirp.ErrorCode { return twirp.Internal } +func (e *internalWithCause) Msg() string { return e.msg } +func (e *internalWithCause) Meta(key string) string { return "" } +func (e *internalWithCause) MetaMap() map[string]string { return nil } +func (e *internalWithCause) WithMeta(key string, val string) twirp.Error { return e } + +// malformedRequestError is used when the twirp server cannot unmarshal a request +func malformedRequestError(msg string) twirp.Error { + return twirp.NewError(twirp.Malformed, msg) +} + +// badRouteError is used when the twirp server cannot route a request +func badRouteError(msg string, method, url string) twirp.Error { + err := twirp.NewError(twirp.BadRoute, msg) + err = err.WithMeta("twirp_invalid_route", method+" "+url) + return err +} + +// withoutRedirects makes sure that the POST request can not be redirected. +// The standard library will, by default, redirect requests (including POSTs) if it gets a 302 or +// 303 response, and also 301s in go1.8. It redirects by making a second request, changing the +// method to GET and removing the body. This produces very confusing error messages, so instead we +// set a redirect policy that always errors. This stops Go from executing the redirect. +// +// We have to be a little careful in case the user-provided http.Client has its own CheckRedirect +// policy - if so, we'll run through that policy first. +// +// Because this requires modifying the http.Client, we make a new copy of the client and return it. +func withoutRedirects(in *http.Client) *http.Client { + copy := *in + copy.CheckRedirect = func(req *http.Request, via []*http.Request) error { + if in.CheckRedirect != nil { + // Run the input's redirect if it exists, in case it has side effects, but ignore any error it + // returns, since we want to use ErrUseLastResponse. + err := in.CheckRedirect(req, via) + _ = err // Silly, but this makes sure generated code passes errcheck -blank, which some people use. + } + return http.ErrUseLastResponse + } + return © +} + +// doProtobufRequest makes a Protobuf request to the remote Twirp service. +func doProtobufRequest(ctx context.Context, client HTTPClient, hooks *twirp.ClientHooks, url string, in, out proto.Message) (_ context.Context, err error) { + reqBodyBytes, err := proto.Marshal(in) + if err != nil { + return ctx, wrapInternal(err, "failed to marshal proto request") + } + reqBody := bytes.NewBuffer(reqBodyBytes) + if err = ctx.Err(); err != nil { + return ctx, wrapInternal(err, "aborted because context was done") + } + + req, err := newRequest(ctx, url, reqBody, "application/protobuf") + if err != nil { + return ctx, wrapInternal(err, "could not build request") + } + ctx, err = callClientRequestPrepared(ctx, hooks, req) + if err != nil { + return ctx, err + } + + req = req.WithContext(ctx) + resp, err := client.Do(req) + if err != nil { + return ctx, wrapInternal(err, "failed to do request") + } + defer func() { _ = resp.Body.Close() }() + + if err = ctx.Err(); err != nil { + return ctx, wrapInternal(err, "aborted because context was done") + } + + if resp.StatusCode != 200 { + return ctx, errorFromResponse(resp) + } + + respBodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return ctx, wrapInternal(err, "failed to read response body") + } + if err = ctx.Err(); err != nil { + return ctx, wrapInternal(err, "aborted because context was done") + } + + if err = proto.Unmarshal(respBodyBytes, out); err != nil { + return ctx, wrapInternal(err, "failed to unmarshal proto response") + } + return ctx, nil +} + +// doJSONRequest makes a JSON request to the remote Twirp service. +func doJSONRequest(ctx context.Context, client HTTPClient, hooks *twirp.ClientHooks, url string, in, out proto.Message) (_ context.Context, err error) { + marshaler := &protojson.MarshalOptions{UseProtoNames: true} + reqBytes, err := marshaler.Marshal(in) + if err != nil { + return ctx, wrapInternal(err, "failed to marshal json request") + } + if err = ctx.Err(); err != nil { + return ctx, wrapInternal(err, "aborted because context was done") + } + + req, err := newRequest(ctx, url, bytes.NewReader(reqBytes), "application/json") + if err != nil { + return ctx, wrapInternal(err, "could not build request") + } + ctx, err = callClientRequestPrepared(ctx, hooks, req) + if err != nil { + return ctx, err + } + + req = req.WithContext(ctx) + resp, err := client.Do(req) + if err != nil { + return ctx, wrapInternal(err, "failed to do request") + } + + defer func() { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = wrapInternal(cerr, "failed to close response body") + } + }() + + if err = ctx.Err(); err != nil { + return ctx, wrapInternal(err, "aborted because context was done") + } + + if resp.StatusCode != 200 { + return ctx, errorFromResponse(resp) + } + + d := json.NewDecoder(resp.Body) + rawRespBody := json.RawMessage{} + if err := d.Decode(&rawRespBody); err != nil { + return ctx, wrapInternal(err, "failed to unmarshal json response") + } + unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true} + if err = unmarshaler.Unmarshal(rawRespBody, out); err != nil { + return ctx, wrapInternal(err, "failed to unmarshal json response") + } + if err = ctx.Err(); err != nil { + return ctx, wrapInternal(err, "aborted because context was done") + } + return ctx, nil +} + +// Call twirp.ServerHooks.RequestReceived if the hook is available +func callRequestReceived(ctx context.Context, h *twirp.ServerHooks) (context.Context, error) { + if h == nil || h.RequestReceived == nil { + return ctx, nil + } + return h.RequestReceived(ctx) +} + +// Call twirp.ServerHooks.RequestRouted if the hook is available +func callRequestRouted(ctx context.Context, h *twirp.ServerHooks) (context.Context, error) { + if h == nil || h.RequestRouted == nil { + return ctx, nil + } + return h.RequestRouted(ctx) +} + +// Call twirp.ServerHooks.ResponsePrepared if the hook is available +func callResponsePrepared(ctx context.Context, h *twirp.ServerHooks) context.Context { + if h == nil || h.ResponsePrepared == nil { + return ctx + } + return h.ResponsePrepared(ctx) +} + +// Call twirp.ServerHooks.ResponseSent if the hook is available +func callResponseSent(ctx context.Context, h *twirp.ServerHooks) { + if h == nil || h.ResponseSent == nil { + return + } + h.ResponseSent(ctx) +} + +// Call twirp.ServerHooks.Error if the hook is available +func callError(ctx context.Context, h *twirp.ServerHooks, err twirp.Error) context.Context { + if h == nil || h.Error == nil { + return ctx + } + return h.Error(ctx, err) +} + +func callClientResponseReceived(ctx context.Context, h *twirp.ClientHooks) { + if h == nil || h.ResponseReceived == nil { + return + } + h.ResponseReceived(ctx) +} + +func callClientRequestPrepared(ctx context.Context, h *twirp.ClientHooks, req *http.Request) (context.Context, error) { + if h == nil || h.RequestPrepared == nil { + return ctx, nil + } + return h.RequestPrepared(ctx, req) +} + +func callClientError(ctx context.Context, h *twirp.ClientHooks, err twirp.Error) { + if h == nil || h.Error == nil { + return + } + h.Error(ctx, err) +} + +var twirpFileDescriptor0 = []byte{ + // 299 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x92, 0xc1, 0x4b, 0xc3, 0x30, + 0x18, 0xc5, 0xad, 0x2d, 0xdb, 0xfa, 0x29, 0x4e, 0x22, 0x48, 0x1d, 0x0a, 0xa5, 0xa7, 0x82, 0xd2, + 0xb2, 0x4e, 0x50, 0x10, 0x2f, 0xee, 0xa2, 0x1e, 0x64, 0xe4, 0xe8, 0x65, 0x64, 0x6d, 0xd8, 0x0a, + 0xcd, 0x92, 0x25, 0x29, 0xe8, 0x41, 0xf0, 0x4f, 0x97, 0xb6, 0x59, 0x75, 0x3a, 0x3c, 0xec, 0x92, + 0xe4, 0x3d, 0xde, 0xf7, 0xc8, 0x8f, 0x04, 0xfa, 0xcb, 0x5c, 0x4b, 0x3e, 0x95, 0x22, 0x8d, 0x84, + 0xe4, 0x9a, 0x23, 0xb7, 0x35, 0x82, 0x47, 0xe8, 0x62, 0xba, 0x2a, 0xa9, 0xd2, 0xe8, 0x12, 0x6c, + 0x49, 0x57, 0x9e, 0xe5, 0x5b, 0xe1, 0x41, 0x72, 0x16, 0x7d, 0x0f, 0x99, 0xc0, 0x84, 0xbc, 0x17, + 0x9c, 0x64, 0xb8, 0x4a, 0xa1, 0x63, 0xb0, 0x55, 0x3e, 0xf7, 0xf6, 0x7d, 0x2b, 0x74, 0x71, 0x75, + 0x0c, 0x3e, 0xe0, 0x68, 0x33, 0x88, 0x2e, 0x00, 0x64, 0xe3, 0x4c, 0xf3, 0xac, 0xee, 0x75, 0xb0, + 0x6b, 0x9c, 0xa7, 0x0c, 0x9d, 0x42, 0x87, 0x51, 0xbd, 0xe0, 0x99, 0x69, 0x31, 0xaa, 0xf2, 0x05, + 0x91, 0x84, 0x29, 0xcf, 0xf6, 0xad, 0xf0, 0x10, 0x1b, 0x85, 0xce, 0xc1, 0xd5, 0x39, 0xa3, 0x4a, + 0x13, 0x26, 0x3c, 0xa7, 0x69, 0x6b, 0x8d, 0xe0, 0x19, 0x7a, 0x98, 0x2a, 0xc1, 0x97, 0x8a, 0xa2, + 0xab, 0x8a, 0x44, 0x19, 0x92, 0xc1, 0x06, 0x49, 0x93, 0xf8, 0x81, 0xa2, 0xb6, 0xa0, 0x7c, 0x5a, + 0xd0, 0xff, 0x15, 0xdd, 0x15, 0xc6, 0x83, 0xae, 0xa4, 0xaa, 0x2c, 0xf4, 0x9a, 0x66, 0x2d, 0xff, + 0xc7, 0x49, 0xee, 0xa1, 0xf7, 0x52, 0x5d, 0x1b, 0x4f, 0xc6, 0x68, 0x08, 0xce, 0x98, 0x14, 0x05, + 0x42, 0x7f, 0xdf, 0x64, 0x70, 0xb2, 0x85, 0x2e, 0xd8, 0x7b, 0x18, 0xbd, 0x0e, 0xe7, 0xb9, 0x5e, + 0x94, 0xb3, 0x28, 0xe5, 0x2c, 0xa6, 0x32, 0xbd, 0xb9, 0x4d, 0xae, 0x63, 0xfa, 0x46, 0x98, 0x28, + 0xa8, 0x8a, 0xdb, 0x99, 0xb8, 0xfe, 0x10, 0x77, 0xf5, 0x3a, 0xeb, 0xd4, 0xdb, 0xe8, 0x2b, 0x00, + 0x00, 0xff, 0xff, 0x42, 0x05, 0x4f, 0x86, 0x30, 0x02, 0x00, 0x00, +} diff --git a/nitro_rpc/proto/tools.go b/nitro_rpc/proto/tools.go new file mode 100644 index 0000000..27ed4f1 --- /dev/null +++ b/nitro_rpc/proto/tools.go @@ -0,0 +1,8 @@ +//go:build proto + +package proto + +import ( + _ "github.com/twitchtv/twirp/protoc-gen-twirp" + _ "google.golang.org/protobuf/cmd/protoc-gen-go" +) diff --git a/nitro_rpc/remappings.txt b/nitro_rpc/remappings.txt new file mode 100644 index 0000000..d5a9862 --- /dev/null +++ b/nitro_rpc/remappings.txt @@ -0,0 +1,6 @@ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ +erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/ +exit-format/=lib/nitro/lib/exit-format/src/ +forge-std/=lib/forge-std/src/ +halmos-cheatcodes/=lib/openzeppelin-contracts/lib/halmos-cheatcodes/src/ +nitro/=lib/nitro/src/ diff --git a/nitro_rpc/src/NitroRPC.sol b/nitro_rpc/src/NitroRPC.sol index 6f4d2e3..9c98629 100644 --- a/nitro_rpc/src/NitroRPC.sol +++ b/nitro_rpc/src/NitroRPC.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; - +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {INitroTypes} from "nitro/interfaces/INitroTypes.sol"; import {NitroUtils} from "nitro/libraries/NitroUtils.sol"; import {IForceMoveApp} from "nitro/interfaces/IForceMoveApp.sol"; @@ -21,8 +22,8 @@ contract NitroRPC is IForceMoveApp { struct PayloadSigned { Payload rpcMessage; - INitroTypes.Signature clientSig; - INitroTypes.Signature serverSig; + bytes clientSig; + bytes serverSig; } enum AllocationIndices { @@ -42,7 +43,7 @@ contract NitroRPC is IForceMoveApp { INitroTypes.RecoveredVariablePart[] calldata proof, INitroTypes.RecoveredVariablePart calldata candidate ) external pure override returns (bool, string memory) { - require(fixedPart.participants.length == uint256(AllocationIndices.Server) + 1, "bad number of participants"); + require(fixedPart.participants.length == uint256(type(AllocationIndices).max) + 1, "bad number of participants"); PayloadSigned memory payloadSigned = abi.decode(candidate.variablePart.appData, (PayloadSigned)); requireValidPayload(fixedPart, payloadSigned); @@ -58,25 +59,7 @@ contract NitroRPC is IForceMoveApp { } // This pure internal function recovers the signer address from the payload and its signature. - function recoverPayloadSigner(Payload memory payload, INitroTypes.Signature memory signature) internal pure returns (address) { - // Encode and hash the payload data. - // Using abi.encode ensures proper padding and decoding, avoiding potential ambiguities with dynamic types. - bytes32 messageHash = keccak256( - abi.encode( - payload.requestId, - payload.timestamp, - payload.method, - payload.params, - payload.result - ) - ); - - // Apply the Ethereum Signed Message prefix. - bytes32 ethSignedMessageHash = keccak256( - abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash) - ); - - // Recover the signer address using ecrecover. - return ecrecover(ethSignedMessageHash, signature.v, signature.r, signature.s); + function recoverPayloadSigner(Payload memory payload, bytes memory signature) internal pure returns (address) { + return ECDSA.recover(MessageHashUtils.toEthSignedMessageHash(keccak256(abi.encode(payload))), signature); } } diff --git a/nitro_rpc/test/NitroRPCTest.t.sol b/nitro_rpc/test/NitroRPCTest.t.sol new file mode 100644 index 0000000..9469de9 --- /dev/null +++ b/nitro_rpc/test/NitroRPCTest.t.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import "../src/NitroRPC.sol"; + +// A helper contract to expose the internal recoverPayloadSigner function. +contract NitroRPCHarness is NitroRPC { + function exposed_recoverPayloadSigner( + Payload memory payload, + bytes memory signature + ) public pure returns (address) { + return recoverPayloadSigner(payload, signature); + } +} + +contract NitroRPCTest is Test { + NitroRPCHarness testContract; + // Fixed private key for testing. + uint256 constant fixedPrivateKey = 1; + + function setUp() public { + testContract = new NitroRPCHarness(); + } + + function test_recoverPayloadSigner_fixedPayloadAndSigner() public view { + // Use fixed values for payload so the test output is deterministic. + NitroRPC.Payload memory payload = NitroRPC.Payload({ + requestId: 42, + timestamp: 1000, + method: "testMethod", + params: bytes("testParams"), + result: bytes("testResult") + }); + + // 1. Compute the message hash from the payload. + bytes32 messageHash = keccak256(abi.encode(payload)); + + // 2. Apply the Ethereum Signed Message prefix. + bytes32 ethSignedMessageHash = keccak256( + abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash) + ); + + // 3. Sign the hash using Foundry's cheatcode. + (uint8 v, bytes32 r, bytes32 s) = vm.sign(fixedPrivateKey, ethSignedMessageHash); + address expectedSigner = vm.addr(fixedPrivateKey); + + // 4. Create the signature object. + bytes memory signature = abi.encodePacked(r, s, v); + // Recover the signer using the testable function. + address recovered = testContract.exposed_recoverPayloadSigner(payload, signature); + + // Log the values in hexadecimal representations. + console.log("privateKey:"); + console.logBytes32(bytes32(fixedPrivateKey)); + + console.log("signature:"); + console.logBytes(signature); + + // Verify that the recovered address matches the expected signer. + assertEq(recovered, expectedSigner, "Recovered signer does not match expected signer"); + } +} diff --git a/nitro_rpc/web/server.go b/nitro_rpc/web/server.go new file mode 100644 index 0000000..c819a33 --- /dev/null +++ b/nitro_rpc/web/server.go @@ -0,0 +1,56 @@ +package web + +import ( + "context" + "math/big" + + "github.com/erc7824/examples/nitro_rpc/proto" + "github.com/layer-3/clearsync/pkg/signer" +) + +var _ proto.NitroRPC = (*Server)(nil) + +type Server struct { + mh MethodHandler + stateSigner signer.Signer +} + +type MethodHandler interface { + HandleCall(ctx context.Context, params []byte) ([]byte, error) +} + +func NewServer(handler MethodHandler, stateSigner signer.Signer) *Server { + return &Server{ + mh: handler, + stateSigner: stateSigner, + } +} + +func (s *Server) Call(ctx context.Context, req *proto.Request) (*proto.Response, error) { + result, err := s.mh.HandleCall(ctx, req.Req.GetParams()) + if err != nil { + return nil, err + } + + state := RPCState{ + RequestID: new(big.Int).SetUint64(req.Req.GetRequestId()), + Timestamp: new(big.Int).SetUint64(req.Req.GetTimestamp()), + Method: req.Req.GetMethod(), + Params: req.Req.GetParams(), + Result: result, + } + sig, err := s.Sign(state) + if err != nil { + return nil, err + } + + return &proto.Response{ + Res: &proto.ResponsePayload{ + RequestId: state.RequestID.Uint64(), + Timestamp: state.Timestamp.Uint64(), + Method: state.Method, + Results: state.Result, + }, + Sig: sig.String(), + }, nil +} diff --git a/nitro_rpc/web/server_test.go b/nitro_rpc/web/server_test.go new file mode 100644 index 0000000..2cf28b8 --- /dev/null +++ b/nitro_rpc/web/server_test.go @@ -0,0 +1,58 @@ +package web + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/erc7824/examples/nitro_rpc/proto" + "github.com/ethereum/go-ethereum/crypto" + "github.com/layer-3/clearsync/pkg/signer" + "github.com/stretchr/testify/require" +) + +type MethodHandlerMock struct{} + +func (m *MethodHandlerMock) HandleCall(ctx context.Context, params []byte) ([]byte, error) { + return params, nil +} + +func TestServerCall(t *testing.T) { + mh := &MethodHandlerMock{} + + privateKey, err := crypto.GenerateKey() + require.NoError(t, err) + ss := signer.NewLocalSigner(privateKey) + + handler := proto.NewNitroRPCServer(NewServer(mh, ss)) + srv := http.Server{ + Addr: ":8089", + Handler: handler, + } + + go func() { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + fmt.Printf("server error: %v", err) + } + }() + defer srv.Shutdown(context.Background()) + + client := proto.NewNitroRPCProtobufClient("http://localhost:8089", &http.Client{}) + req := &proto.Request{ + Req: &proto.RequestPayload{ + RequestId: 1, + Method: "test", + Params: []byte("test"), + Timestamp: uint64(time.Now().UTC().UnixNano()), + }, + } + res, err := client.Call(context.Background(), req) + require.NoError(t, err) + require.NotNil(t, res) + require.Equal(t, req.Req.GetRequestId(), res.Res.GetRequestId()) + require.Equal(t, req.Req.GetMethod(), res.Res.GetMethod()) + require.Equal(t, req.Req.GetTimestamp(), res.Res.GetTimestamp()) + require.Equal(t, req.Req.GetParams(), res.Res.GetResults()) +} diff --git a/nitro_rpc/web/sign.go b/nitro_rpc/web/sign.go new file mode 100644 index 0000000..5cedeef --- /dev/null +++ b/nitro_rpc/web/sign.go @@ -0,0 +1,61 @@ +package web + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/crypto" + "github.com/layer-3/clearsync/pkg/signer" +) + +var rpcStateABIArgs abi.Arguments + +type RPCState struct { + RequestID *big.Int `abi:"requestId"` + Timestamp *big.Int `abi:"timestamp"` + Method string `abi:"method"` + Params []byte `abi:"params"` + Result []byte `abi:"result"` +} + +type SignedRPCState struct { + RPCState + ClientSig signer.Signature + ServerSig signer.Signature +} + +func (s *Server) Sign(state RPCState) (signer.Signature, error) { + packed, err := rpcStateABIArgs.Pack(state) + if err != nil { + return signer.Signature{}, err + } + + messageHash := crypto.Keccak256(packed) + sig, err := signer.SignEthMessage(s.stateSigner, messageHash) + if err != nil { + return signer.Signature{}, err + } + + return sig, nil +} + +func init() { + tupleArgs := []abi.ArgumentMarshaling{ + {Name: "requestId", Type: "uint256"}, + {Name: "timestamp", Type: "uint256"}, + {Name: "method", Type: "string"}, + {Name: "params", Type: "bytes"}, + {Name: "result", Type: "bytes"}, + } + + tupleType, err := abi.NewType("tuple", "", tupleArgs) + if err != nil { + panic(err) + } + + rpcStateABIArgs = abi.Arguments{ + { + Type: tupleType, + }, + } +} diff --git a/nitro_rpc/web/sign_test.go b/nitro_rpc/web/sign_test.go new file mode 100644 index 0000000..679e1f6 --- /dev/null +++ b/nitro_rpc/web/sign_test.go @@ -0,0 +1,35 @@ +package web + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/layer-3/clearsync/pkg/signer" + "github.com/stretchr/testify/require" +) + +func TestServerSign(t *testing.T) { + mh := &MethodHandlerMock{} + + privateKey, err := crypto.HexToECDSA("0000000000000000000000000000000000000000000000000000000000000001") + require.NoError(t, err) + ss := signer.NewLocalSigner(privateKey) + + srv := NewServer(mh, ss) + + state := RPCState{ + Timestamp: new(big.Int).SetUint64(1000), + RequestID: new(big.Int).SetUint64(42), + Method: "testMethod", + Params: []byte("testParams"), + Result: []byte("testResult"), + } + + sig, err := srv.Sign(state) + require.NoError(t, err) + + expectedSig := signer.NewSignatureFromBytes(hexutil.MustDecode("0x4a9aa9cafc9e804a17cf56ba8c6ff82a5dffacb00997d2f95005636ed8b40de32fbaaf2361b62dfd1bbecfd6603422d5497eda127fdfb902951e2acff17c9c361c")) + require.Equal(t, expectedSig, sig) +} From 9450e1ec4441cd9bb99b369d56a8187916c8f04a Mon Sep 17 00:00:00 2001 From: Louis Date: Tue, 18 Mar 2025 15:12:58 +0200 Subject: [PATCH 7/8] Delete .vscode/settings.json --- .vscode/settings.json | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 57a1156..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "solidity.packageDefaultDependenciesContractsDirectory": "src", - "solidity.packageDefaultDependenciesDirectory": "lib" -} - \ No newline at end of file From 4dcb24608f585f877442351ffc76722339bc2be0 Mon Sep 17 00:00:00 2001 From: Louis Date: Tue, 18 Mar 2025 17:53:34 +0200 Subject: [PATCH 8/8] Adding Nitro Channel Protocol Funding, and Finalize --- README.md | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/README.md b/README.md index 2e38bf9..a1a684b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,110 @@ # Nitro Examples Nitro Protocol Examples +## Nitro Channel Protocol + +```mermaid +sequenceDiagram + participant Client + participant Server + + Client->>Server: create_channel(signed funding allocation) + Server-->>Client: response(countersigned allocation) + + Client->>Server: open_channel(signed turnNum=1) + Server-->>Client: response(countersigned turnNum=1) + + opt Application Protocol + Note over Client: App protocol + Client->>Server: AppData(nitro_rpc) + Server-->>Client: AppData(nitro_rpc) + end + + Client->>Server: close_channel(signed state turnNum=N) + Server-->>Client: response(countersigned state at TurnNum=N) +``` + +```protobuf +/** + * A 42-character hexadecimal address + * derived from the last 20 bytes of the public key + */ +message Address { + string value = 1; +} + +/** + * A 132-character hexadecimal string + */ +message Signature { + uint32 v = 1; + bytes r = 2; // 32 bytes + bytes s = 3; // 32 bytes +} + +enum AssetType { + ASSET_TYPE_UNSPECIFIED = 0; + ASSET_TYPE_ERC721 = 1; + ASSET_TYPE_ERC1155 = 2; + ASSET_TYPE_QUALIFIED = 3; +} + +enum AllocationType { + ALLOCATION_TYPE_UNSPECIFIED = 0; + ALLOCATION_TYPE_WITHDRAW_HELPER = 1; + ALLOCATION_TYPE_GUARANTEE = 2; +} + +message Allocation { + bytes destination = 1; // bytes32 in solidity + string amount = 2; // big.Int cast to string + AllocationType allocation_type = 3; + bytes metadata = 4; +} + +message AssetMetadata { + AssetType asset_type = 1; + bytes metadata = 2; +} + +message SingleAssetExit { + // Either the zero address (implying the native token) + // or the address of an ERC20 contract + core.Address asset = 1; + AssetMetadata asset_metadata = 2; + repeated Allocation allocations = 3; +} + +message Exit { + repeated SingleAssetExit single_asset_exits = 1; +} + +message FixedPart { + repeated Address participants = 1; + uint64 channel_nonce = 2; + core.Address app_definition = 3; + uint32 challenge_duration = 4; +} + +message VariablePart { + outcome.Exit outcome = 1; + bytes app_data = 2; + uint64 turn_num = 3; + bool is_final = 4; +} + +message State { + FixedPart fixed_part = 1; + VariablePart variable_part = 2; + string state_hash = 3; + Signature state_sig = 4; +} + +service Channel { + rpc Create(State) returns (State); + rpc Open(State) returns (State); + rpc Close(State) returns (State); +} +``` ## Nitro RPC