Stratis Puzzle Solutions

Khilone
Khilone
Published in
15 min readOct 15, 2018

On July 25, 2018, Stratis released their ‘Puzzle Challenge’. The Puzzle is solved and Stratis gave away 4.000 $STRAT and two job offerings.

On behalf of the Puzzle makers ‘Aprogiena and NoEscape0’, I would like to thank everyone for the participation in the Stratis Puzzle. They hope you had a fun time solving it.

Winners

Congratulations to the winners:

  • Main price: 4000 STRAT, team of:
    - Luigy
    - JTobcat
    - lotek
    - mattm
    - ziot
  • Second price: Job offer — someguy
  • Third prize: Job offer — MithrilMan

Solutions

Since the puzzle is finished ‘Aprogiena and NoEscape0’ would like to share solutions for all pieces of the puzzle.

Puzzle 1 — Nonogram

Original assignment:

7b22515220636f6465203231783231223a7b22766572746963616c223a5b5b372c322c312c37
5d2c5b312c312c312c312c312c315d2c5b312c332c312c312c312c312c312c332c315d2c5b31
2c332c312c312c312c312c332c315d2c5b312c332c312c342c312c332c315d2c5b312c312c31
2c315d2c5b372c312c312c312c375d2c5b335d2c5b342c312c312c312c322c223342222c315d
2c5b312c322c322c312c312c315d2c5b322c322c312c325d2c5b322c223241222c312c312c31
2c325d2c5b312c342c322c312c325d2c5b322c332c315d2c5b372c312c315d2c5b312c312c32
2c322c312c223143225d2c5b312c332c312c312c312c312c345d2c5b312c332c312c322c342c
335d2c5b312c332c312c322c335d2c5b312c312c312c352c223144225d2c5b372c312c312c33
2c315d5d2c22686f72697a6f6e74616c223a5b5b372c322c375d2c5b312c312c312c312c312c
312c315d2c5b312c332c312c312c312c312c332c315d2c5b312c332c312c322c312c312c332c
315d2c5b312c332c312c312c322c312c332c315d2c5b312c312c312c312c315d2c5b372c312c
312c312c375d2c5b312c315d2c5b312c332c312c312c312c345d2c5b322c312c312c352c345d
2c5b312c312c342c223241222c312c325d2c5b322c312c325d2c5b332c312c322c332c355d2c
5b312c312c312c345d2c5b372c322c322c325d2c5b312c312c312c312c315d2c5b312c332c31
2c322c332c315d2c5b312c332c312c312c322c325d2c5b312c332c312c223142222c332c315d
2c5b312c312c322c325d2c5b372c342c223143222c223144225d5d7d7d

Solution:

Which is hex that results in:

{"QR code 21x21":{"vertical":[[7,2,1,7],[1,1,1,1,1,1],[1,3,1,1,1,1,1,3,1],[1,3,1,1,1,1,3,1],[1,3,1,4,1,3,1],[1,1,1,1],[7,1,1,1,7],[3],[4,1,1,1,2,"3B",1],[1,2,2,1,1,1],[2,2,1,2],[2,"2A",1,1,1,2],[1,4,2,1,2],[2,3,1],[7,1,1],[1,1,2,2,1,"1C"],[1,3,1,1,1,1,4],[1,3,1,2,4,3],[1,3,1,2,3],[1,1,1,5,"1D"],[7,1,1,3,1]],"horizontal":[[7,2,7],[1,1,1,1,1,1,1],[1,3,1,1,1,1,3,1],[1,3,1,2,1,1,3,1],[1,3,1,1,2,1,3,1],[1,1,1,1,1],[7,1,1,1,7],[1,1],[1,3,1,1,1,4],[2,1,1,5,4],[1,1,4,"2A",1,2],[2,1,2],[3,1,2,3,5],[1,1,1,4],[7,2,2,2],[1,1,1,1,1],[1,3,1,2,3,1],[1,3,1,1,2,2],[1,3,1,"1B",3,1],[1,1,2,2],[7,4,"1C","1D"]]}}

This encodes nonogram. The correct visualization is:

Those numbers represent series of black squares in one row or column. The whole idea is similar to sudoku puzzle. 1D, 3B and so on — those are hints to decrease amount of possible solutions so it’s easier for the solver, they specify exact coordinates where the black squares should be and therefore eliminating guessing their position.

And the solution is:

This QR leads to a link shortener (http://bit.ly/2GIbl2H) which will redirect you to https://github.com/Aprogiena/StratisBitcoinFullNode/blob/experiment/hrpuzz/hrpuzz/readme.md which contains introduction and also gives the first word which is market. Then you were supposed to go to the words directory and open market.txt.

Puzzle 2 — hash of the next word

Original assignment:

fafe97f7def328bbd4f10779b9625a8aa0bfaa143d7ae64e6f5770e47b51cd1d

Solution:

If you google it you find out that it’s a hash of the word perfect.

Puzzle 3 — tx hash

Original assignment:

e241a9203ce4fd5a31677a9bd293f34453c58304a6222d8b957533717c21dfe2

Solution:

This is a hash of the transaction. If we look it up in the chain explorer we will find it: https://chainz.cryptoid.info/strat/tx.dws?e241a9203ce4fd5a31677a9bd293f34453c58304a6222d8b957533717c21dfe2.htm

There is an output with a script ”OP_DUP OP_HASH160 636f6e74696e7565207769746820227365656422 OP_EQUALVERIFY OP_CHECKSIG” inside this transaction.

636f6e74696e7565207769746820227365656422is hex which can be converted to the following string: continue with “seed”. So the next word is seed.

Puzzle 4 — logs

Original assignment:

To find the next word, you need to download Stratis full node code 
from this GitHub repository branch: https://github.com/Aprogiena/StratisBitcoinFullNode/tree/experiment/hrpuzz
Then you need to build the solution and set up trace logs for “Stratis.Bitcoin.Features.Consensus.*”.
Then run the node on Stratis testnet and inspect the corresponding log file once the node is fully synced with the network.
Hint: Read https://github.com/stratisproject/StratisBitcoinFullNode/blob/master/Documentation/using-logging.md on how to enable trace level logging.

Solution:

In order to enable the logs NLog.config file should be created with single logging target: Stratis.Bitcoin.Features.Consensus.*. Then we need to sync and investigate the logs.

NLog.config file should look like the following one:

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" autoReload="true">
<targets>
<target name="debugConsensusFile" xsi:type="AsyncWrapper" queueLimit="10000" overflowAction="Block" batchSize="1000">
<target xsi:type="File" fileName="consensus.txt" archiveNumbering="Date" maxArchiveFiles="14" archiveEvery="Day" layout="[${longdate:universalTime=true} ${threadid}${mdlc:item=id}] ${level:uppercase=true}: ${callsite} ${message}" encoding="utf-8" />
</target>
</targets>
<rules>
<logger name="Stratis.Bitcoin.Features.Consensus.*" minlevel="Trace" writeTo="debugConsensusFile" />
</rules>
</nlog>

Logs would contain the following:

#####################################################
###################--== START ==--###################
#####################################################

After investigation of the logs we find the next word: `start`.

Puzzle 5 — faucet

Original assignment:

For the next word, you will again need to work with Stratis full node. This time, however, you need to implement a new feature.
More specifically, you need to implement understanding of new payloads to your node.

Let's call these payloads FaucetRequest, FaucetChallenge, FaucetSolution, FaucetReward.

Your node needs to connect to a special node on testnet that listens on this IP address: 52.233.33.138.
After a standard handshake, your node can send FaucetRequest payload (network command "faucetreq"), which contains:

– your Stratis testnet address (40 bytes long byte array that encodes the address as an ASCII string with 0 bytes padding at the end)


The special node replies with FaucetChallenge (network command "faucetchal"), which contains:

– SHA256 hash (32 bytes long byte array)
– challenge data (28 bytes long byte array)


To which your node is supposed to reply with FaucetSolution (network command "faucetsol") payload, which contains:

– challenge data (28 bytes long byte array, byte copy from FaucetChallenge)
– nonce (4 bytes long byte array) such that SHA256 (challenge data + nonce) = hash from FaucetChallenge, where + operator is concatenation of byte arrays

The nonce can be interpreted as 32-bit unsigned integer written in network byte order. As such it is guaranteed that the value stored in the integer is lower than 0x01000000.


If the solution is correct, the special node replies with FaucetReward (network command "faucetrew"), which contains:

– the next word (8 bytes long byte array that encodes the word as a string with 0 bytes padding at the end)

Moreover, the special node will send you some coins to the address you specified in FaucetRequest.

If at any point, you violate the protocol, your node may be disconnected.


Example of the protocol:

1) After a handshake, your node sends FaucetRequest:

54 46 45 37 52 32 46 53 41 67 41 65 4A 78 74 31
66 67 57 32 59 56 43 68 39 5A 63 34 34 38 66 33
6D 73 00 00 00 00 00 00

which would represent testnet address TFE7R2FSAgAeJxt1fgW2YVCh9Zc448f3ms.


2) The special node replies with the following FaucetChallenge:

25 71 9b 0b e9 d9 96 36 05 0b 2e f9 25 eb de 54
f7 2b 9d 3d 83 ce 2d de fb 28 f0 69 27 4d 33 9e
12 34 56 78 90 AB CD EF 11 22 33 44 55 66 77 88
99 00 AA BB CC DD EE FF 11 12 22 33

The first 32 bytes would represent the hash 25719b0be9d99636050b2ef925ebde54f72b9d3d83ce2ddefb28f069274d339e
and the remaining 28 bytes would represent the challenge data.


3) Then your node is expected to send the following FauceSolution back:

12 34 56 78 90 AB CD EF 11 22 33 44 55 66 77 88
99 00 AA BB CC DD EE FF 11 12 22 33 00 00 FA DE

where the nonce would be 0x0000FADE.


4) If the solution is correct, the special node replies with FaucetReward:

6C 61 6C 61 6C 61 00 00

which would represent the the next word is "lalala".

Solution:

In order to solve this puzzle we need to first construct a payload class:

[Payload("faucetchal")]
public class FaucetChallengePayload : Payload
{
public byte[] Hash;
public byte[] ChallengeData;

public FaucetChallengePayload()
{
this.ChallengeData = new byte[28];
this.Hash = new byte[32];
}

public FaucetChallengePayload(FaucetChallenge challenge)
{
this.ChallengeData = challenge.ChallengeData;
this.Hash = challenge.Hash;
}

public override void ReadWriteCore(BitcoinStream stream)
{
stream.ReadWrite(ref this.Hash, 0, 32);
stream.ReadWrite(ref this.ChallengeData, 0, 28);
}
}

And then the solution payload:

[Payload("faucetsol")]
public class FaucetSolutionPayload : Payload
{
public byte[] ChallengeData;
public byte[] Nonce;

public FaucetSolutionPayload()
{
this.ChallengeData = new byte[28];
this.Nonce = new byte[4];
}

public FaucetSolutionPayload(byte[] challengeData, byte[] nonce)
{
Guard.Assert(challengeData.Length == 28);
Guard.Assert(nonce.Length == 4);

this.ChallengeData = challengeData;
this.Nonce = nonce;
}

public override void ReadWriteCore(BitcoinStream stream)
{
stream.ReadWrite(ref this.ChallengeData, 0, 28);
stream.ReadWrite(ref this.Nonce, 0, 4);
}
}

When we receive the challenge we need to solve it and send the solution back to the server.

Solution can be created like this:

public static byte[] SolveChallenge(byte[] hash, byte[] challengeData)
{
Guard.Assert(hash.Length == 32);
Guard.Assert(challengeData.Length == 28);

uint nonce = MinNonce;

while (nonce < MaxNonce)
{
byte[] nonceBytes = nonce.ToBytes();

List<byte> challengeWithNonce = challengeData.ToList();
challengeWithNonce.AddRange(nonceBytes);

var hashOutBytes = Hashes.SHA256(challengeWithNonce.ToArray());

if (StructuralComparisons.StructuralEqualityComparer.Equals(hashOutBytes, hash))
break;

nonce++;
}

return nonce.ToBytes();
}

When the server responds with a reward payload we need to parse it. Here is the reward payload:

[Payload("faucetrew")]
public class FaucetRewardPayload : Payload
{
public string Word
{
get
{
string result = System.Text.Encoding.UTF8.GetString(this.bytes.Substring(0, this.bytes.ToList().IndexOf(0x0)));
return result;
}
}

private byte[] bytes;

public FaucetRewardPayload()
{
this.bytes = new byte[8];
}

public FaucetRewardPayload(string word)
{
List<byte> bytes = word.ToBytes().ToList();

while (bytes.Count < 8)
bytes.Add(0x0);

this.bytes = bytes.ToArray();
}

public override void ReadWriteCore(BitcoinStream stream)
{
stream.ReadWrite(ref this.bytes, 0, 8);
}
}

The reward would contain word basket.

Also we would receive a transaction of value 1.24326789 STRAT.

Puzzle 6 — broken test

Original assignment:

We are still working with the Stratis full node code here (from GitHub repository branch 
https://github.com/Aprogiena/StratisBitcoinFullNode/tree/experiment/hrpuzz).
The project contains many tests. One of the tests inside Stratis.Bitcoin.Features.MemoryPool.Tests
project fails. Can you help us fixing it?

Once you fix the test properly, you can calculate an ID of the transaction that this test creates.
Take the ID and XOR it with

8c1b2533f9126c6e97416733a5b81d2e3875ab5776d44f8cf91d830f4b541ef7

to reveal the next word.

Solution:

Failing test looks like this:

[Fact]
public async Task AcceptToMemoryPool_WithMinimalOutputAmountPossible_IsSuccessfullAsync()
{
string dataDir = Path.Combine("TestData", nameof(MempoolValidatorTest), nameof(this.AcceptToMemoryPool_WithMinimalOutputAmountPossible_IsSuccessfullAsync));
Directory.CreateDirectory(dataDir);
var minerSecret = new BitcoinSecret(new Key(Encoding.UTF8.GetBytes("00000000000000000000000000000012")), Network.Main);
ITestChainContext context = await TestChainFactory.CreateAsync(Network.Main, minerSecret.PubKey.Hash.ScriptPubKey, dataDir);
IMempoolValidator validator = context.MempoolValidator;
Assert.NotNull(validator);
var destSecret = new BitcoinSecret(new Key(Encoding.UTF8.GetBytes("00000000000000000000000000000034")), Network.Main);

var tx = new Transaction();
tx.AddInput(new TxIn(new OutPoint(context.SrcTxs[0].GetHash(), 0), PayToPubkeyHashTemplate.Instance.GenerateScriptPubKey(minerSecret.PubKey)));

long fee = 100000;
long minAmountPossible = 1;

// spending the minimum amount possible
tx.AddOutput(new TxOut(new Money(minAmountPossible), destSecret.PubKeyHash));

// change
tx.AddOutput(new TxOut(new Money(context.SrcTxs[0].TotalOut.Satoshi - fee - minAmountPossible), destSecret.PubKeyHash));
tx.Sign(minerSecret, false);

MempoolValidationState state = new MempoolValidationState(false);
bool isSuccess = await validator.AcceptToMemoryPool(state, tx);

Assert.True(isSuccess, "P2PKH tx not valid.");

Directory.Delete(dataDir, true);
}

Simply by iterating different fee numbers we can come up with the appropriateminAmountPossible value which is 546.

Hash of the transaction is ac4c405f95320801f9244b13eaea5f676c55c22456ba2af48d3df460393030d7.

Now if we xor 8c1b2533f9126c6e97416733a5b81d2e3875ab5776d44f8cf91d830f4b541ef7 with ac4c405f95320801f9244b13eaea5f676c55c22456ba2af48d3df460393030d7 and convert it to string we will get Well done, ORBIT is next word.

Puzzle 7 — Script

Original assignment:

In order to produce new block on Stratis network, a miner has to be online with running node and have its wallet open. 
This is necessary because at each time slot, the miner is supposed to check whether any of its UTXOs is eligible to
be used as so-called coinstake kernel input and if so, it needs to use the private key associated with this UTXO
in order to produce the coinstake transaction.

A coinstake transaction in Stratis protocol is a special transaction that is at the second position in each block.
The UTXO that is spent in this transaction in its first input (called kernel) has to meet some special properties
in order to be allowed to be used as the kernel.

The chance of a UTXO to be eligible for producing a coinstake transaction grows linearly with the number of coins
that this UTXO presents.

This implies that the biggest miners on the network are required to keep the coins in an open hot wallet. This is
dangerous in case the machine where the hot wallet runs is compromised.

We propose cold staking, which is mechanism that eliminates the need to keep the coins in the hot wallet. With cold
staking implemented, the miner still needs to be online with running the node and open hot wallet, but the coins that
are used for staking, can be safely stored in a cold storage. Therefore the open hot wallet does not need to hold any
significant amount of coins, or it can even be empty.

To implement cold staking, we use a soft fork to redefine OP_NOP10 instruction, newly renamed to
OP_CHECKCOLDSTAKEVERIFY. The new behaviour of this opcode is as follows:

* Check if the transaction spending an output, which contains this instruction, is a coinstake transaction.
If it is not, the script fails.

* Check that ScriptPubKeys of all inputs of this transaction are the same. If they are not, the script fails.

* Check that ScriptPubKeys of all outputs of this transaction, except for the marker output (a special first output
of each coinstake transaction) and the pubkey output (an optional special second output that contains public key
in coinstake transaction), are the same as ScriptPubKeys of the inputs. If they are not, the script fails.

* Check that the sum of values of all inputs is smaller or equal to the sum of values of all outputs. If this does
not hold, the script fails.

* If the above-mentioned checks pass, the instruction does nothing.


Using this new opcode, we can now construct a coinstake transaction output in a way that there are two keys that can
be used to spend the output.

The first key is the key from the hot wallet. We want this key to be usable only for creating another coinstake
transaction that preserves this limitation to the newly created output.

The second key is the key from the cold storage. We want this key to be able to spend the output arbitrarily.


Your task in this puzzle is to create a ScriptPubKey of an output of such a cold staking coinstake transaction that
utilizes the new opcode. The ScriptSig to spend that output for another cold staking transaction using a hot wallet
key is:

<sig> <hotPubKey> 1


The ScriptSig to spend that output arbitrarily using a cold storage key is:

<sig> <coldPubKey> 0


Write your ScriptPubKey on a single line with opcodes written with capital letters and instead of hot wallet public
key hash use <hotPubKeyHash> and instead of cold storage public key hash use <coldPubKeyHash>. Use a single space
as the separator. There is no new line character or separator at the end. Use the P2PKH template as an inspiration.
Note that there are many ways on how to write the script, so let's agree on using OP_CHECKCOLDSTAKEVERIFY just
before OP_CHECKSIG. We are not looking for the shortest possible version of how to write the output to the chain,
which is prevented by the agreed specific position of OP_CHECKCOLDSTAKEVERIFY, but we make that condition in order
to eliminate many ways of writing the solution. Even with the condition, there are many ways left, but few make
good sense.

For example, if you were about to construct P2PKH ScriptPubKey for the hot wallet key, you would write:

OP_DUP OP_HASH160 <hotPubKeyHash> OP_EQUALVERIFY OP_CHECKSIG

And then you would calculate its SHA256 hash, which is

a58b0f14654292b0d0ce2b9958fa6243330acfd083b95fcf985343bf2f21c17f


Once you are done, calculate SHA256 hash of your cold staking coinstake output ScriptPubKey. Then XOR the hash with

32c1e59540ea97bbd8d3d41df83946868ddd8656e0942b3d0df65499609c00e2

to reveal the next word.

Solution:

By trying different scripts which satisfy the requirement we will come up with the following script:

OP_IF OP_DUP OP_HASH160 <hotPubKeyHash> OP_EQUALVERIFY OP_CHECKCOLDSTAKEVERIFY OP_CHECKSIG OP_ELSE OP_DUP OP_HASH160 <coldPubKeyHash> OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF

If we hash it using SHA256 we will get: 128CA0C608CADAFE8B9BF450BD6A0EA6C098D51EC0D96E6E45D619DC33D420C2.

Now we xor 128CA0C608CADAFE8B9BF450BD6A0EA6C098D51EC0D96E6E45D619DC33D420C2 with 32c1e59540ea97bbd8d3d41df83946868ddd8656e0942b3d0df65499609c00e` and get the following string:

MESH MESH MESH MESH MESH MESH .

Puzzle 8 — hidden fork

Original assignment:

Once again you will need to work with the full node code from this GitHub repository branch:

https://github.com/Aprogiena/StratisBitcoinFullNode/tree/experiment/hrpuzz

A secret faction of Stratis forked away from Stratis test net. Only one node is known to be operating on this secret network.
It operates on 52.178.220.228:35353. Your task is to infiltrate their network and sync with their chain.

Solution:

First we need to connect to that node by applying -connect=52.178.220.228:35353 to the config or command line arguments.

After doing that we would get the following in the console:

info: Stratis.Bitcoin.Connection.ConnectionManagerBehavior[0]
Peer '[::ffff:52.178.220.228]:35353' offline, reason: 'A banned node tried to connect.'.

It appears that the target node is banned. After investigation why we find out the source of the problem:

this.BanPeer(new IPEndPoint(IPAddress.Parse("52.178.220.228").EnsureIPv6(), 35353), 12345678, "Once you sync with their chain, find the block number 373052 and inspect it.");

in PeerBanning.cs.

If we remove this line we will now get a different connection error that we need to investigate. The node would send us RejectPayload with a message Your version is outdated! Use 90000.

Now if we fake our version to be 90000 and connect we will be able to start syncing but only until the block 373050 and then we will get an error.

To fix this error we need to remove a checkpoint:

{ 373050, new CheckpointInfo(new uint256("0x1347a27e3e52bd1a0b7bad7e9d8257c3e81793be911afe826f606bbcb4d2fff7"), new uint256("0xc9d7ed7835ec1fcea87031b3bb08d74d3bbdc97278a8ef402da3264070069b43")) },

And only after that we will be able to sync till the block 373052 as mentioned in the hint we got in the step 2.

Now we inspect this block and find out the following text in the coinbase transaction: Next is ‘glare’.

Puzzle 9 — largest transaction

Original assignment:

What is the TXID of the largest (in terms of its size in bytes) TX on Stratis testnet before block 400000?

Is it bdc206c4ef88ebe64b68c25fab307c55b3c0cc0725120d2a8356d73cce7f19af? No, it is not.

Solution:

We can check every transaction of each block after syncing enough blocks and then using BlockRepository.GetAsync to retrieve all the blocks one by one and then check every transaction in every block. For every transaction we need to calculate serialized size using Transaction.GetSerializedSize method.

By checking all the transactions in every block before 400000 we find the biggest transaction: fcae6bab9cfccb92230db03a8b0a5575f4afec734a32694bed31b24ee00b61db.

By XORing it with bdc206c4ef88ebe64b68c25fab307c55b3c0cc0725120d2a8356d73cce7f19af we receive the following text: Almost there :) Go to danger.txt

Puzzle 10 — vanity address

Original assignment:

Next one is easy. Just generate a private key and public key pair, such that the mainnet Stratis 
address representing the public key starts with "STRAT" (case-sensitive).

Then connect to a special node on testnet that listens on this IP address: 52.233.33.138 and after
a standard handshake send it VanityRequest payload and receive VanityResponse payload.

VanityRequest (network command "vanityreq") is defined as:

– compressed public key P, which address on Stratis mainnet starts with "STRAT" (33 bytes long byte array)
- length L of signature (1 byte)
- signature of SHA256d(P) created with the corresponding private key (L bytes long byte array)

If the solution is correct, the special node replies with VanityResponse (network command "vanityres"), which contains:

– the next word (8 bytes long byte array that encodes the word as a string with 0 bytes padding at the end)

Solution:

To solve this we need to generate a vanity address:

public static void VanityKey()
{
Network network = Network.StratisMain;

int i = 0;
var start = DateTime.Now;

Parallel.For(0, int.MaxValue, new ParallelOptions { MaxDegreeOfParallelism = 16 }, (int iteration) =>
{
var result = Interlocked.Increment(ref i);

if (result % 50000 == 0)
{
var triesPerSec = result / (DateTime.Now - start).TotalSeconds;

Console.WriteLine("i: {0} speed: {1}", result, triesPerSec);
}

if (TryGeneratePrivKeyWhichAddrStartsWith("STRAT", network, out byte[] seed, out string addressStr))
{
string byteStr = "";
foreach (var b in seed)
byteStr += b.ToString() + " ";

Console.WriteLine("FOUND IT: " + addressStr + " BYTES: " + byteStr);

return;
}
});

Console.ReadLine();
}

Payloads:

[Payload("vanityreq")]
public class VanityRequestPayload : Payload
{
public byte[] pubKeyBytes;

public byte[] signatureBytes;

public VanityRequestPayload()
{
this.pubKeyBytes = new byte[33];
}

public VanityRequestPayload(byte[] pubKeyBytesArray, byte[] signatureBytes)
{
this.pubKeyBytes = pubKeyBytesArray;
this.signatureBytes = signatureBytes;
}

public override void ReadWriteCore(BitcoinStream stream)
{
stream.ReadWrite(ref this.pubKeyBytes, 0, 33);

byte signatureSize = 0;

if (stream.Serializing)
signatureSize = (byte)this.signatureBytes.Length;

stream.ReadWrite(ref signatureSize);

if (!stream.Serializing)
this.signatureBytes = new byte[signatureSize];

stream.ReadWrite(ref this.signatureBytes, 0, signatureSize);
}

public static string GetAddrFromPubKey(byte[] pubKeyBytes)
{
var pubkey = new PubKey(pubKeyBytes);
BitcoinPubKeyAddress address = pubkey.GetAddress(Network.StratisMain);

string addressStr = address.ToString();
return addressStr;
}
}
[Payload("vanityres")]
public class VanityResponsePayload : Payload
{
public string Word
{
get
{
string result = System.Text.Encoding.UTF8.GetString(this.bytes.Substring(0, this.bytes.ToList().IndexOf(0x0)));
return result;
}
}

private byte[] bytes;

public VanityResponsePayload()
{
this.bytes = new byte[8];
}

public VanityResponsePayload(string word)
{
List<byte> bytes = word.ToBytes().ToList();

while (bytes.Count < 8)
bytes.Add(0x0);

this.bytes = bytes.ToArray();
}

public override void ReadWriteCore(BitcoinStream stream)
{
stream.ReadWrite(ref this.bytes, 0, 8);
}
}

Payload construction:

var seedBytes = new byte[] { 62, 48, 83, 73, 57, 67, ...... };

var key = new ExtKey(seedBytes);

PubKey pubkey = GeneratePublicKey(key.Neuter().ToString(Network.StratisMain), 0, false);
BitcoinPubKeyAddress address = pubkey.GetAddress(Network.StratisMain);

var keyPath = new KeyPath("0/0");
ExtKey privKey = key.Derive(keyPath);
byte[] pubKeyBytes = pubkey.ToBytes();
byte[] signatureBytes = this.Sign(privKey.PrivateKey, Hashes.Hash256(pubKeyBytes));

var payload = new VanityRequestPayload(pubKeyBytes, signatureBytes);

if (!address.ToString().StartsWith("STRAT"))
throw new Exception("BAD PUBKEY BYTES");

In that vanity response payload we will receive word able in case we generated and sent pubKey for a valid vanity address and the signature produced by the private key associated with that address.

Puzzle 11 — mutated block

Original assignment:

To get the next word, exploit CVE-2012-2459 against a block number 333445 on the Stratis test net. 
Once you create such a block, connect to a special node that is running on IP address 52.233.33.138
and present that block using Block2 payload to the node after a normal handshake.

Block2 payload is exactly the same as Block payload, except for its network command, which is "block2".

If the solution is correct, the special node replies with Phrase payload (network command "phrasere"), which contains:

– instructions message (150 bytes long byte array that encodes next instructions as a string with 0 bytes padding at the end)

Solution:

Explanation of that vulnerability: https://bitcointalk.org/index.php?topic=102395.0

Construction of the mutated block will look like this:

var mutatedBlock = block333445.Clone(ProtocolVersion.PROTOCOL_VERSION, Network.StratisMain);

var txToCopy1 = mutatedBlock.Transactions[mutatedBlock.Transactions.Count - 2];
var txToCopy2 = mutatedBlock.Transactions[mutatedBlock.Transactions.Count - 1];

mutatedBlock.Transactions.Add(txToCopy1.Clone(false, Network.StratisMain));
mutatedBlock.Transactions.Add(txToCopy2.Clone(false, Network.StratisMain));

mutatedBlock.UpdateMerkleRoot();

if (mutatedBlock.GetHash() != block333445.GetHash())
throw new Exception("Merkle root changed!");

this.BlockMerkleRoot(mutatedBlock, out bool mutated);
if (!mutated)
throw new Exception("Not mutated");

var payload = new MutatedBlockPayload(mutatedBlock);

To a valid mutated block the server will answer with a payload that will contain:

Next word is ‘engage’. Also note that the 7th word you received is actually word number 8, and 8th word you received is word number 7.

Puzzle 12 — entropy

Original assignment:

Missing some entropy? Okay, here is some:1110001

Solution:

After reading BIP39 and in general how HD wallets work we learn how mnemonic is generated.

Here are the words from the previous puzzles:

market perfect seed start basket orbit glare mesh danger able engage

Their numbers in the words list:

1089 1304 1559 1701 153 1247 790 1118 443 2 594

And their binary representation is:

10001000001
10100011000
11000010111
11010100101
00010011001
10011011111
01100010110
10001011110
00110111011
00000000010
01001010010

To which we append provided entropy: 1110001

And then make SHA256 of the whole entropy. First 4 bits from the result of the SHA256 is the missing checksum which is 1111 in binary.

Now add the checksum to provided entropy to get: 11100011111, which is the index of the last word in binary, which is 1823 in decimal, which stands for the word token.

Puzzle 13 — secret puzzle

Original assignment:

Congratulations! Now you can restore the wallet (you can use Swagger on the C# full node!), the password is value in satoshis (or stratoshis?) 
you received from the faucet puzzle (1.23 STRATS would translate to password "123000000"), account is "account 0".

Solution:

The password for the wallet is the amount in satoshis we received in faucet puzzle: 124326789.

Wallets implementing BIP44 usually use gap limit of 20. Which would mean that the node would only see the prize if it would be deposited in the first 20 addresses. However we deposited the prize to address under index 1033. By manually deriving more addresses the node will be able to see the prize and spend it.

Thanks for puzzling!

Cheers,

Aprogiena, NoEscape0 and Khilone

https://twitter.com/Khil0ne

https://medium.com/@khilonecrypto

--

--

Khilone
Khilone

Writing articles about Stratis Platform, Beaxy Exchange and Crypto in general — https://twitter.com/Khil0ne