Game “Attack 51%”: writing a simple standalone game on the Obyte platform





In the last article “Autonomous Agents” or we execute the code in the Obyte open cryptoplatform, we talked about what Autonomous Agents are and compared them with Ethereum smart contracts. Let's now write our first Autonomous Agent (AA) using the 51% Attack example. And at the end of the article, we will analyze ways to improve it: how to protect players from loss / loss of funds and how to improve the algorithm to reduce the effect of “whales” with large deposits on the outcome of the game.



The originals of both stand-alone agents are always available in the Oscript online code editor in the form of templates, just select them from the drop-down menu: “51% attack game” and “Fundraising proxy”.



Before you start writing AA in Oscript, I highly recommend reading the Getting Started Guide (eng) in our documentation to quickly become familiar with the basic principles of writing AA.



The essence of the game



Several teams simultaneously compete among themselves for the right to collect the entire pool of collected funds. Team players make deposits, thereby increasing the total amount of the pool. The team that has made deposits of at least 51% of all funds raised and held out as a leader for at least a day wins. The pool is distributed among the players of the winning team in proportion to the contribution made, thereby each participant can potentially double their investment.



As soon as any of the teams collects> = 51% of all funds, it is previously declared the winner and its participants can no longer make deposits. But all the other teams have 24 hours to overtake the winning team. As soon as this happens, the overtaking team now becomes the winner and the timer starts the countdown again.



Our implementation will issue "shares" to each depositor in exchange for bytes, in a 1-to-1 ratio, which, in the event of a team winning, the participant can exchange for a share in the won pool. Stocks are an asset on the Obyte platform, released specifically for each team. They, like any asset, can be transferred / traded, selling their potential winnings to other people. The share price will depend on the market evaluating the team’s chances of winning.



Anyone can create a team. The creator of the team can set a commission on winning, which will be charged from each member of the team in case of victory.



We will also apply the second AA in our game, on behalf of the creator of the team, who will “crowd fund” the necessary amount, and only if it is achieved (in our example, when reaching> 51% of the game pool) it will send all the funds collected to the address AA games, otherwise the money can be freely taken back.



Writing Oscript Code



Let me remind you that the AA code is called every time any transaction arrives at the address of this AA, the so-called trigger transaction. Actually, AA is a code that, in response to input (data in a trigger transaction and the current state of AA itself, stored in state variables) generates output (another “response” transaction), or changes its state. Our task is to program the rules of AA reaction to the input data. Any transactions in Obyte are a set of messages, most often it is “payment” messages, or “data” messages, etc.



Initialization



First, we initialize our AA. The init block is called every time AA starts, at the very beginning. In it we will set local constants for more convenient access to values.



{     init: `{         $team_creation_fee = 5000;         $challenging_period = 24*3600;         $bFinished = var['finished'];     }`,     messages: { cases: [ ]     } }
      
      





String $ bFinished = var ['finished']; reads the finished variable from state AA.

The array of the resulting messages will be framed by the cases: {} block, which is similar to the usual switch / case / default block, based on the conditions in the if child blocks, only one of the messages will be selected, or if no if blocks returned true , it will be last message selected.



We create a team



So, the first block will process transactions to create a new command:



 // create a new team; any excess amount is sent back if: `{trigger.data.create_team AND !$bFinished}`, init: `{ if (var['team_' || trigger.address || '_asset']) bounce('you already have a team'); if (trigger.output[[asset=base]] < $team_creation_fee) bounce('not enough to pay for team creation'); }`, messages: [ { app: 'asset', payload: { is_private: false, is_transferrable: true, auto_destroy: false, fixed_denominations: false, issued_by_definer_only: true, cosigned_by_definer: false, spender_attested: false } }, { app: 'payment', if: `{trigger.output[[asset=base]] > $team_creation_fee}`, payload: { asset: 'base', outputs: [ {address: "{trigger.address}", amount: "{trigger.output[[asset=base]] - $team_creation_fee}"} ] } }, { app: 'state', state: `{ var['team_' || trigger.address || '_founder_tax'] = trigger.data.founder_tax otherwise 0; var['team_' || trigger.address || '_asset'] = response_unit; response['team_asset'] = response_unit; }` } ]
      
      





As we see, the condition for the execution of this block is the first in the if block and is a check that the trigger transaction contains a message with the data type ( trigger.data.create_team ), in which there is a key called create_team and that the game is not over yet ( ! $ bFinished ). The local constant $ bFinished is accessible from anywhere in the code if it has been initialized in the init block. If at least one of these conditions were not fulfilled, then the parent cases block would simply continue to execute and check the conditions for the following messages, skipping this one.



In the next init block, we do not initialize anything, but we check the necessary conditions, without which the trigger transaction is considered erroneous:



 if (var['team_' || trigger.address || '_asset']) bounce('you already have a team'); if (trigger.output[[asset=base]] < $team_creation_fee) bounce('not enough to pay for team creation');
      
      





Here we concatenate (using ||) the string with the variable from the transaction trigger and try to find out if there is a variable named 'team_' || trigger.address || '_asset' in our AA story.



The bounce () call rolls back any changes made to the current moment and returns an error to the caller.



Also note how the search inside the trigger transaction is performed : trigger.output [[asset = base]] is looking for output with asset == base , which will return the amount in bytes (base asset = bytes) that was specified in the trigger transaction. And if this amount is not enough to create a new team, we call bounce (), silently eating all incoming bytes minus bounce_fee, which by default is 10,000 bytes.



Next, the bulk of the team building code begins. Briefly, the algorithm is as follows:



  1. The first message releases a new asset ( app: 'asset' )

  2. The second message returns everything that is more than the required number of bytes for creating the command ( app: 'payment' ). Check out the if block here. If this condition is false (the creator sent exactly the required number of bytes), then this message will not be included in the resulting transaction, but simply will be thrown out.

  3. The third message changes the state of our AA ( app: 'state' ): we write the founder_tax passed as an argument, or set it to 0 if it was not transferred to the trigger transaction. The var1 construct otherwise var2 returns var1 if it casts to true, otherwise it returns var2. Here we meet the response_unit variable, which always contains the hash of the resulting unit. In this case, because the resulting unit will create a new asset called asset-a and will be the hash of the creating unit. The string response ['team_asset'] = response_unit simply writes the same hash (or asset for the given command) to the responseVars array in the final unit. The response array can also be read to the person who made the trigger transaction, as well as in event listeners subscribed to events with this AA.



We accept deposits



With the creation of the team finished, go to the next block - processing deposits from team members.



 // contribute to a team if: `{trigger.data.team AND !$bFinished}`, init: `{ if (!var['team_' || trigger.data.team || '_asset']) bounce('no such team'); if (var['winner'] AND var['winner'] == trigger.data.team) bounce('contributions to candidate winner team are not allowed'); }`, messages: [ { app: 'payment', payload: { asset: `{var['team_' || trigger.data.team || '_asset']}`, outputs: [ {address: "{trigger.address}", amount: "{trigger.output[[asset=base]]}"} ] } }, { app: 'state', state: `{ var['team_' || trigger.data.team || '_amount'] += trigger.output[[asset=base]]; if (var['team_' || trigger.data.team || '_amount'] > balance[base]*0.51){ var['winner'] = trigger.data.team; var['challenging_period_start_ts'] = timestamp; } }` } ]
      
      





From the new that we meet here is the issuance of tokens of the asset of the selected team to its participant in exchange for its deposit in bytes:



 asset: `{var['team_' || trigger.data.team || '_asset']}`, outputs: [{address: "{trigger.address}", amount: "{trigger.output[[asset=base]]}"}
      
      





As we recall, the state variable 'team_' || trigger.data.team || We saved the '_asset' at the stage of creating the team, and it stores the hash of the unit in which we created the asset for this team, that is, the name of this asset-a.



In the same block, the main condition is checked for 51%:



 if (var['team_' || trigger.data.team || '_amount'] > balance[base]*0.51){ var['winner'] = trigger.data.team; var['challenging_period_start_ts'] = timestamp; }
      
      





If after this trigger transaction the balance of the specified team exceeds 51%, then the team is declared the winner and we record the current unix timestamp (start the timer).



This timestamp will be checked in the third block when we receive a trigger transaction with an attempt to end the game:



 // finish the challenging period and set the winner if: `{trigger.data.finish AND !$bFinished}`, init: `{ if (!var['winner']) bounce('no candidate winner yet'); if (timestamp < var['challenging_period_start_ts'] + $challenging_period) bounce('challenging period not expired yet'); }`, messages: [ { app: 'state', state: `{ var['finished'] = 1; var['total'] = balance[base]; var['challenging_period_start_ts'] = false; response['winner'] = var['winner']; }` } ]
      
      





We pay a prize



And the final block, the most pleasant, is the payment of the entire deposit to the winners:



 // pay out the winnings if: `{ if (!$bFinished) return false; $winner = var['winner']; $winner_asset = var['team_' || $winner || '_asset']; $asset_amount = trigger.output[[asset=$winner_asset]]; $asset_amount > 0 }`, init: `{ $share = $asset_amount / var['team_' || $winner || '_amount']; $founder_tax = var['team_' || $winner || '_founder_tax']; $amount = round(( $share * (1-$founder_tax) + (trigger.address == $winner AND !var['founder_tax_paid'] ? $founder_tax : 0) ) * var['total']); }`, messages: [ { app: 'payment', payload: { asset: "base", outputs: [ {address: "{trigger.address}", amount: "{$amount}"} ] } }, { app: 'state', state: `{ if (trigger.address == $winner) var['founder_tax_paid'] = 1; }` } ]
      
      





The initialization block is interesting here, in which we calculate the necessary values ​​in advance:



  $share = $asset_amount / var['team_' || $winner || '_amount']; $founder_tax = var['team_' || $winner || '_founder_tax']; $amount = round(( $share * (1-$founder_tax) + (trigger.address == $winner AND !var['founder_tax_paid'] ? $founder_tax : 0) ) * var['total']);
      
      





All members of the team, except its creator, are paid an amount proportional to their initial contribution (1-by-1 bytes in exchange for asset-tokens sent in a trigger transaction). The creator is also paid a commission (it is verified that the address of the person who sent the trigger transaction is equal to the address of the creator of the winning team trigger.address == $ winner ). It is important not to forget that the commission should be paid only once, and the creator can send infinitely many trigger transactions, so we save the flag in state AA.



Start the game



So, the code is ready. Here is a complete listing of it:



full AA code
 { init: `{ $team_creation_fee = 5000; $challenging_period = 24*3600; $bFinished = var['finished']; }`, messages: { cases: [ { // create a new team; any excess amount is sent back if: `{trigger.data.create_team AND !$bFinished}`, init: `{ if (var['team_' || trigger.address || '_amount']) bounce('you already have a team'); if (trigger.output[[asset=base]] < $team_creation_fee) bounce('not enough to pay for team creation'); }`, messages: [ { app: 'asset', payload: { is_private: false, is_transferrable: true, auto_destroy: false, fixed_denominations: false, issued_by_definer_only: true, cosigned_by_definer: false, spender_attested: false } }, { app: 'payment', if: `{trigger.output[[asset=base]] > $team_creation_fee}`, payload: { asset: 'base', outputs: [ {address: "{trigger.address}", amount: "{trigger.output[[asset=base]] - $team_creation_fee}"} ] } }, { app: 'state', state: `{ var['team_' || trigger.address || '_founder_tax'] = trigger.data.founder_tax otherwise 0; var['team_' || trigger.address || '_asset'] = response_unit; response['team_asset'] = response_unit; }` } ] }, { // contribute to a team if: `{trigger.data.team AND !$bFinished}`, init: `{ if (!var['team_' || trigger.data.team || '_asset']) bounce('no such team'); if (var['winner'] AND var['winner'] == trigger.data.team) bounce('contributions to candidate winner team are not allowed'); }`, messages: [ { app: 'payment', payload: { asset: `{var['team_' || trigger.data.team || '_asset']}`, outputs: [ {address: "{trigger.address}", amount: "{trigger.output[[asset=base]]}"} ] } }, { app: 'state', state: `{ var['team_' || trigger.data.team || '_amount'] += trigger.output[[asset=base]]; if (var['team_' || trigger.data.team || '_amount'] > balance[base]*0.51){ var['winner'] = trigger.data.team; var['challenging_period_start_ts'] = timestamp; } }` } ] }, { // finish the challenging period and set the winner if: `{trigger.data.finish AND !$bFinished}`, init: `{ if (!var['winner']) bounce('no candidate winner yet'); if (timestamp < var['challenging_period_start_ts'] + $challenging_period) bounce('challenging period not expired yet'); }`, messages: [ { app: 'state', state: `{ var['finished'] = 1; var['total'] = balance[base]; var['challenging_period_start_ts'] = false; response['winner'] = var['winner']; }` } ] }, { // pay out the winnings if: `{ if (!$bFinished) return false; $winner = var['winner']; $winner_asset = var['team_' || $winner || '_asset']; $asset_amount = trigger.output[[asset=$winner_asset]]; $asset_amount > 0 }`, init: `{ $share = $asset_amount / var['team_' || $winner || '_amount']; $founder_tax = var['team_' || $winner || '_founder_tax']; $amount = round(( $share * (1-$founder_tax) + (trigger.address == $winner AND !var['founder_tax_paid'] ? $founder_tax : 0) ) * var['total']); }`, messages: [ { app: 'payment', payload: { asset: "base", outputs: [ {address: "{trigger.address}", amount: "{$amount}"} ] } }, { app: 'state', state: `{ if (trigger.address == $winner) var['founder_tax_paid'] = 1; }` } ] } ] } }
      
      







Let's check the code for validity and try to deploy it in testnet.



  1. Go to the online editor: https://testnet.oscript.org

  2. Paste our code and click Validate. If everything is correct, we will see the calculation of the code complexity: AA validated, complexity = 27, ops = 176 . Here ops is the number of operations in our code, complexity is the complexity of the code. Obyte AAs do not have cycles, but even this does not allow 100% protection of the network from malicious AAs with bad code. Therefore, all AAs have an upper limit of complexity, complexity = 100. Code complexity is calculated at the time of deployment, and all branches of the code are taken into account. Some operations are relatively easy, such as ± and others. They do not add complexity. Others, such as access to the database (modification of state) or complex calculations (calling some functions) add complexity. To find out which operations are easy and which are complex, refer to the language reference .

  3. Click Deploy. We see something similar to



 Check in explorer: https://testnetexplorer.obyte.org/#DiuxsmIijzkfAVgabS9chJm5Mflr74lZkTGud4PM1vI= Agent address: 6R7SF6LTCNSPLYLIJWDF57BTQVZ7HT5N
      
      





I advise you to follow the link in explorer and make sure that the unit with our AA is posted to the network. Explorer also shows the full code for our AA, as All AAs on the Obyte network are open source. You can study the code of any agent at its address.



Crowdfunding



And now to the promised optimizations. In the current implementation, each player sends money immediately to the AA address of the game, indicating his team. At the same time, the team may never become a leader, and the money has already been sent. We can optimize the process of collecting money and avoid the situation that we send money to a team that will never become a winner. This can be done using the second AA, arrange the so-called crowdfunding of funds by setting a dynamic goal for the amount of funds raised equal to 51% of the amount in the game.



In Oscript, we can read the state of any other AA, so we have the opportunity to set a dynamic goal in our crowdfunding AA.



The algorithm will be as follows: the creator of the team asks the players to send money not to the address of the game, but to the address of an agent that implements crowdfunding functionality. This agent will store the collected amount of bytes at home and send them to the game only if it collects> = 51% of the amount in the game. And immediately this team becomes a leader. If the necessary amount is not collected, then the money will simply be returned to the players. At the stage of fundraising, players will not receive team game tokens, but crowdfunding tokens, which in the future can be refunded or exchanged for game tokens, if successful.



In the next article, we implement this functionality.



We collect not bytes, but certifications



In the simplest form of the game “Attack 51%” we are talking about the amounts of funds raised. Thus, “whales” with large wallets can take most of the winnings.



To make the game more honest for all participants, we’ll try to count not the amount of bytes sent, but the number of participants in the teams. The team that was able to attract the maximum number of participants wins. Each player will invest a fixed amount, say 1GB, and count as a unit in the team pool. But nothing prevents us from creating an infinite number of new addresses, so the critical condition will be that only addresses that are tied to some other IDs are allowed in the game, in which the “one person - one ID” rule is respected. Such a binding is called certification . An example of certification is the passage of the KYC procedure, after which a message is sent to the DAG about the connection between the Obyte address and the hash of personal data (the personal data itself is stored in the user's wallet, and he can disclose it to individual counterparties if he wants, but for this task they are not necessary, it is important only the fact of binding). Another example of certification is certification of email on domains where the “one person - one email” rule is respected, for example, on the domains of some universities, companies and countries. Thus, one real person will be counted only once.



Autonomous Agents can request the status of certification of addresses on the network, so there will be a minimum number of changes in the code.



I suggest readers in the comments suggest in which places and exactly how to change the code lines of the current version of the game in order to implement this. To help, as always, the Oscript Language Reference .



Our Discord & Twitter



All Articles