This is a writeup and example implementation for puzzle #9 by https://nebolsin.keybase.pub/puzzles/
Our clue was given in form of the pubkey GBW7N7EXR5MV4A34N7LEQGSKZMFEJGW4SQWHUPXGDX2JCGNJH2RXKHUL
When looking up the accounts state we find an threshold setting of 2/3/4
, 2 active signers (and one to clawback). The master key has been removed.
The account also posseses an data entry containing
More puzzles: https://erayd.keybase.pub/puzzles.html. Have fun!
<=>
Puzzle9//by+nebolsin//The+Clueless+Multisig=
where the second part is base64 encoded.
All operations required to put the account in its base state are performed in a single transaction.
The submitted XDR is as follows:
AAAAAJQDnG831sTaSXvoh8GYWdlJQ69AEu1zFjaTqoGfJ3a3AAACWAGbM7AAAAAEAAAAAAAAAAAAAAAGAAAAAAAAAAEAAAAAbfb8l49ZXgN8b9ZIGkrLCkSa3JQsej7mHfSRGak+o3UAAAAAAAAAACqeCYgAAAABAAAAAG32/JePWV4DfG/WSBpKywpEmtyULHo+5h30kRmpPqN1AAAABQAAAAEAAAAA6KF3fpZCZZEshXSN3u/zC4N2RFxA7TymoYrfI6IiIzUAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAJQDnG831sTaSXvoh8GYWdlJQ69AEu1zFjaTqoGfJ3a3AAAABQAAAAEAAAAAbfb8l49ZXgN8b9ZIGkrLCkSa3JQsej7mHfSRGak+o3UAAAAFAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAACi9BDLJjzLiYzOrD+jN9LTE67InNSUe6IYLKENF06kVkAAAABAAAAAQAAAABt9vyXj1leA3xv1kgaSssKRJrclCx6PuYd9JEZqT6jdQAAAAUAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEH7YqDp4KUmJ6ZASBrWwllDkh8GfmXHdtnFdWJPC39FQAAAAMAAAABAAAAAG32/JePWV4DfG/WSBpKywpEmtyULHo+5h30kRmpPqN1AAAABQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAgAAAAEAAAADAAAAAQAAAAQAAAAAAAAAAAAAAAEAAAAAbfb8l49ZXgN8b9ZIGkrLCkSa3JQsej7mHfSRGak+o3UAAAAKAAAAP01vcmUgcHV6emxlczogaHR0cHM6Ly9lcmF5ZC5rZXliYXNlLnB1Yi9wdXp6bGVzLmh0bWwuIEhhdmUgZnVuIQAAAAABAAAAID7s85Xvf/28vp3m6JbIp//04XvgpbnpXrLPjLpbYrIoAAAAAAAAAAKfJ3a3AAAAQG7kycI2fAlbGS7tUPkcpd7fj0qiJnQIaxI3t1OemQTxLVKaQgDr+0UgMwPIGFXWgVTFLrtWyci8aVXHO3HmWgSpPqN1AAAAQLF6GamqadZBtre3tAspGywIWPe4YakyhmtZcOunBJ3pWfWmJ1h3kwRiRv30kTiKkOcX3K7WNq44SJc78zUUcQ4=
After decoding the Transaction Envelope we find a fee of 600
stroops for 6 operations and no timebound.
We can thus conclude that the used baseFee
is 100
stroops.
The two previously mentioned signers are defined as follows:
-
Signer1
- type:
hashX
- hashValue:
8bd0432c98f32e26333ab0fe8cdf4b4c4ebb22735251ee8860b284345d3a9159
- weight:
3
- type:
-
Signer2
- type:
preAuthTx
- hashValue:
07ed8a83a78294989e9901206b5b09650e487c19f9971ddb6715d5893c2dfd15
- weight:
1
This being told we can see:
- type:
-
With only
Signer2
we are unable to perform any transactions -
With only
Signer1
we are able to perform low and medium threshold transactions:- Basically all operations but
set Options
andAccount merge
- Basically all operations but
As our final goal is to merge away or at least payout the content of this account we may argue that we will need a combination of both signers.
To be able to do anything with this account we therefore must crack Signer1
:
Signer1
is a hash signer, we thus need to find some data (byte[]
) - also called "preimage" - which after calculating its sha256 hash is equal to the given hashValue derrived from the ledger. We then can use the preimage encoded in hex to sign any of our transactions with an weight of 1
.
On the first glance over the account we noticed a strange data entry.
At first its purpose may seem to brand the challenge account with the challenge website, but uppon further inspection we notice something strange:
Normally an data entry has the form $key <=> base64($value)
but in this case the base64 encoded value field (Puzzle9//by+nebolsin//The+Clueless+Multisig=
) is already in reasonable plain text ($key <=> $value
).
Very curious...
After stumbeling upon this curiosity one may want to try this strange value as their preimage.
The obvious seems to use the given value as a plain string for our preimage, but the resulting hash does not match the given one.
A little more puzzeling around and getting more and more fustrated over all the base64 strangeness one will eventually come to use the base64 decoded value (some gibberish) as the preimage and (Eureka!) it fits our hashX signer.
We therefore find our hashX preimage to be
base64_decode(Puzzle9//by+nebolsin//The+Clueless+Multisig=)
Signer2
is a pre-authorized transaction.
As our final goal is to merge away / payout the reward to an abitrary target and the pre-authorized transaction must not include our public key (else it would be non deterministic) the pre-auth'ed transaction must include a setOptions
operation giving us more control over the puzzle account.
To come to our desired outcome we must therefore have a final signing weight of 3
(payment operation) or even 4
(merge / full control).
We already have the known signer Signer1
with weight 1
and thus only need another signer (or increase an existing one) with weight 2
/ 3
The required threshold to be able to use a setOptions
operation is high
(4
in our case).
As the pre-auth'ed transaction has only a weight of 3
it on its own will not be able to complete the required operation.
This is where our Signer1
comes into play. If we sign our potential setOptions
transaction with our hashX signer, the resulting transaction will have the required weight of 4
.
Now we can play the game of edcuated guessing potential setOptions
parameters, where we can already set the following:
- The base fee of the setup transaction was
100
stroops, therefore we can conclude with reasonable certainty that our base fee should be the same.
This also matches with the balance of the puzzle account (reward + 2.50002
) where2.5
XLM are required as base reserves and two transactions will be required to solve the puzzle and transfer the reward to ones account. - In each
setOptions
operation used in the setup transaction thesetFlags
andclearFlags
field was set to0
. (After speaking with the puzzle author it seems like this is a default value of many stellar sdks) - As sequence numbers must be successive (if not using
bumpSeq
) the puzzle would be unsolveable if the sequence number was not the next sequence number. - The
timeBound
of the creating transaction was set to None (Rhis hint was also amplified by the author, as in most SDKs the default value for this field was changed over the years to now(0,0)
)
As it seems that we do not have access to the secret key of the puzzle account it would be reasonable to assume that we need to change some threshold weights or increase a known signer (e.g. Signer1
) to the required value.
After spending some hours trying different combinations of parameters and questioning their sanity this is were most of the competitors became stuck.
Only after @SiD mentioning something of them having the secret key
another idea struck.
The hidden signer
Most of the time a keypair in steller is generated randomly using something like Keypair.random()
but there is also the lesser known method of creating a secret key from a seed.
After the helpful tip from @SiD it came to mind that maybe the full keypair of the puzzle account can be derived from some clue hidden on the ledger. (Eureka!)
It appears that the ed25519 keypair derived from the same pre image used for the hashX signer (i.e. using from_raw_ed25519_seed
) matches our puzzle accounts keypair.
There now is a new way to gain the required signer weight of 3
or 4
:
If we are able to recover the master signing key with a weight of 2
or 3
we can combine it with the hashX signer for the required weight.
This gives us two more possibilites for the setOptions
transaction of Signer2
and after a little more trial and error we find the matching hash by creating the following transaction:
baseFee
: 100timeBounds
: None(!)setOptions
:masterWeight
: 2setFlags
: 0clearFlags
: 0
This now allows us to create an abitrary transaction with medium
threshold (e.g payment
) by signing it using Signer1
and the master Key
.
Beautiful! Really.. just absolutely marvelous 💪 🎉