question

Matthew Miskiewicz avatar image
Matthew Miskiewicz asked

Using an Entity object as a semaphore to counter concurrency hack attempts?

The problem scenario: a hacker sends multiple CloudScript requests simultaneously, in order to do things like open a chest twice, complete a quest twice, etc. I've read the answer here, https://community.playfab.com/questions/25146/multiple-calls-to-a-executecloudscript-how-to-deal.html, which proposes to use after-the-fact analytics to detect when such behaviors occur. However, in a multiplayer game, allowing duplicate CloudScripts to successfully execute could have negative impacts on other players - I'd like to try and prevent that if possible.

My proposed solution is to use an Entity Object on title_player as a semaphor, aka, a multi-thread lock, by utilizing the optimistic concurrency option. I would also disable title_player access to itself using the global policy, making the objects in title_player entity similar to internal player data. Here is a simple example in pseudo code (I haven't used the Entity API yet) of how I envision the system working:

Context: sensitive actions are performed when a player completes a quest; thus, the player should only be able to complete a given quest once. Two calls are made by the client:

A: finishQuest(questId: 12345)

B: finishQuest(questId: 12345)

Assume at the start that LastQuestCompleted != 12345

handlers.finishQuest(questId){
	var entity = server.GetEntity(playerId);
	var getObjectsResult = entity.GetObjects(key: "LastQuestCompleted");
	var lastQuestCompleted = getObjectsResult ["LastQuestCompleted"];
	var profileVersion = getObjectsResult ["ProfileVersion"];

	// this check will fail if A and B are executed simultaneously
	if(lastQuestCompleted == questId) return denied;

	// as I understand it, this will always fail for either A or B
	var result = entity.SetObjects(
		"LastQuestCompleted": questId,
		"ExpectedProfileVersion": profileVersion
	);

	if (result == error) return denied;

	doStuff();
}

Can I expect this approach to work, and is it a reasonable? Thanks!

edit: I realized that it looks possible to turn it into a pessimistic concurrency system quite easily...does this work?

var getObjectsResult = entity.GetObjects()

// deny if currently locked
if(getObjectsResult.Locked) return denied;

// attempt to lock
var result = entity.SetObjects(Locked: true);

// deny if optimistic concurrency failed
if(result == error) return denied;

doStuff();

// unlock
entity.SetObjects(Lock: false);
entities
10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

1 Answer

·
Citrus Yan avatar image
Citrus Yan answered

Hi @Matthew Miskiewicz,

I think the first approach might work although it seems more complex compared to the second one. The problem with the second approach is that if both Quest A and B executes line 1 when the entity object is not locked, aka, getObjectsResult.Locked == true, hence they will both pass the if-statement in line 4, and no doubt they will both execute line 7 without errors since you didn’t add concurrency control like line 13 in the first approach. For instance, if the execution is in this order:

A->line 1,

B->line 1,

A->line 2,

A->line 7,

A->line 10,

B->line 2,

B->line 7,

B->line 10,

A->line 12,

B->line 12,

A->line 15,

B->line 15.

They will both get executed without blockage.

2 comments
10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

Matthew Miskiewicz avatar image Matthew Miskiewicz commented ·

Ok, terrific. I believe that I can work with these kinds of method to solve most concurrency issues. No more questions on this for now then. Thanks! :-)

Also, good catch on the second method - I appreciate it!

0 Likes 0 ·
Citrus Yan avatar image Citrus Yan Matthew Miskiewicz commented ·

Glad it helped:)

0 Likes 0 ·

Write an Answer

Hint: Notify or tag a user in this post by typing @username.

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.