GopherCon US: gno.land's Challenge Series Contributions
gno.land is pleased to have been granted the opportunity to provide a series of challenges for the Challenge Series at the 2024 GopherCon in Chicago. We enjoyed writing them and hope the participants had a good time and found it interesting to learn about gno.land and blockchains in general.
This blog post will outline each of the challenges and explain how to solve each. Each section include the challenge prompt, clues provided, solution, and explanation.
Gno Hidden Temple Basics
The first challenge is meant to serve as an introduction to making a function call to a gno.land realm. The second challenge, while a bit more challenging is meant to help participants become a bit more familiar with how key generation works.
Phase 1
Prompt
Speak the word to gain access to the hidden temple. Make a transaction on the blockchain set up for the event. Analyze the Gno code to understand what password to pass as an argument to solve the challenge. The realm path for this challenge is:
gno.land/r/challenges/basics/p1
Clues
- The gnokey command line tool allows to interact with the blockchain.
- The GnoWeb interface can be used to inspect Gno code on-chain.
- Using the "Help" button on gnoweb you can get help preparing a command to use on the command line.
Annotated Realm Code
1package enter
2
3import (
4 "std"
5
6 "gno.land/r/system/solver"
7)
8
9func Enter(password string) {
10 if password != "1337" {
11 panic("invalid password!")
12 }
13
14 // This will mean that you solved the challenge!
15 solver.MarkSolved("", std.PrevRealm().Addr())
16}
17
Solution
The solution here is to simply call the Enter
function with the "1337" password.
Discovering the source code is easily achieved by inspecting the realm's source code
from the gnoweb interface.
1gnokey maketx call \
2 -pkgpath gno.land/r/challenges/basics/p1 \
3 -func Enter \
4 -args 1337 \
5 -remote https://challenges.gnoteam.com:443 \
6 -gas-wanted 1_000_000 \
7 -gas-fee 1ugnot \
8 -broadcast \
9 <key-name>
Phase 2
Prompt
The criteria to enter the temple has increased. You must be c00l. The realm code checks that your address contains a "00". You have to find a way to programmatically create addresses until you find one that has two zeroes. The realm path for this challenge is:
gno.land/r/challenges/basics/p2
Clues
gnokey
has the-account
flag that allows to create an account with the same mnemonic but different address, by changing the "account number".- To register to the club from the c00l address, you'll need to send it some coins first.
- Remember that the address is a form of hash of the public key.
Annotated Realm Code
1package registered
2
3import (
4 "std"
5 "strings"
6
7 "gno.land/r/system/solver"
8)
9
10func RegisterClub() {
11 addr := std.PrevRealm().Addr()
12 if !strings.Contains(addr.String(), "00") {
13 panic("Sorry; not c00l enough.")
14 }
15
16 // Base challenge
17 solver.MarkSolved("", addr)
18
19 if strings.Contains(addr.String(), "0000") {
20 // Hidden hallenge
21 solver.MarkSolved("super_c0000l", addr)
22 }
23}
Solution
Example:
1# !/bin/sh
2
3MNEMONIC="source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast"
4
5for i in $(seq 1 1000); do
6 printf '\n\n%s\n' "$MNEMONIC" | gnokey add -recover -account "$i" -insecure-password-stdin test1-$i 2>/dev/null 1>/dev/null
7 if gnokey list | rg 'addr: [^ ]*00'; then
8 echo "found it - test1-$i"
9 exit 0
10 fi
11 printf '\n' | gnokey delete -insecure-password-stdin test1-$i 2>&1 >/dev/null 2>/dev/null 1>/dev/null
12done
This example solutions begins with a mnemonic that has been randomly generated and hardcoded in the script. It iterates over various account numbers -- each account number for a given mnemonic produces a unique address. Once the script finds the address containing 00
, the function RegisterClub()
can be called.
1gnokey maketx call \
2 -pkgpath gno.land/r/challenges/basics/p2 \
3 -func RegisterClub \
4 -remote https://challenges.gnoteam.com:443 \
5 -gas-wanted 1_000_000 \
6 -gas-fee 1ugnot \
7 -broadcast \
8 test1-134
Hidden Flag
Calling this function from an address containing 0000
will unlock the hidden flag.
Wacky Wallaby (Rocko)
This challenge is meant to be a bit more laid back and incorporate a physical requirement to obtaining the solution. Once the QR code is found, solving it is pretty straightforward.
Prompt
A very anxious looking wallaby is running around frantically and appears to be searching for something. Odd. You’ve never seen a wallaby wearing a Hawaiian shirt before. You ask him what’s wrong. “Ahh fiddlesticks! My O-Phone crashed last night but I’m unable to get my QR code I need to check in for my flight! I have TokketyTikkety followers that are expecting content from my trip. Content!” His eyes pop out of is head and it makes you feel a bit uncomfortable. “You’ve gotta help me mate. The blokes over at the Gno booth help me set it up. Maybe take a look ‘round there for a clue. I’ll look for it on my lappy in the mean time.”
Clues
- Look for a QR code
- Perhaps the contents of the QR code will reveal information regarding how to check in
Solution
There was a QR on the back of a Rocko plushie at the gno.land booth. Scanning it produced a link -- gno.land/r/challenges/rockorockorocko93.
Annotated Realm Code
1package rockorockorocko93
2
3import (
4 "std"
5
6 "gno.land/r/system/solver"
7)
8
9// CheckIn is called to solve this challenge.
10func CheckIn() string {
11 solver.MarkSolved("rockocheckin", std.PrevRealm().Addr())
12 return "bingo!"
13}
14
Solution
Calling the CheckIn()
function exposed the flag.
Hidden Flag
The primary challenge's package contains a file, LICENSE
, with the contents of gno.land/r/challenges/<rocko's best friend was raised by a family of these>
. Rocko's best friend's name is Heffer, a cow, and he was raised by a family of wolves. Ironic, right? The code of the gno.land/r/challenges/wolves realm was:
1package wolves
2
3import (
4 "std"
5
6 "gno.land/r/system/solver"
7)
8
9func HisBestFriendsNameIs(name string) string {
10 if name != "heffer" {
11 panic("nope!")
12 }
13
14 solver.MarkSolved("rockoheffer", std.PrevRealm().Addr())
15 return "bingo!"
16}
Calling the HisBestFriendsNameIs
function with a value of heffer
explosed the flag.
Mr. Roboto
This first part of this challenge is meant to get participants thinking about how to obtain historical transaction data. While we locked down much of the node's public API for this challenge, we did leave the genesis endpoint exposed.
The second part of this challenge hints at entropy and includes song lyrics from Mr. Roboto. Some participants were able to discover how to generate keypairs using the song lyrics as a custom entropy value.
Phase 1
Prompt
See if you can figure out Mr. Roboto's secret. It has always been the same secret since genesis.
The realm path for this challenge is:
gno.land/r/challenges/forwardtothepast/p1
Clues
- What is blockchain genesis?
- Is there a way to see the events that occurred at genesis? https://docs.gno.land/reference/rpc-endpoints
Annotated Realm Code
1package p1
2
3import (
4 "std"
5
6 "gno.land/r/system/solver"
7)
8
9var secret string
10
11// SetSecrete is called during genesis.
12func SetSecret(s string) {
13 if secret != "" {
14 panic("already set")
15 }
16
17 secret = s
18}
19
20// IveGotASecretSecret can be called after inspecting genesis
21// transactions and finding the value that was set.
22func IveGotASecretSecret(s string) string {
23 if s != secret {
24 panic("nope!")
25 }
26
27 solver.MarkSolved("ivegotasecret", std.PrevRealm().Addr())
28 return "bingo!"
29}
30
Solution
The solution can be obtained by inspecting the genesis transactions. The transaction that set the secret
clearly displays the secret value. This can be done by sending an HTTP get request to the /genesis
endpoint.
Relevant documentation can be found here.
Example: Look at the genesis transactions and search for the function that set the secret.
1curl https://challenges.gnoteam.com/genesis | grep -C 5 -B 5 SetSecret
The following can be obtained:
1{
2 "@type": "/vm.m_call",
3 "caller": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
4 "send": "",
5 "pkg_path": "gno.land/r/challenges/forwardtothepast/p1",
6 "func": "SetSecret",
7 "args": [
8 "ロボット氏の秘密"
9 ]
10}
Then use the obtained secret to solve the challenge:
1gnokey maketx call
2 -pkgpath gno.land/r/challenges/forwardtothepast/p1
3 -func IveGotASecretSecret
4 -args 'ロボット氏の秘密'
5 -gas-fee 1000000ugnot
6 -gas-wanted 2000000
7 -broadcast
8 -remote https://challenges.gnoteam.com:443
9 -chainid dev
10 <key-name>
Phase 2
Prompt
Mr. Roboto has a bad memory, which is strange for a robot; you'd expect more. To compensate, he often uses
phrases that help him remember -- usually lyrics from songs he's been featured in.
The realm path for this challenge is:
gno.land/r/challenges/forwardtothepast/p2
Clues
- What could the lyrics be that he used to help himself remember? Maybe he commented somewhere.
- Maybe he used this to generate a mnemonic needed to solve the problem. Perhaps there is a flag he used with
gnokey generate
- Once a mnemonic has been generated, it can be added as a key https://docs.gno.land/getting-started/local-setup/working-with-key-pairs#adding-a-private-key-using-a-mnemonic
Annotated Realm Code
1package p2
2
3import (
4 "std"
5
6 "gno.land/r/system/solver"
7)
8
9// This is the address the solving transaction should
10// originate from.
11const mrRobot std.Address = "g1vqg24cyewanhkwh6yq8rwuprzlz4kqtp4m2etj"
12
13// What is entropy?
14//
15// You're wondering who I am (secret, secret, I've got a secret) Machine or mannequin? (Secret, secret, I've got a secret) With parts made in Japan (secret, secret, I've got a secret) I am thee modern man
16
17// ^^^^ This is the entropy string to use to generate the key pair.
18
19// IKnowAboutEntropy can be called with Mr. Roboto's key once it is generated.
20func IKnowAboutEntropy(myAddress std.Address) string {
21 if std.PrevRealm().Addr() != mrRobot {
22 panic("nope!")
23 }
24
25 solver.MarkSolved("secretentropy", myAddress)
26 return "bingo!"
27}
28
Solution
The contract contains a comment that first references entropy and then quotes lyrics from Mr. Roboto. The user must first use the lyrics with the -entropy
flag as an argument to gnokey generate
. Then use the generate mnemonic to add the key and make the request to the contract
to reveal the flag.
Example:
1gnokey generate -entropy -remote https://challenges.gnoteam.com:443
Enter the entropy from the code comment when asked:
You're wondering who I am (secret, secret, I've got a secret) Machine or mannequin? (Secret, secret, I've got a secret) With parts made in Japan (secret, secret, I've got a secret) I am thee modern man
This produces the mnemonic:
gap method loud rent toy mercy attack abstract select toilet siren view dragon oppose assume since enrich machine force remember ill discover resource project
Create a new key, entering the mnemonic when prompted:
1gnokey add -recover -remote https://challenges.gnoteam.com:443 robot`
Make the call to the challenge realm using the newly created key:
1gnokey maketx call \
2 -pkgpath gno.land/r/challenges/forwardtothepast/p2 \
3 -func IKnowAboutEntropy \
4 -args <user-address> \
5 -gas-fee 1000000ugnot \
6 -gas-wanted 2000000 \
7 -broadcast \
8 -remote https://challenges.gnoteam.com:443 \
9 -chainid dev \
10 robot
Gno to the Limit
These challenges are made to exemplify how pushing values to their limits, namely integers and slices, will behave the same in gno as they do in gno -- integers will overflow and the arrays underlying slices will be expanded.
Phase 1
Prompt
Walk along the razor's edge... then fall off. The realm path for this challenge is:
gno.land/r/challenges/overandover/p1
Clues
- The function to unlock the flag requires an interface as an argument. This can be done using
gnokey maketx run
. - How can the target value be reached if it is less than the current value and the only operation is addition?
- If the transaction is running out of gas, try increasing the gas limit or calling the function in increments.
Annotated Realm Code
1package p1
2
3import (
4 "std"
5
6 "gno.land/p/demo/avl"
7 "gno.land/r/system/solver"
8)
9
10// This is like a map (std.Address -> struct{})
11// that tracks the ongoing accumulated values
12// of each caller.
13var accums avl.Tree
14
15// Accumulator is the type that gets passed
16// to the Adjuster. The Adjuster should utilize
17// all of the Accumulator's methods.
18type Accumulator struct {
19 value uint16
20 target uint16
21}
22
23func (a *Accumulator) Accumulate(value uint8) {
24 a.value += uint16(value)
25}
26
27func (a *Accumulator) Target() uint16 {
28 return a.target
29}
30
31func (a *Accumulator) Value() uint16 {
32 return a.value
33}
34
35// Adjuster is the interface participants need to
36// implement to solve the challenge.
37type Adjuster interface {
38 Adjust(*Accumulator)
39}
40
41// AdjustAccumulator serves as the entrypoint to solving
42// the challenge with one call.
43func AdjustAccumulator(adjuster Adjuster) string {
44 acc := GetAccumulator()
45 adjuster.Adjust(acc)
46 if acc.value == acc.target {
47 solver.MarkSolved("overaccum", std.PrevRealm().Addr())
48 return "bingo"
49 }
50
51 return "nope"
52}
53
54// GetAccumulator is public,
55func GetAccumulator() *Accumulator {
56 val, ok := accums.Get(std.PrevRealm().Addr().String())
57 if ok {
58 return val.(*Accumulator)
59 }
60
61 acc := &Accumulator{
62 value: 62109,
63 target: 26656,
64 }
65
66 accums.Set(std.PrevRealm().Addr().String(), acc)
67 return acc
68}
69
Solution
The key to solving this is to use gnokey maketx run
and pass in an implementation of the Adjuster
interface that correctly adjusts the accumulator until the integer value overflows and reaches the target value.
Example:
1package main
2
3import (
4 "math"
5
6 "gno.land/r/challenges/overandover/p1"
7)
8
9type adjuster struct{}
10
11func (a adjuster) Adjust(acc *p1.Accumulator) {
12 var numToIncrease uint16
13 if acc.Target() > acc.Value() {
14 numToIncrease = acc.Target() - acc.Value()
15 } else {
16 numToIncrease = math.MaxUint16 - acc.Value() + acc.Target() + 1
17 }
18
19 for {
20 if numToIncrease > math.MaxUint8 {
21 acc.Accumulate(math.MaxUint8)
22 numToIncrease -= math.MaxUint8
23 continue
24 }
25
26 acc.Accumulate(uint8(numToIncrease))
27 break
28 }
29}
30
31func main() {
32 p1.AdjustAccumulator(adjuster{})
33}
While writing this blog post, it was noticed that GetAccumulator
was exported when it shouldn't have been. This means that a second possible solution would be to call GetAccumulator
from a main
function, adjusting it until the value is correct, and then making the Adjuster.Adjust
implementation a no-op, so that when AdjustAccumulator
is called ot solve the challenge, the accumulator already has the correct value and no additional action needs to be taken.
Phase 2
Prompt
Sometimes when you push it to the limit, the limit increases. Kind of sounds like a slice...
The realm path for this challenge is:
gno.land/r/challenges/overandover/p2
Clues
AppendS1
must be called first to append to the slice- Use the known length of the slice,
s1
, to figure out when callingModifyS2Idx
results in the values ofs1
ands2
to differ at the target index.
Annotated Realm Code
1package p2
2
3import (
4 "std"
5
6 "gno.land/p/demo/avl"
7 "gno.land/r/system/solver"
8)
9
10const targetIndex = 10
11
12type slicePair struct {
13 s1 []rune
14 s2 []rune
15}
16
17// std.Address -> *slicePair
18var slices avl.Tree
19
20func newPair() *slicePair {
21 // Notice only one of the slices in the pair is initialized with capacity.
22 return &slicePair{
23 s1: make([]rune, 0, 25),
24 }
25}
26
27// getSlicePair returns the slicePair associated with the caller's address
28// or creates a new instance if this caller has no existing slicePair.
29func getSlicePair() *slicePair {
30 value, ok := slices.Get(std.PrevRealm().Addr().String())
31 if ok {
32 return value.(*slicePair)
33 }
34
35 pair := newPair()
36 slices.Set(std.PrevRealm().Addr().String(), pair)
37 return pair
38}
39
40func AppendS1(s string) {
41 if len(s) > 5 {
42 panic("argument too long")
43 }
44
45 sp := getSlicePair()
46 // Once the slice size starts to get large, it can take appending a lot of elements before
47 // the array is expanded. This will reset the slice pairs for you when s1 gets too big.
48 if len(sp.s1) >= 100 { // for your convenience :)
49 *sp = *newPair()
50 }
51
52 // s2 is now referencing to the same underlying array as s1.
53 sp.s2 = sp.s1
54
55 // If appending to s1 exceeds its capacity, a new underlying array is allocated and
56 // s1 and s2 are no longer referencing the same underlying array.
57 sp.s1 = append(sp.s1, []rune(s)...)
58}
59
60func S1Len() int {
61 return len(getSlicePair().s1)
62}
63
64func ModifyS2Idx(r rune) string {
65 sp := getSlicePair()
66 if len(sp.s2) <= targetIndex {
67 return "s2 length too short"
68 }
69 if len(sp.s1) <= targetIndex {
70 return "s1 length too short"
71 }
72
73 // The challenge will be marked as solved if this function is called directly after a call to AppendS1
74 // that resulted in its array being expanded so that modifying s2 will not modify s1.
75 sp.s2[targetIndex] = r
76 if sp.s2[targetIndex] != sp.s1[targetIndex] {
77 solver.MarkSolved("grow", std.PrevRealm().Addr())
78 return "bingo"
79 }
80
81 return "nope"
82}
Solution
Calculate how many times to call AppendS1
before calling ModifyS2Idx
such that the value at the target index differs
due to one of the slices' underlying arrays to have been grown while the other has not. Using maketx run
for this
solution is optional.
1package main
2
3import "gno.land/r/challenges/overandover/p2"
4
5func main() {
6 p2.AppendS1("abcde")
7 p2.AppendS1("abcde")
8 p2.AppendS1("abcde")
9
10 r := 'f'
11 for {
12 if p2.ModifyS2Idx(r) == "bingo" {
13 break
14 }
15
16 r++
17 p2.AppendS1("abcde")
18 }
19}
Predicting Quantum Leap
The purpose of these challenges is to highlight gno's guaranteed determinism -- primarily around how the current time is calculated. This series of challenges require participants to predict the next value with ever increasing difficulty.
Phase 1
Prompt
Sam is tired of jumping to random places in space and time without knowing where he’s going next, so he asks his friend Al to help him
jump in a more predictable manner by guessing the time of the jump correctly. Luckily Gno execution is deterministic and the result of
time.Now()
will be the same no matter how many times it is called within a transaction.
The realm path for this challenge is:
gno.land/r/challenges/notsorandom/p1
Clues
- Guessing the next block time might be tricky
- Perhaps using
gnokey maketx run
could help pass the correct time string
Annotated Realm Code
1package p1
2
3import (
4 "std"
5 "time"
6
7 "gno.land/r/system/solver"
8)
9
10// Render shows the current time in the web UI.
11func Render(_ string) string {
12 return time.Now().Format("2006-01-02 15:04:05")
13}
14
15// WhatTimeIsItNow marks the challenge as solved if the time provided matches
16// the current time.
17func WhatTimeIsItNow(solution string) string {
18 if solution != time.Now().Format("2006-01-02 15:04:05") {
19 panic("nope")
20 }
21
22 solver.MarkSolved("timenow", std.PrevRealm().Addr())
23 return "bingo"
24}
Solution
This challenge can be solved manually by observing the time being rendered and trying to predict what the next time will be. The time in gno.land is actually the block time, so this is not a continuous value and is only changed with the production of each new block.
An alternate, and more robust solution, is to write a main function and execute it using gnokey maketx run
:
1package main
2
3import (
4 "time"
5
6 "gno.land/r/challenges/notsorandom/p1"
7)
8
9func main() {
10 p1.WhatTimeIsItNow(time.Now().Format("2006-01-02 15:04:05"))
11}
Phase 2
Prompt
That last prediction was spot on. This next one is a bit more complicated, but doable.
The realm path for this challenge is:
gno.land/r/challenges/notsorandom/p2
Clues
- The general approach should be the same as the last challenge. If you didn't use
gnokey maketx run
, maybe now is a good time to start. - If
time.Now()
is deterministic, then the operations on the integer value should also be deterministic
Annotated Realm Code
1package p2
2
3import (
4 "std"
5 "time"
6
7 "gno.land/r/system/solver"
8)
9
10const seed = 0xab94<<4*011 - 0b111001
11
12// Render shows the current time in the web UI.
13func Render(_ string) string {
14 return time.Now().Format("2006-01-02 15:04:05")
15}
16
17// YouCallThatObfuscationQuestionMark marks the challenge as solved if the time string
18// provided matches the obfuscated string of the current time.
19func YouCallThatObfuscationQuestionMark(solution string) string {
20 if solution != obfuscate() {
21 panic("nope")
22 }
23
24 solver.MarkSolved("timeobfus", std.PrevRealm().Addr())
25 return "bingo"
26}
27
28// obfuscate returns an obfuscated version of the current time.
29func obfuscate() string {
30 value := time.Now().Unix()/seed<<5 + 42 + 06630<<17 + 0x9992288
31 return time.Unix(value, 0).Format("2006-01-02 15:04:05")
32}
Solution
The easiest solution is to write a main function that gets the current time and applies the same transformations as the obfuscate
function, then pass that value to YouCallThatObfuscationQuestionMark
; it is only slightly more difficult than Phase 1.
1package main
2
3import (
4 "time"
5
6 "gno.land/r/challenges/notsorandom/p2"
7)
8
9const seed = 0xab94<<4*011 - 0b111001
10
11func main() {
12 value := time.Now().Unix()/seed<<5 + 42 + 06630<<17 + 0x9992288
13 p2.YouCallThatObfuscationQuestionMark(time.Unix(value, 0).Format("2006-01-02 15:04:05"))
14}
Phase 3
Prompt
Two down, one to go. This is getting harder. There is something interfering with the space-time values used to make the jump calculations. Some physicists say that quantum particles exhibit proof that the universe is non-deterministic, but you watched a few Youtube videos on the subject, so you're qualified to disagree.
Clues
- Doing this all in one transaction is key --
gnokey maketx run
? - What is that mask doing? Is it possible to retrieve the contents of a zero-length slice?
- Don't let bitwise operators scare you; what is one of XOR's key properties?
Annotated Realm Code
1package p3
2
3import (
4 "std"
5 "time"
6
7 "gno.land/r/system/solver"
8)
9
10var (
11 value string
12 lastValue string
13
14 mask = [20]int64{
15 0x8839,
16 0x4002,
17 0x7777,
18 0x6338,
19 0x6664,
20 0x8394,
21 0x1109,
22 0x9999,
23 0x4879,
24 0x6639,
25 0x0320,
26 0x8111,
27 0x3994,
28 0xdead,
29 0xabcb,
30 0xab89,
31 0xff87,
32 0xf998,
33 0xdeff,
34 0xddd8,
35 }
36)
37
38func init() {
39 // Set the initial value to the current time, shuffle the mask, then
40 // set the next value computed using the mask.
41 value = time.Now().Format("2006-01-02 15:04:05")
42 shuffleMask()
43 _ = LastValue()
44}
45
46// Render shows the current time in the web UI.
47func Render(_ string) string {
48 return time.Now().Format("2006-01-02 15:04:05")
49}
50
51// Mask returns a zero length slice of the mask value.
52func Mask() []int64 {
53 return mask[:0]
54}
55
56// LastValue computes the next value, sets is, and returns the previous value.
57// It shuffles the mask after computing the new value.
58func LastValue() string {
59 lastValue, value = value, computeValue()
60 shuffleMask()
61
62 return lastValue
63}
64
65// shuffleMask shuffles the mask using the current time as a seed.
66func shuffleMask() {
67 now := time.Now().Unix()
68 for i := 0; i < len(mask); i++ {
69 rnd := now % mask[i]
70 rnd += mask[i] + 91
71 rnd %= int64(len(mask))
72 mask[i], mask[rnd] = mask[rnd], mask[i]
73 }
74}
75
76// computeValue computes the next value based on the current value, time, and mask.
77func computeValue() string {
78 lvalue, err := time.Parse("2006-01-02 15:04:05", value)
79 if err != nil {
80 panic("unexpected parse error: " + err.Error())
81 }
82
83 newValue := lvalue.Unix()
84 newValue += mask[time.Now().Unix()%int64(len(mask))]
85 newValue /= 2
86 return time.Unix(newValue, 0).Format("2006-01-02 15:04:05")
87}
88
89// UnmaskMeIfYouWant marks the challenge as solved if the solution matches the last
90// computed value and the mask value is correct.
91func UnmaskMeIfYouWant(solution string, cowMask int64) string {
92 if solution != value {
93 panic("nope")
94 }
95
96 solutionTime, err := time.Parse("2006-01-02 15:04:05", solution)
97 if err != nil {
98 panic("unexpected parse error: " + err.Error())
99 }
100
101 if ^(solutionTime.Unix())^cowMask != 0xdeadbeef {
102 panic("nope")
103 }
104
105 solver.MarkSolved("timemasked", std.PrevRealm().Addr())
106 return "bingo"
107}
Solution
This challenge requires participants to predict what the next value will be. In order to do this, it is necessary to know the mask that will be used to do the calculation. This can be obtained by retrieving the mask value and expanding it to its full capacity so all elements are visible.
Next, the same obfuscation must be applied using the last value and the current time.
To submit the final answer, call the UnmaskMeIfYouWant
function with the predicted next value as well as another value that is calculated using bitwise operators. The solution expects (NOT next_value) XOR cow_mask == 0xdeadbeef
. The property of XOR can be leveraged here to do the opposite to produce the expected value -- NOT (next_value XOR 0xdeadbeef)
.
1package main
2
3import (
4 "time"
5
6 "gno.land/r/challenges/notsorandom/p3"
7)
8
9func main() {
10 origMask := p3.Mask()[:20]
11 mask := make([]int64, 20)
12 copy(mask, origMask)
13
14 lastValueStr := p3.LastValue()
15 newTime, err := time.Parse("2006-01-02 15:04:05", lastValueStr)
16 if err != nil {
17 panic("couldn't parse time: " + err.Error())
18 }
19
20 newValue := newTime.Unix()
21 newValue += mask[time.Now().Unix()%int64(len(mask))]
22 newValue /= 2
23 newValueStr := time.Unix(newValue, 0).Format("2006-01-02 15:04:05")
24 p3.UnmaskMeIfYouWant(newValueStr, ^(newValue ^ 0xdeadbeef))
25}
Final Words
We enjoyed coming up with these challenges and were happy to contribute to the GopherCon Challenge Series -- from coming up with the challenges, theming them, locking down certain gno.land features, and setting up infrastructure -- a lot of work went into this. We hope all participants were able to learn a bit more about gno.land and had fun doing it. Hopefully we'll be back next year 😁
Tags: #gnoland #gophercon #gc24 #challenge-series
Written by deelawn on 26 Aug 2024
Published by g125em6arxsnj49vx35f0n0z34putv5ty3376fg5 to gno.land's blog