Skip to content

Commit 4082a5d

Browse files
authored
Merge pull request #101 from iExecBlockchainComputing/feature/check-result
Check result before uploading
2 parents 5df1b0f + df8bb8d commit 4082a5d

File tree

4 files changed

+186
-27
lines changed

4 files changed

+186
-27
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
66

77
### New Features
88
- Remove `nexus.intra.iex.ec` repository. (#94)
9+
- Check result hash before uploading. (#101)
910
### Bug Fixes
1011
- Fix and harmonize `Dockerfile entrypoint` in all Spring Boot applications. (#98)
1112
### Quality

src/main/java/com/iexec/resultproxy/proxy/ProxyController.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,11 @@ public ResponseEntity<String> addResult(@RequestHeader("Authorization") String t
7373
}
7474

7575
String walletAddress = jwtService.getWalletAddressFromJwtString(token);
76-
boolean canUploadResult = proxyService.canUploadResult(model.getChainTaskId(), walletAddress);
77-
78-
// TODO check if the result to be added is the correct result for that task
76+
boolean canUploadResult = proxyService.canUploadResult(
77+
model.getChainTaskId(),
78+
walletAddress,
79+
model.getZip()
80+
);
7981

8082
if (!canUploadResult) {
8183
return ResponseEntity.status(HttpStatus.UNAUTHORIZED.value()).build();

src/main/java/com/iexec/resultproxy/proxy/ProxyService.java

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
package com.iexec.resultproxy.proxy;
1818

1919

20+
import com.iexec.common.result.ComputedFile;
21+
import com.iexec.common.utils.FileHelper;
22+
import com.iexec.common.worker.result.ResultUtils;
23+
import com.iexec.commons.poco.chain.ChainContribution;
2024
import com.iexec.commons.poco.chain.ChainContributionStatus;
2125
import com.iexec.commons.poco.chain.ChainTask;
2226
import com.iexec.commons.poco.chain.ChainTaskStatus;
@@ -27,8 +31,15 @@
2731
import lombok.extern.slf4j.Slf4j;
2832
import org.springframework.stereotype.Service;
2933

34+
import java.io.IOException;
35+
import java.nio.file.Files;
36+
import java.nio.file.Path;
37+
import java.util.Objects;
3038
import java.util.Optional;
3139

40+
import static com.iexec.common.utils.IexecFileHelper.SLASH_IEXEC_OUT;
41+
import static com.iexec.common.utils.IexecFileHelper.readComputedFile;
42+
3243
/**
3344
* Service class to manage all the results. If the result is public, it will be stored on IPFS. If there is a dedicated
3445
* beneficiary, the result will be pushed to mongo.
@@ -47,7 +58,7 @@ public ProxyService(IexecHubService iexecHubService,
4758
}
4859

4960

50-
boolean canUploadResult(String chainTaskId, String walletAddress) {
61+
boolean canUploadResult(String chainTaskId, String walletAddress, byte[] zip) {
5162
if (iexecHubService.isTeeTask(chainTaskId)){
5263
Optional<ChainTask> chainTask = iexecHubService.getChainTask(chainTaskId);//TODO Add requester field to getChainTask
5364
if (chainTask.isEmpty()){
@@ -83,10 +94,64 @@ boolean canUploadResult(String chainTaskId, String walletAddress) {
8394
return false;
8495
}
8596

97+
return isResultValid(chainTaskId, walletAddress, zip);
98+
}
99+
}
100+
101+
/**
102+
* A result for a standard task is considered as valid if:
103+
* <ul>
104+
* <li>It has an associated on-chain contribution
105+
* <li>The on-chain contribution result hash is the one we compute here again.
106+
* </ul>
107+
*
108+
* @param chainTaskId ID of the task
109+
* @param walletAddress Address of the uploader
110+
* @param zip Result as a zip
111+
* @return {@literal true} if the result is valid, {@literal false} otherwise.
112+
*/
113+
boolean isResultValid(String chainTaskId, String walletAddress, byte[] zip) {
114+
final String resultFolderPath = getResultFolderPath(chainTaskId);
115+
final String resultZipPath = resultFolderPath + ".zip";
116+
final String zipDestinationPath = resultFolderPath + SLASH_IEXEC_OUT;
117+
try {
118+
final Optional<ChainContribution> oChainContribution = iexecHubService.getChainContribution(chainTaskId, walletAddress);
119+
if (oChainContribution.isEmpty()) {
120+
log.error("Trying to upload result but no on-chain contribution [chainTaskId:{}, uploader:{}]",
121+
chainTaskId, walletAddress);
122+
return false;
123+
}
124+
125+
final String onChainHash = oChainContribution.get().getResultHash();
126+
try {
127+
Files.write(Path.of(resultZipPath), zip);
128+
} catch (IOException e) {
129+
log.error("Can't write result file [chainTaskId:{}, uploader:{}]", chainTaskId, walletAddress);
130+
return false;
131+
}
132+
FileHelper.unZipFile(resultZipPath, zipDestinationPath);
133+
134+
final ComputedFile computedFile = readComputedFile(chainTaskId, zipDestinationPath);
135+
final String resultDigest = ResultUtils.computeWeb2ResultDigest(computedFile, resultFolderPath);
136+
final String computedResultHash = ResultUtils.computeResultHash(chainTaskId, resultDigest);
137+
138+
if (!Objects.equals(computedResultHash, onChainHash)) {
139+
log.error("Trying to upload result but on-chain result hash differs from given hash " +
140+
"[chainTaskId:{}, uploader:{}, onChainHash:{}, computedResultHash:{}]",
141+
chainTaskId, walletAddress, onChainHash, computedResultHash);
142+
return false;
143+
}
144+
86145
return true;
146+
} finally {
147+
FileHelper.deleteFolder(resultFolderPath);
87148
}
88149
}
89150

151+
String getResultFolderPath(String chainTaskId) {
152+
return "/tmp/" + chainTaskId;
153+
}
154+
90155
boolean isResultFound(String chainTaskId) {
91156
return ipfsResultService.doesResultExist(chainTaskId);
92157
}
Lines changed: 114 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,154 @@
11
package com.iexec.resultproxy.proxy;
22

3-
import static org.assertj.core.api.Assertions.assertThat;
4-
import static org.mockito.ArgumentMatchers.any;
5-
import static org.mockito.Mockito.when;
6-
3+
import com.iexec.commons.poco.chain.ChainContribution;
74
import com.iexec.resultproxy.chain.IexecHubService;
85
import com.iexec.resultproxy.ipfs.IpfsResultService;
9-
106
import org.junit.jupiter.api.BeforeEach;
117
import org.junit.jupiter.api.Test;
8+
import org.junit.jupiter.api.io.TempDir;
129
import org.mockito.InjectMocks;
1310
import org.mockito.Mock;
1411
import org.mockito.MockitoAnnotations;
12+
import org.mockito.Spy;
13+
14+
import java.io.File;
15+
import java.util.Base64;
16+
import java.util.Optional;
17+
18+
import static com.iexec.commons.poco.chain.ChainContributionStatus.REVEALED;
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.mockito.Mockito.*;
1521

1622
class ProxyServiceTest {
23+
private static final String CHAIN_TASK_ID = "0x59d9b6c36d6db89bae058ff55de6e4d6a6f6e0da3f9ea02297fc8d6d5f5cedf1";
24+
private static final String WALLET_ADDRESS = "0x123abc";
25+
/**
26+
* Contains a valid result zip, with the following files:
27+
* <ul>
28+
* <li>{@literal computed.json}, pointing on `/iexec_out/result.txt`
29+
* <li>{@literal result.txt}
30+
* </ul>
31+
*/
32+
private static final byte[] RESULT_ZIP = Base64.getDecoder().decode("UEsDBBQACAgIADhKL1cAAAAAAAAAAAAAAAANAAAAY29tcHV0ZWQuanNvbqtWSkktSS3KzczLLC7JTNbNLy0pKC3RLUgsyVCyUlDSz0ytSE2OB4rqF6UWl+aU6JVUlCjVAgBQSwcIv08alTcAAAA2AAAAUEsDBBQACAgIADhKL1cAAAAAAAAAAAAAAAAKAAAAcmVzdWx0LnR4dG2NsQrDMAxEd33FdbOhoL1boUM/wnCE4MFg2qEZOvjjK9tKhhIbW9LTnQQC6G8ET7DX/5CQhnlJHol3FQkGAuOAU9ENPaqZE45k6h0NSC/N0Ff231DzgW3qbWxqDqeCZuDYkfpCehIQoeYHEtMOR4NRcHKantF55JlrfV9xr2XNF5HHsi2fvCFoyd+8srw03iDyA1BLBwgqyEkSkwAAAE0BAABQSwECFAAUAAgICAA4Si9Xv08alTcAAAA2AAAADQAAAAAAAAAAAAAAAAAAAAAAY29tcHV0ZWQuanNvblBLAQIUABQACAgIADhKL1cqyEkSkwAAAE0BAAAKAAAAAAAAAAAAAAAAAHIAAAByZXN1bHQudHh0UEsFBgAAAAACAAIAcwAAAD0BAAAAAA==");
33+
/**
34+
* {@link ChainContribution} with a hash
35+
* corresponding to {@link ProxyServiceTest#RESULT_ZIP} hash,
36+
* including a fixed ChainTask ID: {@literal 0x59d9b6c36d6db89bae058ff55de6e4d6a6f6e0da3f9ea02297fc8d6d5f5cedf1}.
37+
*/
38+
private static final ChainContribution CHAIN_CONTRIBUTION = ChainContribution.builder()
39+
.resultHash("0x865e1ebff87de7928040a42383b46690a12a988b278eb880e0e641f5da3cc9d1")
40+
.build();
41+
42+
@TempDir
43+
File tmpFolder;
1744

1845
@Mock
1946
private IexecHubService iexecHubService;
2047

2148
@Mock
2249
private IpfsResultService ipfsResultService;
2350

24-
51+
@Spy
2552
@InjectMocks
2653
private ProxyService proxyService;
2754

28-
private String chainTaskId;
29-
private String walletAddress;
3055

3156
@BeforeEach
3257
void init() {
3358
MockitoAnnotations.openMocks(this);
34-
chainTaskId = "0x1";
35-
walletAddress = "0x123abc";
59+
}
60+
61+
@Test
62+
void isNotAbleToUploadSinceNoChainContribution() {
63+
when(iexecHubService.isTeeTask(CHAIN_TASK_ID)).thenReturn(false);
64+
when(ipfsResultService.doesResultExist(CHAIN_TASK_ID)).thenReturn(false);
65+
when(iexecHubService.isStatusTrueOnChain(CHAIN_TASK_ID, WALLET_ADDRESS, REVEALED)).thenReturn(true);
66+
when(iexecHubService.getChainContribution(CHAIN_TASK_ID, WALLET_ADDRESS)).thenReturn(Optional.empty());
67+
68+
assertThat(proxyService.canUploadResult(CHAIN_TASK_ID, WALLET_ADDRESS, RESULT_ZIP)).isFalse();
69+
70+
verify(iexecHubService).isTeeTask(CHAIN_TASK_ID);
71+
verify(proxyService).isResultFound(CHAIN_TASK_ID);
72+
verify(iexecHubService).isStatusTrueOnChain(CHAIN_TASK_ID, WALLET_ADDRESS, REVEALED);
73+
verify(iexecHubService).getChainContribution(CHAIN_TASK_ID, WALLET_ADDRESS);
74+
}
75+
76+
@Test
77+
void isNotAbleToUploadSinceCannotWriteZip() {
78+
when(iexecHubService.isTeeTask(CHAIN_TASK_ID)).thenReturn(false);
79+
when(ipfsResultService.doesResultExist(CHAIN_TASK_ID)).thenReturn(false);
80+
when(iexecHubService.isStatusTrueOnChain(CHAIN_TASK_ID, WALLET_ADDRESS, REVEALED)).thenReturn(true);
81+
when(iexecHubService.getChainContribution(CHAIN_TASK_ID, WALLET_ADDRESS)).thenReturn(Optional.of(CHAIN_CONTRIBUTION));
82+
when(proxyService.getResultFolderPath(CHAIN_TASK_ID)).thenReturn("/this/path/does/not/exist");
83+
84+
assertThat(proxyService.canUploadResult(CHAIN_TASK_ID, WALLET_ADDRESS, RESULT_ZIP)).isFalse();
85+
86+
verify(iexecHubService).isTeeTask(CHAIN_TASK_ID);
87+
verify(proxyService).isResultFound(CHAIN_TASK_ID);
88+
verify(iexecHubService).isStatusTrueOnChain(CHAIN_TASK_ID, WALLET_ADDRESS, REVEALED);
89+
verify(iexecHubService).getChainContribution(CHAIN_TASK_ID, WALLET_ADDRESS);
90+
}
91+
92+
@Test
93+
void isNotAbleToUploadSinceWrongHash() {
94+
when(iexecHubService.isTeeTask(CHAIN_TASK_ID)).thenReturn(false);
95+
when(ipfsResultService.doesResultExist(CHAIN_TASK_ID)).thenReturn(false);
96+
when(iexecHubService.isStatusTrueOnChain(CHAIN_TASK_ID, WALLET_ADDRESS, REVEALED)).thenReturn(true);
97+
when(iexecHubService.getChainContribution(CHAIN_TASK_ID, WALLET_ADDRESS)).thenReturn(Optional.of(CHAIN_CONTRIBUTION));
98+
when(proxyService.getResultFolderPath(CHAIN_TASK_ID)).thenReturn(tmpFolder.getAbsolutePath());
99+
100+
assertThat(proxyService.canUploadResult(CHAIN_TASK_ID, WALLET_ADDRESS, new byte[] {})).isFalse();
101+
102+
verify(iexecHubService).isTeeTask(CHAIN_TASK_ID);
103+
verify(proxyService).isResultFound(CHAIN_TASK_ID);
104+
verify(iexecHubService).isStatusTrueOnChain(CHAIN_TASK_ID, WALLET_ADDRESS, REVEALED);
105+
verify(iexecHubService).getChainContribution(CHAIN_TASK_ID, WALLET_ADDRESS);
36106
}
37107

38108
@Test
39109
void isNotAbleToUploadSinceResultAlreadyExistsWithIpfs() {
40-
when(iexecHubService.isPublicResult(chainTaskId, 0)).thenReturn(true);
41-
when(ipfsResultService.doesResultExist(chainTaskId)).thenReturn(true);
110+
when(iexecHubService.isTeeTask(CHAIN_TASK_ID)).thenReturn(false);
111+
when(ipfsResultService.doesResultExist(CHAIN_TASK_ID)).thenReturn(true);
112+
when(iexecHubService.getChainContribution(CHAIN_TASK_ID, WALLET_ADDRESS)).thenReturn(Optional.of(CHAIN_CONTRIBUTION));
113+
when(proxyService.getResultFolderPath(CHAIN_TASK_ID)).thenReturn(tmpFolder.getAbsolutePath());
114+
115+
assertThat(proxyService.canUploadResult(CHAIN_TASK_ID, WALLET_ADDRESS, RESULT_ZIP)).isFalse();
42116

43-
assertThat(proxyService.canUploadResult(chainTaskId, walletAddress)).isFalse();
117+
verify(iexecHubService).isTeeTask(CHAIN_TASK_ID);
118+
verify(proxyService).isResultFound(CHAIN_TASK_ID);
119+
verify(iexecHubService, never()).isStatusTrueOnChain(CHAIN_TASK_ID, WALLET_ADDRESS, REVEALED);
120+
verify(iexecHubService, never()).getChainContribution(CHAIN_TASK_ID, WALLET_ADDRESS);
44121
}
45122

46123
@Test
47124
void isNotAbleToUploadSinceChainStatusIsNotRevealedWithIpfs() {
48-
when(iexecHubService.isPublicResult(chainTaskId, 0)).thenReturn(true);
49-
when(ipfsResultService.doesResultExist(chainTaskId)).thenReturn(true);
50-
when(iexecHubService.isStatusTrueOnChain(any(), any(), any())).thenReturn(false);
51-
52-
assertThat(proxyService.canUploadResult(chainTaskId, walletAddress)).isFalse();
125+
when(iexecHubService.isTeeTask(CHAIN_TASK_ID)).thenReturn(false);
126+
when(ipfsResultService.doesResultExist(CHAIN_TASK_ID)).thenReturn(true);
127+
when(iexecHubService.isStatusTrueOnChain(CHAIN_TASK_ID, WALLET_ADDRESS, REVEALED)).thenReturn(false);
128+
when(iexecHubService.getChainContribution(CHAIN_TASK_ID, WALLET_ADDRESS)).thenReturn(Optional.of(CHAIN_CONTRIBUTION));
129+
when(proxyService.getResultFolderPath(CHAIN_TASK_ID)).thenReturn(tmpFolder.getAbsolutePath());
130+
131+
assertThat(proxyService.canUploadResult(CHAIN_TASK_ID, WALLET_ADDRESS, RESULT_ZIP)).isFalse();
132+
133+
verify(iexecHubService).isTeeTask(CHAIN_TASK_ID);
134+
verify(proxyService).isResultFound(CHAIN_TASK_ID);
135+
verify(iexecHubService, never()).isStatusTrueOnChain(CHAIN_TASK_ID, WALLET_ADDRESS, REVEALED);
136+
verify(iexecHubService, never()).getChainContribution(CHAIN_TASK_ID, WALLET_ADDRESS);
53137
}
54138

55139
@Test
56140
void isAbleToUploadWithIpfs() {
57-
when(iexecHubService.isPublicResult(chainTaskId, 0)).thenReturn(true);
58-
when(ipfsResultService.doesResultExist(chainTaskId)).thenReturn(false);
59-
when(iexecHubService.isStatusTrueOnChain(any(), any(), any())).thenReturn(true);
60-
61-
assertThat(proxyService.canUploadResult(chainTaskId, walletAddress)).isTrue();
141+
when(iexecHubService.isTeeTask(CHAIN_TASK_ID)).thenReturn(false);
142+
when(ipfsResultService.doesResultExist(CHAIN_TASK_ID)).thenReturn(false);
143+
when(iexecHubService.isStatusTrueOnChain(CHAIN_TASK_ID, WALLET_ADDRESS, REVEALED)).thenReturn(true);
144+
when(iexecHubService.getChainContribution(CHAIN_TASK_ID, WALLET_ADDRESS)).thenReturn(Optional.of(CHAIN_CONTRIBUTION));
145+
when(proxyService.getResultFolderPath(CHAIN_TASK_ID)).thenReturn(tmpFolder.getAbsolutePath());
146+
147+
assertThat(proxyService.canUploadResult(CHAIN_TASK_ID, WALLET_ADDRESS, RESULT_ZIP)).isTrue();
148+
149+
verify(iexecHubService).getChainContribution(CHAIN_TASK_ID, WALLET_ADDRESS);
150+
verify(iexecHubService).isTeeTask(CHAIN_TASK_ID);
151+
verify(proxyService).isResultFound(CHAIN_TASK_ID);
152+
verify(iexecHubService).isStatusTrueOnChain(CHAIN_TASK_ID, WALLET_ADDRESS, REVEALED);
62153
}
63154
}

0 commit comments

Comments
 (0)