Skip to content

Commit 56f5c6d

Browse files
Full ReferenceDA implementation (#3873)
* Add proof enhancer system with customda enhancers This adds infrastructure to enhance one-step proofs with additional data required by the arbitrator, particularly for custom DA systems. The proof enhancer system intercepts one-step proofs that have an enhancement flag set by the arbitrator. When the arbitrator needs additional data that it cannot access directly (like DA certificates or preimage data), it sets this flag along with a marker byte indicating what type of enhancement is needed. The system includes: - ProofEnhancementManager: Routes proofs to appropriate enhancers based on marker bytes - ReadPreimageProofEnhancer: Handles DA preimage read requests (marker 0xDA) - ValidateCertificateProofEnhancer: Handles certificate validation requests (marker 0xDB) Both enhancers retrieve the certificate from the sequencer message stored in the inbox, then use the daprovider.Validator interface to generate the appropriate proofs. This design allows the arbitrator to request DA operations without needing to store large certificates in its limited WASM memory. The enhanced proofs are then sent to the OSP (on-chain prover) which can verify them against the actual DA system's validation logic. * Add more comments explaining proof enhancement * ProofMarker byte type alias * Remove hardcoded values * Move proof enhancer to its own package * Convenience method for creating custom DA proof enhancers * add comments about enhancement flags * Full ReferenceDA implementation This commit moves the ReferenceDAProofValidator contract and tests from nitro-contracts to contracts-local, as this is a reference implementation that doesn't need to be part of the core nitro-contracts package. The solidity contract was already reviewed in OffchainLabs/nitro-contracts#357 Since the Reference DA contract is now available, this commit activates contract-based certificate validation by uncommenting the ValidateWithContract calls in certificate.go, reference_reader.go, and reference_validator.go. These were previously disabled with TODO comments waiting for contract merge. This commit also includes some changes required for nitro-testnode to work in CustomDA mode with Reference DA. It Ensures contracts are available in Docker builds by copying both contracts/ and contracts-local/ directories. It also adds ReferenceDA signing key to config dump exclusion list to prevent accidental exposure of private keys. This change was merged into the custom-da branch in: #3803 Other changes required that were needed for the standalone daprovider to work with nitro-testnode were: - New parent-chain-node-url and parent-chain-connection-attempts config - L1 client creation in daprovider startup for ReferenceDA mode This change was merged into the custom-da branch in: #3819 * Add ProviderType byte to ReferenceDA certificate This shows how different custom DA providers can distinguish themselves by using a byte after the DACertificateMessageHeaderFlag which identifies the certificate as coming from some custom DA system.
1 parent 15e82c9 commit 56f5c6d

File tree

10 files changed

+849
-68
lines changed

10 files changed

+849
-68
lines changed

Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,8 @@ COPY --from=node-builder /workspace/target/bin/bidder-client /usr/local/bin/
331331
COPY --from=node-builder /workspace/target/bin/el-proxy /usr/local/bin/
332332
COPY --from=node-builder /workspace/target/bin/datool /usr/local/bin/
333333
COPY --from=node-builder /workspace/target/bin/genesis-generator /usr/local/bin/
334+
COPY --from=contracts-builder /workspace/contracts/ /contracts/
335+
COPY --from=contracts-builder /workspace/contracts-local/ /contracts-local/
334336
COPY --from=nitro-legacy /home/user/target/machines /home/user/nitro-legacy/machines
335337
RUN rm -rf /workspace/target/legacy-machines/latest
336338
RUN export DEBIAN_FRONTEND=noninteractive && \

cmd/daprovider/daprovider.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ func parseDAProvider(args []string) (*Config, error) {
112112

113113
if config.Conf.Dump {
114114
err = confighelpers.DumpConfig(k, map[string]interface{}{
115-
"anytrust.key.priv-key": "",
115+
"anytrust.key.priv-key": "",
116+
"referenceda.signing-key.private-key": "",
116117
})
117118
if err != nil {
118119
return nil, fmt.Errorf("error removing extra parameters before dump: %w", err)
@@ -232,6 +233,10 @@ func startup() error {
232233
if !config.ReferenceDA.Enable {
233234
return errors.New("--referenceda.enable is required to start a ReferenceDA provider server")
234235
}
236+
l1Client, err = das.GetL1Client(ctx, config.ReferenceDA.ParentChainConnectionAttempts, config.ReferenceDA.ParentChainNodeURL)
237+
if err != nil {
238+
return err
239+
}
235240
}
236241

237242
// Create DA provider factory based on mode

contracts-local/foundry.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ cache_path = 'forge-cache/sol'
77
via_ir = false
88
remappings = ['ds-test/=lib/forge-std/lib/ds-test/src/',
99
'forge-std/=lib/forge-std/src/',
10-
'openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/']
10+
'openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/',
11+
'@nitro-contracts/=../contracts/src/']
1112
fs_permissions = [{ access = "read", path = "./"}]
1213
solc = '0.8.17'
1314
optimizer = true
@@ -23,7 +24,8 @@ cache_path = 'forge-cache/sol'
2324
via_ir = false
2425
remappings = ['ds-test/=lib/forge-std/lib/ds-test/src/',
2526
'forge-std/=lib/forge-std/src/',
26-
'openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/']
27+
'openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/',
28+
'@nitro-contracts/=../contracts/src/']
2729
fs_permissions = [{ access = "read", path = "./"}]
2830
solc = '0.8.24'
2931
optimizer = true

contracts-local/lib/forge-std

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../contracts/lib/forge-std
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// Copyright 2021-2024, Offchain Labs, Inc.
2+
// For license information, see https://github.com/OffchainLabs/nitro-contracts/blob/main/LICENSE
3+
// SPDX-License-Identifier: BUSL-1.1
4+
5+
pragma solidity ^0.8.0;
6+
7+
/* TODO Once the Custom DA contracts branch is merged we can uncomment this
8+
and remove the interface definition below.
9+
import "@nitro-contracts/osp/ICustomDAProofValidator.sol";
10+
*/
11+
interface ICustomDAProofValidator {
12+
function validateReadPreimage(
13+
bytes32 certHash,
14+
uint256 offset,
15+
bytes calldata proof
16+
) external view returns (bytes memory preimageChunk);
17+
18+
function validateCertificate(
19+
bytes calldata proof
20+
) external view returns (bool isValid);
21+
}
22+
23+
/**
24+
* @title ReferenceDAProofValidator
25+
* @notice Reference implementation of a CustomDA proof validator
26+
*/
27+
contract ReferenceDAProofValidator is ICustomDAProofValidator {
28+
uint256 private constant CERT_SIZE_LEN = 8;
29+
uint256 private constant CLAIMED_VALID_LEN = 1;
30+
uint256 private constant VERSION_LEN = 1;
31+
uint256 private constant PREIMAGE_SIZE_LEN = 8;
32+
uint256 private constant CERT_HEADER = 0x01;
33+
uint256 private constant PROVIDER_TYPE = 0xFF;
34+
uint256 private constant CERT_TOTAL_LEN = 99;
35+
uint256 private constant PROOF_VERSION = 0x01;
36+
37+
mapping(address => bool) public trustedSigners;
38+
39+
constructor(
40+
address[] memory _trustedSigners
41+
) {
42+
for (uint256 i = 0; i < _trustedSigners.length; i++) {
43+
trustedSigners[_trustedSigners[i]] = true;
44+
}
45+
}
46+
/**
47+
* @notice Validates a ReferenceDA proof and returns the preimage chunk
48+
* @param certHash The keccak256 hash of the certificate (from machine's proven state)
49+
* @param offset The offset into the preimage to read from (from machine's proven state)
50+
* @param proof The proof data: [certSize(8), certificate, version(1), preimageSize(8), preimageData]
51+
* @return preimageChunk The up to 32-byte chunk at the specified offset
52+
*/
53+
54+
function validateReadPreimage(
55+
bytes32 certHash,
56+
uint256 offset,
57+
bytes calldata proof
58+
) external pure override returns (bytes memory preimageChunk) {
59+
// Extract certificate size from proof
60+
uint256 certSize = uint256(uint64(bytes8(proof[0:CERT_SIZE_LEN])));
61+
62+
require(proof.length >= CERT_SIZE_LEN + certSize, "Proof too short for certificate");
63+
bytes calldata certificate = proof[CERT_SIZE_LEN:CERT_SIZE_LEN + certSize];
64+
65+
// Verify certificate hash matches what OSP validated
66+
require(keccak256(certificate) == certHash, "Certificate hash mismatch");
67+
68+
// Validate certificate format: [header(1), providerType(1), dataHash(32), v(1), r(32), s(32)] = 99 bytes
69+
// First byte must be 0x01 (CustomDA message header flag)
70+
// Second byte must be 0xFF (ReferenceDA provider type)
71+
require(certificate.length == CERT_TOTAL_LEN, "Invalid certificate length");
72+
require(certificate[0] == bytes1(uint8(CERT_HEADER)), "Invalid certificate header");
73+
require(certificate[1] == bytes1(uint8(PROVIDER_TYPE)), "Invalid provider type");
74+
75+
// Custom data starts after certificate
76+
uint256 customDataStart = CERT_SIZE_LEN + certSize;
77+
require(
78+
proof.length >= customDataStart + VERSION_LEN + PREIMAGE_SIZE_LEN,
79+
"Proof too short for custom data"
80+
);
81+
82+
// Verify version
83+
require(proof[customDataStart] == bytes1(uint8(PROOF_VERSION)), "Unsupported proof version");
84+
85+
// Extract preimage size
86+
uint256 preimageSize = uint256(
87+
uint64(
88+
bytes8(
89+
proof[
90+
customDataStart + VERSION_LEN:
91+
customDataStart + VERSION_LEN + PREIMAGE_SIZE_LEN
92+
]
93+
)
94+
)
95+
);
96+
97+
require(
98+
proof.length >= customDataStart + VERSION_LEN + PREIMAGE_SIZE_LEN + preimageSize,
99+
"Invalid proof length"
100+
);
101+
102+
// Extract and verify preimage against sha256sum in the certificate
103+
bytes calldata preimage = proof[
104+
customDataStart + VERSION_LEN + PREIMAGE_SIZE_LEN:
105+
customDataStart + VERSION_LEN + PREIMAGE_SIZE_LEN + preimageSize
106+
];
107+
bytes32 dataHashFromCert = bytes32(certificate[2:34]);
108+
require(sha256(preimage) == dataHashFromCert, "Invalid preimage hash");
109+
110+
// Extract chunk at offset, matching the behavior of other preimage types
111+
// Returns up to 32 bytes from the specified offset
112+
uint256 preimageEnd = offset + 32;
113+
if (preimageEnd > preimage.length) {
114+
preimageEnd = preimage.length;
115+
}
116+
117+
if (offset >= preimage.length) {
118+
return new bytes(0);
119+
}
120+
121+
return preimage[offset:preimageEnd];
122+
}
123+
124+
/**
125+
* @notice Validates whether a certificate is well-formed and legitimate
126+
* @dev The proof format is: [certSize(8), certificate, claimedValid(1), validityProof...]
127+
* For ReferenceDA, the validityProof is simply a version byte (0x01).
128+
* Other DA providers can include more complex validity proofs after the claimedValid byte,
129+
* such as cryptographic signatures, merkle proofs, or other verification data.
130+
*
131+
* Return vs Revert behavior:
132+
* - Reverts when:
133+
* - Proof is malformed (checked in this function)
134+
* - Provided cert matches proven hash in the instruction (checked in hostio)
135+
* - Claimed valid but is invalid and vice versa (checked in hostio)
136+
* - Returns false when:
137+
* - Certificate is malformed, including wrong length
138+
* - Signature is malformed
139+
* - Signer is not a trustedSigner
140+
* - Returns true when:
141+
* - Signer is a trustedSigner and certificate is valid
142+
*
143+
* @param proof The proof data starting with [certSize(8), certificate, claimedValid(1), validityProof...]
144+
* @return isValid True if the certificate is valid, false otherwise
145+
*/
146+
function validateCertificate(
147+
bytes calldata proof
148+
) external view override returns (bool isValid) {
149+
// Extract certificate size
150+
require(proof.length >= CERT_SIZE_LEN, "Proof too short");
151+
152+
uint256 certSize = uint256(uint64(bytes8(proof[0:CERT_SIZE_LEN])));
153+
154+
// Check we have enough data for certificate and validity proof
155+
require(
156+
proof.length >= CERT_SIZE_LEN + certSize + CLAIMED_VALID_LEN + VERSION_LEN,
157+
"Proof too short for cert and validity"
158+
);
159+
160+
bytes calldata certificate = proof[CERT_SIZE_LEN:CERT_SIZE_LEN + certSize];
161+
162+
// Certificate format is: [header(1), providerType(1), dataHash(32), v(1), r(32), s(32)] = 99 bytes total
163+
// First byte must be 0x01 (CustomDA message header flag)
164+
// Second byte must be 0xFF (ReferenceDA provider type)
165+
// Note: We return false for invalid certificates instead of reverting
166+
// because the certificate is already onchain. An honest validator must be able
167+
// to win a challenge to prove that ValidatePreImage should return false
168+
// so that an invalid cert can be skipped.
169+
if (certificate.length != CERT_TOTAL_LEN) {
170+
return false; // Invalid certificate length
171+
}
172+
if (certificate[0] != bytes1(uint8(CERT_HEADER))) {
173+
return false; // Invalid certificate header
174+
}
175+
if (certificate[1] != bytes1(uint8(PROVIDER_TYPE))) {
176+
return false; // Invalid provider type
177+
}
178+
179+
// Extract data hash and signature components
180+
bytes32 dataHash = bytes32(certificate[2:34]);
181+
uint8 v = uint8(certificate[34]);
182+
bytes32 r = bytes32(certificate[35:67]);
183+
bytes32 s = bytes32(certificate[67:99]);
184+
185+
// Recover signer from signature
186+
address signer = ecrecover(dataHash, v, r, s);
187+
188+
// Check if signature is valid (ecrecover returns 0 on invalid signature)
189+
if (signer == address(0)) {
190+
return false;
191+
}
192+
193+
// Check if signer is trusted
194+
if (!trustedSigners[signer]) {
195+
return false;
196+
}
197+
198+
// Check version byte at the end of the proof
199+
// Note: This is a deliberately simple example. A good rule of thumb is that
200+
// anything added to the proof beyond the isValid byte must not be able to cause both
201+
// true and false to be returned from this function, given the same certificate.
202+
uint8 version = uint8(proof[proof.length - VERSION_LEN]);
203+
require(version == PROOF_VERSION, "Invalid proof version");
204+
205+
return true;
206+
}
207+
}

0 commit comments

Comments
 (0)