Skip to content

Commit 23cfb8a

Browse files
authored
Support multiple scoring chains (#39)
## Motivation Add flexibility in how we use scoring chain(s) ## Proposal * Support several scoring chains * Let frontends pick the one their user should use (possibly none) ## Test Plan CI + manual tests
1 parent 7f795a3 commit 23cfb8a

File tree

5 files changed

+49
-48
lines changed

5 files changed

+49
-48
lines changed

backend/README.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ CHAIN="${INFO[0]}"
4343
OWNER="${INFO[1]}"
4444
```
4545

46-
### Creating the scoring chain
46+
### Creating the scoring chains
4747

48-
Let's create another wallet and the scoring chain. The app wallet will be accessible with `linera -w1`.
48+
Let's create another wallet and two scoring chains. The app wallet will be accessible with `linera -w1`.
4949

5050
```bash
5151
export LINERA_WALLET_1="$LINERA_TMP_DIR/wallet_1.json"
@@ -57,6 +57,10 @@ linera -w1 wallet init --faucet $FAUCET_URL
5757
INFO_1=($(linera -w1 wallet request-chain --faucet $FAUCET_URL))
5858
CHAIN_1="${INFO_1[0]}"
5959
OWNER_1="${INFO_1[1]}"
60+
61+
INFO_2=($(linera -w1 wallet request-chain --faucet $FAUCET_URL))
62+
CHAIN_2="${INFO_1[0]}"
63+
OWNER_2="${INFO_1[1]}"
6064
```
6165

6266
### Creating the GoL challenge application
@@ -66,8 +70,7 @@ node service.
6670

6771
```bash
6872
APP_ID=$(linera --wait-for-outgoing-messages \
69-
project publish-and-create backend gol_challenge $CHAIN \
70-
--json-parameters "{ \"scoring_chain_id\": \"$CHAIN_1\" }")
73+
project publish-and-create backend gol_challenge $CHAIN)
7174
```
7275

7376
### Creating a new puzzle
@@ -80,18 +83,18 @@ BLOB_ID=$(linera publish-data-blob "$LINERA_TMP_DIR/02_beehive_pattern_puzzle.bc
8083

8184
### Publishing puzzles and running code-generation
8285

83-
Run the node service for the scoring chain.
86+
Run the node service for the scoring chains.
8487
```bash
8588
linera -w1 service --port 8081 &
8689
sleep 1
8790
```
8891

8992
The following script creates puzzles with the `gol` tool, then it uses the user wallet to
90-
publish them. At the same time, it also sends GraphQL queries to the scoring chain to register
93+
publish them. At the same time, it also sends GraphQL queries to the scoring chain(s) to register
9194
the puzzles.
9295

9396
```bash
94-
./publish-puzzles.sh http://localhost:8081/chains/$CHAIN_1/applications/$APP_ID
97+
./publish-puzzles.sh http://localhost:8081/chains/$CHAIN_1/applications/$APP_ID http://localhost:8081/chains/$CHAIN_2/applications/$APP_ID
9598
```
9699

97100
Note that we never unregister puzzles.
@@ -121,7 +124,7 @@ query {
121124

122125
```gql,uri=http://localhost:8080/chains/$CHAIN/applications/$APP_ID
123126
mutation {
124-
submitSolution(puzzleId: "$BLOB_ID", board: {
127+
submitSolution(puzzleId: "$BLOB_ID", scoringChainId: "$CHAIN_1", board: {
125128
size: 9,
126129
liveCells: [{x: 3, y: 2}, {x: 4, y: 2}, {x: 2, y: 3}, {x: 5, y: 3}, {x: 3, y: 4}, {x: 4, y: 4}]
127130
})
@@ -133,6 +136,7 @@ mutation {
133136
To debug GraphQL APIs, uncomment the line with `read` and run `bash -x -e <(linera extract-script-from-markdown backend/README.md)`.
134137
```bash
135138
echo http://localhost:8081/chains/$CHAIN_1/applications/$APP_ID
139+
echo http://localhost:8081/chains/$CHAIN_2/applications/$APP_ID
136140
# read
137141
```
138142

@@ -161,6 +165,7 @@ Restart the service with the scoring chain followed in read-only:
161165
kill $PID
162166

163167
linera wallet follow-chain "$CHAIN_1"
168+
linera wallet follow-chain "$CHAIN_2"
164169

165170
linera service --port 8080 &
166171
```

backend/src/contract.rs

Lines changed: 25 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ mod state;
88
use async_graphql::ComplexObject;
99
use gol_challenge::{game::Puzzle, GolChallengeAbi, Operation};
1010
use linera_sdk::{
11-
linera_base_types::{AccountOwner, ChainId, DataBlobHash, Timestamp, WithContractAbi},
11+
linera_base_types::{AccountOwner, DataBlobHash, Timestamp, WithContractAbi},
1212
views::{RootView, View},
1313
Contract, ContractRuntime,
1414
};
@@ -22,12 +22,6 @@ pub struct GolChallengeContract {
2222

2323
linera_sdk::contract!(GolChallengeContract);
2424

25-
#[derive(Clone, Debug, Serialize, Deserialize)]
26-
pub struct Parameters {
27-
/// Where to report puzzles for scoring.
28-
scoring_chain_id: ChainId,
29-
}
30-
3125
impl WithContractAbi for GolChallengeContract {
3226
type Abi = GolChallengeAbi;
3327
}
@@ -45,7 +39,7 @@ pub struct Message {
4539
impl Contract for GolChallengeContract {
4640
type Message = Message;
4741
type InstantiationArgument = ();
48-
type Parameters = Parameters;
42+
type Parameters = ();
4943
type EventValue = ();
5044

5145
async fn load(runtime: ContractRuntime<Self>) -> Self {
@@ -68,6 +62,7 @@ impl Contract for GolChallengeContract {
6862
puzzle_id,
6963
board,
7064
owner,
65+
scoring_chain_id,
7166
} => {
7267
let owner = owner.unwrap_or_else(|| {
7368
self.runtime
@@ -88,18 +83,19 @@ impl Contract for GolChallengeContract {
8883
.insert(&puzzle_id, solution)
8984
.expect("Store solution");
9085

91-
let Parameters { scoring_chain_id } = self.runtime.application_parameters();
92-
let message = Message {
93-
puzzle_id,
94-
timestamp,
95-
owner,
96-
};
97-
self.runtime
98-
.prepare_message(message)
99-
.send_to(scoring_chain_id);
86+
if let Some(scoring_chain_id) = scoring_chain_id {
87+
let message = Message {
88+
puzzle_id,
89+
timestamp,
90+
owner,
91+
};
92+
self.runtime
93+
.prepare_message(message)
94+
.send_to(scoring_chain_id);
95+
}
10096
}
10197
Operation::RegisterPuzzle { puzzle_id } => {
102-
self.assert_scoring_chain("Puzzles are only registered on the scoring chain.");
98+
// Puzzles are only registered on a scoring chain.
10399
self.state.registered_puzzles.insert(&puzzle_id).unwrap();
104100
}
105101
}
@@ -112,35 +108,30 @@ impl Contract for GolChallengeContract {
112108
timestamp,
113109
owner,
114110
} = message;
115-
self.assert_scoring_chain("Messages are sent to the scoring chain.");
116111
let is_registered = self
117112
.state
118113
.registered_puzzles
119114
.contains(&puzzle_id)
120115
.await
121116
.unwrap();
122-
assert!(is_registered, "Puzzle must be registered");
123-
let map = self
124-
.state
125-
.reported_solutions
126-
.load_entry_mut(&owner)
127-
.await
128-
.unwrap();
129-
map.insert(&puzzle_id, timestamp).unwrap();
117+
if is_registered {
118+
let map = self
119+
.state
120+
.reported_solutions
121+
.load_entry_mut(&owner)
122+
.await
123+
.unwrap();
124+
map.insert(&puzzle_id, timestamp).unwrap();
125+
} else {
126+
log::trace!("Ignoring unregistered puzzle");
127+
}
130128
}
131129

132130
async fn store(mut self) {
133131
self.state.save().await.expect("Failed to save state");
134132
}
135133
}
136134

137-
impl GolChallengeContract {
138-
fn assert_scoring_chain(&mut self, error: &str) {
139-
let Parameters { scoring_chain_id } = self.runtime.application_parameters();
140-
assert_eq!(self.runtime.chain_id(), scoring_chain_id, "{}", error);
141-
}
142-
}
143-
144135
/// This implementation is only nonempty in the service.
145136
#[ComplexObject]
146137
impl GolChallengeState {}

backend/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ pub mod game;
1111
use async_graphql::{Request, Response};
1212
use linera_sdk::{
1313
graphql::GraphQLMutationRoot,
14-
linera_base_types::{AccountOwner, ContractAbi, DataBlobHash, ServiceAbi},
14+
linera_base_types::{AccountOwner, ChainId, ContractAbi, DataBlobHash, ServiceAbi},
1515
};
1616
use serde::{Deserialize, Serialize};
1717

@@ -31,6 +31,8 @@ pub enum Operation {
3131
board: Board,
3232
/// Optional owner to credit instead of the current authenticated owner.
3333
owner: Option<AccountOwner>,
34+
/// The scoring chain to use.
35+
scoring_chain_id: Option<ChainId>,
3436
},
3537
// Scoring appchain only
3638
/// Register a puzzle to activate scoring for it.

backend/src/state.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ use serde::{Deserialize, Serialize};
1414
#[graphql(complex)]
1515
#[view(context = ViewStorageContext)]
1616
pub struct GolChallengeState {
17+
// User chains only.
1718
/// The local solutions previously submitted by an owner of the chain. Puzzles do not
1819
/// need to be registered.
1920
pub solutions: MapView<DataBlobHash, Solution>,
2021

21-
// Scoring chain only.
22+
// Scoring chains only.
2223
/// The set of registered puzzles.
2324
pub registered_puzzles: SetView<DataBlobHash>,
2425
/// The set of all solutions reported to us, indexed by owner, then by puzzle_id. We only track

publish-puzzles.sh

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55

66
set -e
77

8-
# Expecting some URI to query the node service of the scoring chain.
8+
# Expecting some URIs to query the node service of the scoring chain.
99
# If absent, puzzles will be published but not registered for scoring.
10-
URI="$1"
10+
URIS=("$@")
11+
12+
echo "Using ${#URIS[@]} scoring chains"
1113

1214
# Create temporary directory
1315
TEMP_DIR=$(mktemp -d)
@@ -63,12 +65,12 @@ for i in "${!PUZZLE_FILES[@]}"; do
6365
echo " \"$PUZZLE\": \"$BLOB_ID\"," >> "$BLOB_MAP_FILE"
6466
fi
6567

66-
if [ -n "$URI" ]; then
68+
for URI in "${URIS[@]}"; do
6769
QUERY="mutation { registerPuzzle(puzzleId: \"$BLOB_ID\")}"
6870
JSON_QUERY=$( jq -n --arg q "$QUERY" '{"query": $q}' )
6971
# Use js to parse the result and fail otherwise.
7072
curl -w '\n' -g -X POST -H "Content-Type: application/json" -d "$JSON_QUERY" "$URI" | tee /dev/stderr | jq -e .data
71-
fi
73+
done
7274
else
7375
echo " Error: Could not extract blob ID for $PUZZLE"
7476
exit 1

0 commit comments

Comments
 (0)