RFC: Secure, Serverless Network Play Idea

Or, How I Learned To Stop Cheating And Love Cryptography
I had an idea today of how to implement network play in a way that:
a.) Does not require a central server
b.) Prevents 'active cheating' (altering things that should not be alterable)
c.) Prevents 'passive cheating' (peeking at things that should be hidden)
d.) For all players (no 'hosting player runs gdb' kind of holes)
The basic idea is to use cryptographic commitments in order to 'lock in' secret information, without revealing it. To explain, I will begin with a trivial design that only fulfills a.) and b.) and then extend it to fulfill c.) and d.)
A cryptographic commitment is a tool by which you can prove you knew something before you revealed it - i.e. 'commit' to it - similar to getting an envelope post-dated.
As an example, let's say I know "The combination for the safe is 4423". I don't want to reveal it right now, but I want people to believe me two weeks from now when I claim I had been sitting on this for two weeks. A trivial (read: do not use this for actual security) commitment scheme is simply a hash - like SHA256. So I take the hash H("The combination for the safe is 4423") = random_looking_value, and reveal that. Later, when I reveal "The combination for the safe is 4423", people can hash it and check against the hash I gave them in order to verify that I knew it back then. This works because finding two things with the same hash value is exceedingly difficult.
So, let's begin with a trivial network play system - each player runs a separate instance of Forge. When the game starts, they completely synchronize their state - the contents of each players' library, hand, etc. All information is known to all Forge instances. As play continues, each command ("play card 3 in player 1's hand") goes to both instances, and if something would desynchronize the state (such as shuffling) then the full data gets sent. Each player has a 'check for cheating' button, which when pressed makes the two instances compare their state. If there are differences, they start yelling. This satisfies b.)
However, since each instance has all the data, any player can just attach a debugger and pull out things like what cards the other is holding.
In order to resolve this, instead of synchronizing secret state (library, face-down cards, hand) each player instead sends a commitment of those things.
Specifically, the player sends an ordered list of per-card commitments - so the library would be an array that goes something like:
( commit('Forest'), commit('Llanowar Elves'), commit('Darksteel Citadel'), ... )
This is where the trivial commitment scheme I laid out before ('just hash it') fails: Because there are a limited number of cards to choose from, some would have the same hash (basic lands especially) and the other players could also brute-force it by hashing every possible card and comparing. This would reveal too much. The solution is to use a random nonce (number used once) for each, concatenated with the card. A 16-byte nonce is generally considered sufficient. Thus, it becomes
( commit( nonce1 + 'Forest' ), commit( nonce2 + 'Llanowar Elves' ), ... )
When a card ceases to be secret, then the player must reveal both the card and the nonce, and the other players verify it.
When a card becomes secret is slightly tricky though. In real-life play turning a Morph creature facedown again doesn't really make it any particular secret what it is - we've already seen it. Thus, it doesn't matter much that when the player generates a new commitment for it, we can still follow the 'secret' card around since the nonce stays the same.
But when a formerly-known card would be shuffled into the library, that becomes a big problem. Because of that, when randomizing any group of secret cards, all cards in the group must be assigned new nonces and re-commited - otherwise the randomization would not actually hide anything.
At the end of the game, all secret data must be revealed, and a final comparison run.
With this system in place, each Forge instance only has access to public information, its own secret information, and commitments over the other instances' secret information - this fulfills c.) and d.)
Thoughts?
EDITED TO ADD:
Comparing the state deserves more attention than I gave it. In order to do so securely, it's necessary to prevent the other players from just replaying your state back at you. In order to do so efficiently, you don't want to send the entire state over.
I'll define state-comparison in a pairwise manner:
1.) Players A and B each serialize their state into a canonical form (i.e. the same state always results in the same sequence of bits). This should probably serialize _all_ secret data as commitments, rather than the actual data (even for themselves).
2.) Each player commits that state, generating their own nonce.
2.) Each player sends the other the nonce and commitment from step 2
3.) Each player uses the other's nonce to commit their own state. If the received commitment and the new commitment match, the states match.
This only sends one hash and one nonce across the network, and prevents spoofing.
However, it does require hashing the state twice - if this is large, it may be a performance issue. To reduce this, each player could instead hash the state without a nonce as step 1.5, and commit to that hash instead. That allows each player to only hash the (potentially large) state once.
In order to do this for more than two players, simply repeat steps 2 and 3 for each pair. That is where the above optimization would really help, because otherwise the entire state would need to be hashed for each pairwise comparison. With the optimization, the intermediate nonce-less hash can be cached, reducing the computation needed significantly.
I had an idea today of how to implement network play in a way that:
a.) Does not require a central server
b.) Prevents 'active cheating' (altering things that should not be alterable)
c.) Prevents 'passive cheating' (peeking at things that should be hidden)
d.) For all players (no 'hosting player runs gdb' kind of holes)
The basic idea is to use cryptographic commitments in order to 'lock in' secret information, without revealing it. To explain, I will begin with a trivial design that only fulfills a.) and b.) and then extend it to fulfill c.) and d.)
A cryptographic commitment is a tool by which you can prove you knew something before you revealed it - i.e. 'commit' to it - similar to getting an envelope post-dated.
As an example, let's say I know "The combination for the safe is 4423". I don't want to reveal it right now, but I want people to believe me two weeks from now when I claim I had been sitting on this for two weeks. A trivial (read: do not use this for actual security) commitment scheme is simply a hash - like SHA256. So I take the hash H("The combination for the safe is 4423") = random_looking_value, and reveal that. Later, when I reveal "The combination for the safe is 4423", people can hash it and check against the hash I gave them in order to verify that I knew it back then. This works because finding two things with the same hash value is exceedingly difficult.
So, let's begin with a trivial network play system - each player runs a separate instance of Forge. When the game starts, they completely synchronize their state - the contents of each players' library, hand, etc. All information is known to all Forge instances. As play continues, each command ("play card 3 in player 1's hand") goes to both instances, and if something would desynchronize the state (such as shuffling) then the full data gets sent. Each player has a 'check for cheating' button, which when pressed makes the two instances compare their state. If there are differences, they start yelling. This satisfies b.)
However, since each instance has all the data, any player can just attach a debugger and pull out things like what cards the other is holding.
In order to resolve this, instead of synchronizing secret state (library, face-down cards, hand) each player instead sends a commitment of those things.
Specifically, the player sends an ordered list of per-card commitments - so the library would be an array that goes something like:
( commit('Forest'), commit('Llanowar Elves'), commit('Darksteel Citadel'), ... )
This is where the trivial commitment scheme I laid out before ('just hash it') fails: Because there are a limited number of cards to choose from, some would have the same hash (basic lands especially) and the other players could also brute-force it by hashing every possible card and comparing. This would reveal too much. The solution is to use a random nonce (number used once) for each, concatenated with the card. A 16-byte nonce is generally considered sufficient. Thus, it becomes
( commit( nonce1 + 'Forest' ), commit( nonce2 + 'Llanowar Elves' ), ... )
When a card ceases to be secret, then the player must reveal both the card and the nonce, and the other players verify it.
When a card becomes secret is slightly tricky though. In real-life play turning a Morph creature facedown again doesn't really make it any particular secret what it is - we've already seen it. Thus, it doesn't matter much that when the player generates a new commitment for it, we can still follow the 'secret' card around since the nonce stays the same.
But when a formerly-known card would be shuffled into the library, that becomes a big problem. Because of that, when randomizing any group of secret cards, all cards in the group must be assigned new nonces and re-commited - otherwise the randomization would not actually hide anything.
At the end of the game, all secret data must be revealed, and a final comparison run.
With this system in place, each Forge instance only has access to public information, its own secret information, and commitments over the other instances' secret information - this fulfills c.) and d.)
Thoughts?
EDITED TO ADD:
Comparing the state deserves more attention than I gave it. In order to do so securely, it's necessary to prevent the other players from just replaying your state back at you. In order to do so efficiently, you don't want to send the entire state over.
I'll define state-comparison in a pairwise manner:
1.) Players A and B each serialize their state into a canonical form (i.e. the same state always results in the same sequence of bits). This should probably serialize _all_ secret data as commitments, rather than the actual data (even for themselves).
2.) Each player commits that state, generating their own nonce.
2.) Each player sends the other the nonce and commitment from step 2
3.) Each player uses the other's nonce to commit their own state. If the received commitment and the new commitment match, the states match.
This only sends one hash and one nonce across the network, and prevents spoofing.
However, it does require hashing the state twice - if this is large, it may be a performance issue. To reduce this, each player could instead hash the state without a nonce as step 1.5, and commit to that hash instead. That allows each player to only hash the (potentially large) state once.
In order to do this for more than two players, simply repeat steps 2 and 3 for each pair. That is where the above optimization would really help, because otherwise the entire state would need to be hashed for each pairwise comparison. With the optimization, the intermediate nonce-less hash can be cached, reducing the computation needed significantly.