Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(protocol): proof verification aggregation #17938

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/protocol/contracts/L1/ITaikoL1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,13 @@ interface ITaikoL1 {
/// @param _blockIds The indices of the blocks to prove.
/// @param _inputArr An list of abi-encoded (TaikoData.BlockMetadata, TaikoData.Transition,
/// TaikoData.TierProof) tuples.
function proveBlocks(uint64[] calldata _blockIds, bytes[] calldata _inputArr) external;
/// @param _proof The proof.
function proveBlocks(
uint64[] calldata _blockIds,
bytes[] calldata _inputArr,
bytes calldata _proof
)
external;

/// @notice Verifies up to a certain number of blocks.
/// @param _maxBlocksToVerify Max number of blocks to verify.
Expand Down
24 changes: 21 additions & 3 deletions packages/protocol/contracts/L1/TaikoL1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ contract TaikoL1 is EssentialContract, ITaikoL1, TaikoEvents {
/// @inheritdoc ITaikoL1
function proveBlocks(
uint64[] calldata _blockIds,
bytes[] calldata _inputArr
bytes[] calldata _inputArr,
bytes calldata _proof
)
external
whenNotPaused
Expand All @@ -162,8 +163,24 @@ contract TaikoL1 is EssentialContract, ITaikoL1, TaikoEvents {

TaikoData.Config memory config = getConfig();

// Run over all blocks and collect all blocks that need to be verified
IVerifier.Context[] memory ctxs = new IVerifier.Context[](_blockIds.length);
Brechtpd marked this conversation as resolved.
Show resolved Hide resolved
for (uint256 i; i < _blockIds.length; ++i) {
_proveBlock(_blockIds[i], _inputArr[i], config);
ctxs[i] = _proveBlock(_blockIds[i], _inputArr[i], config);
// We have to make sure that we only batch verify the same type of proof
if (
ctxs[i].verifier != ctxs[0].verifier || ctxs[i].isContesting != ctxs[0].isContesting
) {
revert L1_INVALID_PARAMS();
}
}

// Batch verify the blocks
// Do not run proof verification to contest an existing proof
if (!ctxs[0].isContesting) {
IVerifier(ctxs[0].verifier).verifyProofs(
ctxs, abi.decode(_proof, (TaikoData.TierProof))
);
}
}

Expand Down Expand Up @@ -342,8 +359,9 @@ contract TaikoL1 is EssentialContract, ITaikoL1, TaikoEvents {
TaikoData.Config memory _config
)
internal
returns (IVerifier.Context memory ctx_)
{
LibProving.proveBlock(state, _config, this, _blockId, _input);
ctx_ = LibProving.proveBlock(state, _config, this, _blockId, _input);

if (LibUtils.shouldVerifyBlocks(_config, _blockId, false)) {
LibVerifying.verifyBlocks(state, _config, this, _config.maxBlocksToVerify);
Expand Down
9 changes: 5 additions & 4 deletions packages/protocol/contracts/L1/libs/LibProving.sol
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ library LibProving {
bytes calldata _input
)
public
returns (IVerifier.Context memory ctx_)
{
Local memory local;

Expand Down Expand Up @@ -260,18 +261,18 @@ library LibProving {
address verifier = _resolver.resolve(local.tier.verifierName, false);
bool isContesting = proof.tier == ts.tier && local.tier.contestBond != 0;

IVerifier.Context memory ctx = IVerifier.Context({
ctx_ = IVerifier.Context({
metaHash: local.metaHash,
blobHash: meta.blobHash,
// Separate msgSender to allow the prover to be any address in the future.
prover: msg.sender,
msgSender: msg.sender,
blockId: local.blockId,
isContesting: isContesting,
blobUsed: meta.blobUsed
blobUsed: meta.blobUsed,
tran: tran,
verifier: verifier
});

IVerifier(verifier).verifyProof(ctx, tran, proof);
}

local.isTopTier = local.tier.contestBond == 0;
Expand Down
13 changes: 4 additions & 9 deletions packages/protocol/contracts/L1/provers/GuardianProver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -227,15 +227,10 @@ contract GuardianProver is IVerifier, EssentialContract {
}

/// @inheritdoc IVerifier
function verifyProof(
Context calldata _ctx,
TaikoData.Transition calldata,
TaikoData.TierProof calldata
)
external
view
{
if (_ctx.msgSender != address(this)) revert GV_PERMISSION_DENIED();
function verifyProofs(Context[] calldata _ctx, TaikoData.TierProof calldata) external view {
for (uint256 i = 0; i < _ctx.length; i++) {
if (_ctx[i].msgSender != address(this)) revert GV_PERMISSION_DENIED();
}
}

/// @notice Returns the number of guardians
Expand Down
10 changes: 3 additions & 7 deletions packages/protocol/contracts/verifiers/IVerifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,12 @@ interface IVerifier {
bool isContesting;
bool blobUsed;
address msgSender;
TaikoData.Transition tran;
address verifier;
}

/// @notice Verifies a proof.
/// @param _ctx The context of the proof verification.
/// @param _tran The transition to verify.
/// @param _proof The proof to verify.
function verifyProof(
Context calldata _ctx,
TaikoData.Transition calldata _tran,
TaikoData.TierProof calldata _proof
)
external;
function verifyProofs(Context[] calldata _ctx, TaikoData.TierProof calldata _proof) external;
}
47 changes: 28 additions & 19 deletions packages/protocol/contracts/verifiers/RiscZeroVerifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,39 +47,48 @@ contract RiscZeroVerifier is EssentialContract, IVerifier {
}

/// @inheritdoc IVerifier
function verifyProof(
Context calldata _ctx,
TaikoData.Transition calldata _tran,
TaikoData.TierProof calldata _proof
)
external
{
// Do not run proof verification to contest an existing proof
if (_ctx.isContesting) return;

function verifyProofs(Context[] calldata _ctx, TaikoData.TierProof calldata _proof) external {
// Decode will throw if not proper length/encoding
(bytes memory seal, bytes32 imageId) = abi.decode(_proof.data, (bytes, bytes32));
(bytes memory seal, bytes32 blockImageId, bytes32 aggregationImageId) =
abi.decode(_proof.data, (bytes, bytes32, bytes32));

if (!isImageTrusted[imageId]) {
// Check if the aggregation program is trusted
if (!isImageTrusted[aggregationImageId]) {
revert RISC_ZERO_INVALID_IMAGE_ID();
}
// Check if the block proving program is trusted
if (!isImageTrusted[blockImageId]) {
revert RISC_ZERO_INVALID_IMAGE_ID();
}

bytes32 publicInputHash = LibPublicInput.hashPublicInputs(
_tran, address(this), address(0), _ctx.prover, _ctx.metaHash, taikoChainId()
);
// Collect public inputs
bytes32[] memory public_inputs = new bytes32[](_ctx.length + 1);
// First public input is the block proving program key
public_inputs[0] = blockImageId;
// All other inputs are the block program public inputs (a single 32 byte value)
for (uint256 i = 0; i < _ctx.length; i++) {
public_inputs[i + 1] = LibPublicInput.hashPublicInputs(
_ctx[i].tran,
address(this),
address(0),
_ctx[i].prover,
_ctx[i].metaHash,
taikoChainId()
);
emit ProofVerified(_ctx[i].metaHash, public_inputs[i + 1]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not yet verified here??

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically true only but:

  • this is the only way to emit events per transition (one by one) since we are going thru the aggregated proofs.
  • if something fails, the TXN reverted anyways

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assumption indeed that all the proofs are verified correctly in some way afterwards.

}

// journalDigest is the sha256 hash of the hashed public input
bytes32 journalDigest = sha256(bytes.concat(FIXED_JOURNAL_HEADER, publicInputHash));
bytes32 journalDigest =
sha256(bytes.concat(FIXED_JOURNAL_HEADER, abi.encodePacked(public_inputs)));

// call risc0 verifier contract
(bool success,) = resolve(LibStrings.B_RISCZERO_GROTH16_VERIFIER, false).staticcall(
abi.encodeCall(IRiscZeroVerifier.verify, (seal, imageId, journalDigest))
abi.encodeCall(IRiscZeroVerifier.verify, (seal, aggregationImageId, journalDigest))
);
if (!success) {
revert RISC_ZERO_INVALID_PROOF();
}

emit ProofVerified(_ctx.metaHash, publicInputHash);
}

function taikoChainId() internal view virtual returns (uint64) {
Expand Down
46 changes: 33 additions & 13 deletions packages/protocol/contracts/verifiers/SP1Verifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -41,33 +41,53 @@ contract SP1Verifier is EssentialContract, IVerifier {
}

/// @inheritdoc IVerifier
function verifyProof(
Context calldata _ctx,
TaikoData.Transition calldata _tran,
function verifyProofs(
Context[] calldata _ctx,
TaikoData.TierProof calldata _proof
)
external
view
{
// Do not run proof verification to contest an existing proof
if (_ctx.isContesting) return;
// Extract the necessary data
bytes32 aggregation_program = bytes32(_proof.data[0:32]);
bytes32 block_proving_program = bytes32(_proof.data[32:64]);
bytes memory proof = _proof.data[64:];

// Avoid in-memory decoding, so in-place decode with slicing.
// e.g.: bytes32 programVKey = bytes32(_proof.data[0:32]);
if (!isProgramTrusted[bytes32(_proof.data[0:32])]) {
// Check if the aggregation program is trusted
if (!isProgramTrusted[aggregation_program]) {
revert SP1_INVALID_PROGRAM_VKEY();
}
// Check if the block proving program is trusted
if (!isProgramTrusted[block_proving_program]) {
revert SP1_INVALID_PROGRAM_VKEY();
}

// Need to be converted from bytes32 to bytes
bytes32 hashedPublicInput = LibPublicInput.hashPublicInputs(
_tran, address(this), address(0), _ctx.prover, _ctx.metaHash, taikoChainId()
);
// Collect public inputs
bytes32[] memory public_inputs = new bytes32[](_ctx.length + 1);
// First public input is the block proving program key
public_inputs[0] = block_proving_program;
// All other inputs are the block program public inputs (a single 32 byte value)
for (uint256 i = 0; i < _ctx.length; i++) {
// Need to be converted from bytes32 to bytes
public_inputs[i + 1] = sha256(
abi.encodePacked(
LibPublicInput.hashPublicInputs(
_ctx[i].tran,
address(this),
address(0),
_ctx[i].prover,
_ctx[i].metaHash,
taikoChainId()
)
)
);
}

// _proof.data[32:] is the succinct's proof position
(bool success,) = sp1RemoteVerifier().staticcall(
abi.encodeCall(
ISP1Verifier.verifyProof,
(bytes32(_proof.data[0:32]), abi.encode(hashedPublicInput), _proof.data[32:])
(block_proving_program, abi.encodePacked(public_inputs), proof)
)
);

Expand Down
45 changes: 29 additions & 16 deletions packages/protocol/contracts/verifiers/SgxVerifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -143,30 +143,43 @@ contract SgxVerifier is EssentialContract, IVerifier {
}

/// @inheritdoc IVerifier
function verifyProof(
Context calldata _ctx,
TaikoData.Transition calldata _tran,
function verifyProofs(
Context[] calldata _ctx,
TaikoData.TierProof calldata _proof
)
external
onlyFromNamed(LibStrings.B_TAIKO)
{
// Do not run proof verification to contest an existing proof
if (_ctx.isContesting) return;

// Size is: 89 bytes
// 4 bytes + 20 bytes + 65 bytes (signature) = 89
if (_proof.data.length != 89) revert SGX_INVALID_PROOF();
// 4 bytes + 20 bytes + 20 bytes + 65 bytes (signature) = 109
Copy link
Contributor

@dantaik dantaik Aug 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the current SGX prover doesn't support multiple blocks, can we ensure the length of _ctx and _tran are both 1 for now so we can upgrade contracts ealier?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inputs into the verification are still changed so a bit tricky. If length is 1 we could use the old verification path, but then this would be a special case and proof aggregation on a single proof would not be valid. Seems easier if aggregation is always used even if there's only a single proof, otherwise we need multiple verification paths.

if (_proof.data.length != 109) revert SGX_INVALID_PROOF();

uint32 id = uint32(bytes4(_proof.data[:4]));
address newInstance = address(bytes20(_proof.data[4:24]));

address oldInstance = ECDSA.recover(
LibPublicInput.hashPublicInputs(
_tran, address(this), newInstance, _ctx.prover, _ctx.metaHash, taikoChainId()
),
_proof.data[24:]
);
address oldInstance = address(bytes20(_proof.data[4:24]));
address newInstance = address(bytes20(_proof.data[24:44]));
bytes memory signature = _proof.data[44:];

// Collect public inputs
bytes32[] memory public_inputs = new bytes32[](_ctx.length + 1);
// First public input is the current instance public key
public_inputs[0] = bytes32(bytes20(oldInstance));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary? I mean as oldInstance is always recovered from signature, why we need explicit list oldInstance here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to have it available in the aggregation proof, because that's the address we need to verify against for the first block proof. We need to get it from somewhere to be able to correctly verify that proof, because we don't have it available anymore in the SGX instance itself (could be many blocks ago, and we only store the latest key pair in SGX).

// All other inputs are the block program public inputs (a single 32 byte value)
for (uint256 i = 0; i < _ctx.length; i++) {
// TODO: For now this assumes the new instance public key to remain the same
public_inputs[i + 1] = LibPublicInput.hashPublicInputs(
_ctx[i].tran,
address(this),
newInstance,
_ctx[i].prover,
_ctx[i].metaHash,
taikoChainId()
);
}

// Verify the blocks
if (oldInstance != ECDSA.recover(keccak256(abi.encodePacked(public_inputs)), signature)) {
revert SGX_INVALID_PROOF();
}

if (!_isInstanceValid(id, oldInstance)) revert SGX_INVALID_INSTANCE();

Expand Down
Loading
Loading