Access to data of "divulged" contracts

Background

Can you double-check me on this?

I created a Daml-based number guessing game. Ala, “I’m thinking of a number between 1 and 100.” I’m wondering if the Player can cheat and peek at Owner’s contract that holds my secret number.

Details

Here is the Daml model, in case you are interested in following along the details. (The game play consists of alternating back-and-forth between a PlayersTurn and a OwnersTurn.)

Daml model
module DivulgedTest where

import Daml.Script

template SecretNumber
  with
    owner : Party
    secret : Int
  where
    signatory owner
    nonconsuming choice GetHint : Text
      with guess : Int
      controller owner
      do
        let msg | guess < secret = "Too low"
                | guess > secret = "Too high"
                | otherwise = "You guessed it!"
        return msg

template PlayersTurn
  with
    owner : Party
    player : Party
    secretNumberId : ContractId SecretNumber
    response : Text
  where
    signatory owner
    observer player
    choice Guess : ContractId OwnersTurn
      with guess : Int
      controller player
      do create OwnersTurn with
          owner, player, secretNumberId, guess

template OwnersTurn
  with
    owner : Party
    player : Party
    secretNumberId : ContractId SecretNumber
    guess : Int
  where
    signatory player
    observer owner
    choice Respond : ContractId PlayersTurn
      controller owner
      do
        response <- exercise secretNumberId $ GetHint guess
        create PlayersTurn with
          owner, player, secretNumberId, response

test = script do
  owner <- allocatePartyWithHint "Owner" (PartyIdHint "Owner")
  player <- allocatePartyWithHint "Player" (PartyIdHint "Player")

  secret <- submit owner do
    createCmd SecretNumber with owner, secret = 42

  guessTurn0 <- submit owner do
    createCmd PlayersTurn with
      owner = owner
      player = player
      secretNumberId = secret
      response = "Guess a number."
  
  hintTurn0 <- submit player do
    exerciseCmd guessTurn0 Guess with guess = 10

  guessTurn1 <- submit owner do
    exerciseCmd hintTurn0 Respond
  
  lst <- query @SecretNumber player
  debug ("Secret number contracts queryable by player: " <> show lst)

  s <- queryContractId player secret
  debug ("This specific secret number queryied by player: " <> show s)

  return ()

There is a specific reason I’m wondering if the player can cheat. The “Script Results” in Daml Studio shows that my SecretNumber contract has been divulged to the player.

Notice that D in the row with my secret number.

To test, I tried without success to get the secret number for the player using Daml Script.

To further investigate, I’ve used the Ledger API (via grpcui) calling GetTransactionTrees. With the following request body:

gRPC body
{
  "begin": {
    "boundary": "LEDGER_BEGIN"
  },
  "end": {
    "boundary": "LEDGER_END"
  },
  "filter": {
    "filtersByParty": {
      "Player::12201...8f9ed": {}
    }
  },
  "verbose": true
}

Calling GetTransactionTrees returns several GetTransactionTreesResponses.

  • The transaction that created the SecretNumber is not returned for the player.
  • The later transaction that divulged to the player is returned. However, I do not see the actual secret number anywhere in that gRPC response.

Here is that transaction, returned by the call to GetTransactionTrees. (For reference, the divulgence occurs when the Owner exercises the nonconsuming choice to GetHint from the SecretNumber. It is the first child event.)

divulging transaction
{
  "transactions": [
    {
      "transaction_id": "12201...68542",
      "command_id": "",
      "workflow_id": "",
      "effective_at": "2022-11-22T15:22:40.659871Z",
      "offset": "00000000000000000e",
      "events_by_id": {
        "#12201...68542:0": {
          "exercised": {
            "event_id": "#12201...68542:0",
            "contract_id": "007fe...d6bf5",
            "template_id": {
              "package_id": "758d9...f0df4",
              "module_name": "DivulgedTest",
              "entity_name": "OwnersTurn"
            },
            "choice": "Respond",
            "choice_argument": {
              "record": {
                "record_id": {
                  "package_id": "758d9...f0df4",
                  "module_name": "DivulgedTest",
                  "entity_name": "Respond"
                },
                "fields": []
              }
            },
            "acting_parties": [
              "Owner::12201...8f9ed"
            ],
            "consuming": true,
            "witness_parties": [
              "Player::12201...8f9ed"
            ],
            "child_event_ids": [
              "#12201...68542:1",
              "#12201...68542:2"
            ],
            "exercise_result": {
              "contract_id": "00c13...789bb"
            },
            "interface_id": null
          }
        },
        "#12201...68542:1": {
          "exercised": {
            "event_id": "#12201...68542:1",
            "contract_id": "0045f...752b5",
            "template_id": {
              "package_id": "758d9...f0df4",
              "module_name": "DivulgedTest",
              "entity_name": "SecretNumber"
            },
            "choice": "GetHint",
            "choice_argument": {
              "record": {
                "record_id": {
                  "package_id": "758d9...f0df4",
                  "module_name": "DivulgedTest",
                  "entity_name": "GetHint"
                },
                "fields": [
                  {
                    "label": "guess",
                    "value": {
                      "int64": "20"
                    }
                  }
                ]
              }
            },
            "acting_parties": [
              "Owner::12201...8f9ed"
            ],
            "consuming": false,
            "witness_parties": [
              "Player::12201...8f9ed"
            ],
            "child_event_ids": [],
            "exercise_result": {
              "text": "Too low"
            },
            "interface_id": null
          }
        },
        "#12201...68542:2": {
          "created": {
            "event_id": "#12201...68542:2",
            "contract_id": "00c13...789bb",
            "template_id": {
              "package_id": "758d9...f0df4",
              "module_name": "DivulgedTest",
              "entity_name": "PlayersTurn"
            },
            "create_arguments": {
              "record_id": {
                "package_id": "758d9...f0df4",
                "module_name": "DivulgedTest",
                "entity_name": "PlayersTurn"
              },
              "fields": [
                {
                  "label": "owner",
                  "value": {
                    "party": "Owner::12201...8f9ed"
                  }
                },
                {
                  "label": "player",
                  "value": {
                    "party": "Player::12201...8f9ed"
                  }
                },
                {
                  "label": "secretNumberId",
                  "value": {
                    "contract_id": "0045f...752b5"
                  }
                },
                {
                  "label": "response",
                  "value": {
                    "text": "Too low"
                  }
                }
              ]
            },
            "witness_parties": [
              "Player::12201...8f9ed"
            ],
            "agreement_text": "",
            "contract_key": null,
            "signatories": [
              "Owner::12201...8f9ed"
            ],
            "observers": [
              "Player::12201...8f9ed"
            ],
            "metadata": null,
            "interface_views": []
          }
        }
      },
      "root_event_ids": [
        "#12201...68542:0"
      ]
    }
  ]
}

Question

My conclusion is that nothing was divulged to the player that would allow him to get the secret number. Is that true?

I suspect that if I had had a nonconsuming choice GetSecretNumber instead of GetHint then that would be a problem.

3 Likes

I have continued working with this example. Specifically, I changed the Daml model in a way that definitely exposes the secret number to the player.

Original Code
-- from OwnersTurn, Respond choice
response <- exercise secretNumberId $ GetHint guess
create PlayersTurn with
    owner, player, secretNumberId, response
Modified Code
-- from OwnersTurn, Respond choice
secret <- exercise secretNumberId $ GetSecret
let response
      | guess < secret = "Too low"
      | guess > secret = "Too high"
      | otherwise = "You guessed it!"
create PlayersTurn with
    owner, player, secretNumberId, response
Transaction Tree (excerpt)
{
  "exercised": {
    "event_id": "#1220d...d3475:1",
    "contract_id": "00642...a8f75",
    "template_id": {
      "package_id": "b540d...01fd3",
      "module_name": "DivulgedTest",
      "entity_name": "SecretNumber"
    },
    "choice": "GetSecret",
    "choice_argument": {
      "record": {
        "record_id": {
          "package_id": "b540d...01fd3",
          "module_name": "DivulgedTest",
          "entity_name": "GetSecret"
        },
        "fields": []
      }
    },
    "acting_parties": [
      "Owner::12202...7a44a"
    ],
    "consuming": false,
    "witness_parties": [
      "Player::12202...7a44a"
    ],
    "child_event_ids": [],
    "exercise_result": {
      "int64": "42"
    },
    "interface_id": null
  }
}

Notice the secret number 42 is available to the player in the transaction tree.

1 Like

What happens if you fetch the secretNumberId inside of Respond, instead of just exercising?

Good question, @Leonid_Rozenberg. So instead of executing a choice (e.g., GetHint or GetSecret) on the SecretNumber contract, just fetch the SecretNumber itself.

I would expect that a fetch will reveal the secret number to the observing player party. Let’s see…

Modified, Modified Code
-- -- from OwnersTurn, Respond choice
secretNumber <- fetch secretNumberId
let secret = secretNumber.secret
let response
      | guess < secret = "Too low"
      | guess > secret = "Too high"
      | otherwise = "You guessed it!"
create PlayersTurn with
    owner, player, secretNumberId, response
Resulting Transaction Tree
{
  "transactions": [
    {
      "transaction_id": "1220b...cf7b1",
      "command_id": "",
      "workflow_id": "",
      "effective_at": "2022-11-23T13:37:13.505661Z",
      "offset": "00000000000000000a",
      "events_by_id": {
        "#1220...cf7b1:0": {
          "exercised": {
            "event_id": "#1220...cf7b1:0",
            "contract_id": "00b0f...dad1f",
            "template_id": {
              "package_id": "27e7c...4e4df",
              "module_name": "DivulgedTest",
              "entity_name": "OwnersTurn"
            },
            "choice": "Respond",
            "choice_argument": {
              "record": {
                "record_id": {
                  "package_id": "27e7c...4e4df",
                  "module_name": "DivulgedTest",
                  "entity_name": "Respond"
                },
                "fields": []
              }
            },
            "acting_parties": [
              "Owner::12201...7deeb"
            ],
            "consuming": true,
            "witness_parties": [
              "Player::12201...7deeb"
            ],
            "child_event_ids": [
              "#1220...cf7b1:2"
            ],
            "exercise_result": {
              "contract_id": "00a4b...fae88"
            },
            "interface_id": null
          }
        },
        "#1220...cf7b1:2": {
          "created": {
            "event_id": "#1220...cf7b1:2",
            "contract_id": "00a4b...fae88",
            "template_id": {
              "package_id": "27e7c...4e4df",
              "module_name": "DivulgedTest",
              "entity_name": "PlayersTurn"
            },
            "create_arguments": {
              "record_id": {
                "package_id": "27e7c...4e4df",
                "module_name": "DivulgedTest",
                "entity_name": "PlayersTurn"
              },
              "fields": [
                {
                  "label": "owner",
                  "value": {
                    "party": "Owner::12201...7deeb"
                  }
                },
                {
                  "label": "player",
                  "value": {
                    "party": "Player::12201...7deeb"
                  }
                },
                {
                  "label": "secretNumberId",
                  "value": {
                    "contract_id": "003ef...e3ca6"
                  }
                },
                {
                  "label": "response",
                  "value": {
                    "text": "Too low"
                  }
                }
              ]
            },
            "witness_parties": [
              "Player::12201...7deeb"
            ],
            "agreement_text": "",
            "contract_key": null,
            "signatories": [
              "Owner::12201...7deeb"
            ],
            "observers": [
              "Player::12201...7deeb"
            ],
            "metadata": null,
            "interface_views": []
          }
        }
      },
      "root_event_ids": [
        "#1220...cf7b1:0"
      ]
    }
  ]
}

I do not see anything in the transaction tree that would give away the secret number. Apparently the result of the fetch is not included in the transaction tree returned by the Ledger API.

I suppose that makes sense in retrospect. The fetch is not a transaction. So it does not appear in the transaction tree.

Is there anywhere else I should be looking besides the transaction tree? Can the player still cheat in some way that I don’t know about yet?

Information that is divulged is now physically on servers controlled by the divulgee, so yes as long as the contract containing the secret is divulged to the player the player can cheat. Even if the ledger API does not provide access, the databases on the participant node, controlled by the player, do. In the deployments I have been involved with this would be a trivial application of pgsql. The only way to keep the secret secret is to ensure you don’t divulge it.

The key documentation you need to read and understand is: Privacy — Daml SDK 2.4.0 documentation

But the TLDR; here is: Signatories see the entire transaction tree or sub-tree rooted in one of the contracts choices. So any access to secrets or other contracts from within a choice will be witnessed by every signatory of the enclosing contract—and there is no way around this that doesn’t undermine Daml’s privacy model. So, any secret you need to keep must be accessed and used in a sibling or ancestor transaction sub-tree. In this case it does mean the Player will not be able to confirm the Owner is not cheating.

My suggestion here is that you keep the Owner signatory on all contracts, and that you are careful to avoid divulgence until a final “you have won” or “you have lost” transaction where you can use divulgence to verify that the Owner didn’t cheat. Probably by storing the contract-id of the secret in the game contract and then fetching it at the very end in a transaction the Player can see.

3 Likes

Thank you, @Andrae for your response! That really helped me expand my understanding of “divulgence” beyond what is visible to query and gRPC.

I managed to build a model that does not divulge the secret until the game is ended (thanks to your suggestion.)

Here are relevant excerpts of my solution. Notice that the secret number is not divulged (via a fetch) until the owner chooses to end the game.

template SecretNumber
          :
    nonconsuming choice GiveHint : Either PlayersTurnId GameOverId
        controller owner
          :
template PlayersTurn
          :
   choice Guess : PlayersGuessId
      controller player
          :
template PlayersGuess
          :
    choice GuessAgain : PlayersTurnId
      controller owner
          :

    choice EndGame : GameOverId
      with msg : Text
      controller owner
      do
        secretNumber <- fetch secretNumberId
        archive secretNumberId
        create GameOver with
          secret = secretNumber.secret
          lastGuess = guess
          ..

Full Daml model
module DivulgedTest2 where

import Daml.Script

type SecretNumberId = ContractId SecretNumber
type PlayersTurnId = ContractId PlayersTurn
type PlayersGuessId = ContractId PlayersGuess
type GameOverId = ContractId GameOver

template SecretNumber
  with
    owner: Party
    player: Party
    secret : Int
  where
    signatory owner

    nonconsuming choice StartGame : PlayersTurnId
      with allowedGuesses: Int
      controller owner
      do create PlayersTurn with
            secretNumberId = self
            lastGuess = None
            remainingGuesses = allowedGuesses
            msg = "Guess a number"
            ..

    nonconsuming choice GiveHint : Either PlayersTurnId GameOverId
      with playersGuessId: PlayersGuessId
      controller owner
      do
        playersGuess <- fetch playersGuessId
        let msg | playersGuess.guess < secret = "Too low"
                | playersGuess.guess > secret = "Too high"
                | otherwise = "You guessed it!"
        let isGameOver =
              playersGuess.guess == secret
              || playersGuess.remainingGuesses == 0
        if not isGameOver
        then do Left <$> exercise playersGuessId GuessAgain with msg
        else do Right <$> exercise playersGuessId EndGame with msg

template PlayersTurn
  with
    owner: Party
    player: Party
    secretNumberId: SecretNumberId
    lastGuess: Optional Int
    remainingGuesses: Int
    msg: Text
  where
    signatory owner
    observer player

    choice Guess : PlayersGuessId
      with guess: Int
      controller player
      do  
        create PlayersGuess with
          remainingGuesses = remainingGuesses - 1
          ..

template PlayersGuess
  with
    owner: Party
    player: Party
    secretNumberId: SecretNumberId
    remainingGuesses: Int
    guess: Int
  where
    signatory player, owner

    choice GuessAgain : PlayersTurnId
      with msg : Text
      controller owner
      do create PlayersTurn with
          lastGuess = Some guess
          ..

    choice EndGame : GameOverId
      with msg : Text
      controller owner
      do
        secretNumber <- fetch secretNumberId
        archive secretNumberId
        create GameOver with
          secret = secretNumber.secret
          lastGuess = guess
          ..

template GameOver
  with
    owner: Party
    player: Party
    secretNumberId: SecretNumberId
    secret: Int
    lastGuess: Int
    msg: Text
  where
    signatory owner, player

game = script do
  owner <- allocatePartyWithHint "owner" (PartyIdHint "owner")    
  player <- allocatePartyWithHint "player" (PartyIdHint "player")
  secret <- submit owner $ createCmd SecretNumber with owner, player, secret = 42
  playersTurn1 <- submit owner $ exerciseCmd secret StartGame with allowedGuesses = 3
  playersGuess1 <- submit player $ exerciseCmd playersTurn1 Guess with guess = 20
  Left playersTurn2 <- submit owner $ exerciseCmd secret GiveHint with playersGuessId = playersGuess1
  playersGuess2 <- submit player $ exerciseCmd playersTurn2 Guess with guess = 60
  Left playersTurn3 <- submit owner $ exerciseCmd secret GiveHint with playersGuessId = playersGuess2
  playersGuess3 <- submit player $ exerciseCmd playersTurn3 Guess with guess = 40
  Right gameOver <- submit owner $ exerciseCmd secret GiveHint with playersGuessId = playersGuess3
  return ()

I welcome feedback!

2 Likes