#If you have input bytes and output bytes from randomness beacon how would you verify off chain?

96 messages · Page 1 of 1 (latest)

fresh dust
#

Title

#

I believe I’ll need to import some library and also get the proof somehow, python preferred but any language is fine

fresh dust
fresh dust
#

Or an example of vrf_verify would be great 🙂

#

@delicate marsh

rustic pulsar
#

giorgio halp

fresh dust
#

Maybe if anyone is familiar with this?:

    def verify_proof(
        self,
        input_bytes: Bytes,
        proof: Bytes,
        public_key: Bytes
    ) -> tuple[Bytes, bool]:
        
    
        ensure_budget(required_budget=6000, fee_source=OpUpFeeSource.GroupCredit)
        random_bytes, correct = op.vrf_verify(op.VrfVerify.VrfAlgorand, input_bytes, proof, public_key)

        return random_bytes, correct```

```app_client.compose_call(
    atc,
    call_abi_method='verify_proof',
    input_bytes=bytearray([216, 189, 26, 61, 137, 191, 5, 153, 133, 167, 223, 254, 84, 254, 108, 37, 145, 245, 191, 151]),
    proof=b'\x00\x00\x00\x00\x02\xe3\xff\xb8\x00\x00\x00\x00\x02\xe3\xf9\xd8\xc7\x8d\xb5Xs\x86\x81\xdcx>\xa0\\z\x15\xfc\x0c\x1f\nbS\xe2\xbb\xbfB@\x9b~\xdc\xf2(9\x17\xf4\xae\x8e\xbb\xbc\xba\xae/\xf9>\x9b_\xcfG\xd1mpl\x02\tp\x18\xae\x1bC/\x86\x1f \xaaa\x18',
    public_key=decode_address('ELJEB3OYX325FATYL765AM5ZSJPSWZX745TYM5KCLTTSHJN2BJSHEMQ2JE'),
)```

```algosdk.error.AlgodHTTPError: TransactionPool.Remember: transaction A5MN3P6YG4BQJJB23ITC5FGKFWKASOJDOK3YAFY2CVHM4KB3HAZQ: logic eval error: proto was executed without a callsub. Details: app=733564386, pc=285, opcodes=uncover 3; retsub; label5:; proto 3 2```
#
      "subroutine": "vrf.get_onchain_randomness.verify_proof",
      "params": {
        "input_bytes#0": "bytes",
        "proof#0": "bytes",
        "public_key#0": "bytes"
      },
      "block": "verify_proof",
      "stack_in": [],
      "op": "proto 3 2"
    },```
#

This is how I get the proof for the VRF round:

global_states = algod_client.application_info(vrf_app_id)['params']['global-state']
for state in global_states:
    if state['key'] == '':
        vrf_proof = b64decode(state['value']['bytes'])
        print(vrf_proof)```
#

Its the only global state in the randomness beacon that is 80 bytes long, which the docs say is how long the proof is

#
    def get_random_bytes(
        self,
        round: UInt64,
        input_data: Bytes,
        block_seed: String
    ) -> tuple[UInt64, Bytes, String, DynamicBytes]:
        

        result, txn = abi_call[DynamicBytes](
            'must_get(uint64,byte[])byte[]',
            round,
            input_data,
            app_id=self.vrf_app_id,
        )

        return round, input_data, block_seed, result.copy()```

I'm saving the block seed for the VRF round in the abi results by passing it in if needed, that way its always accessible in the logs
#

As I noticed in the TEAL for randomness beacon it may use sha512_256 on the input_bytes + block seed concatenated? Not sure if I need that, but stuck on PC code 285 for now

delicate marsh
#

Hello there, I'm not sure I understand what you are trying to do. Why are you trying to verify it off-chain?
The proto error outside of a callsub looks extra strange. Is this compiled algorand python with no modifications? What version of the stack are you on?

#

On the RB, the global state with an empty key (what the manual calls the main slot) does not contain a proof. It contains the concatenation of two 8-bytes numbers and a 64-byte VRF public key.

#

All other global state slots contain a VRF output (which is not the proof). The proof is never actually saved onto the state because once the output is verified there's no use for it anymore

#

Every VRF output saved onto state is already verified so there's no need to save the proof

fresh dust
#

@delicate marsh this method looks like it may only work until the VRF round expires?

#

The submit method

fresh dust
#

Trying to verify off-chain was something I got into when I was getting the proto error


def sha512_256(data: bytes) -> str:
    hash_obj = hashlib.new("sha512_256")
    hash_obj.update(data)
    return hash_obj.digest()

def sha3_256(data: bytes) -> str:
    hash_obj = hashlib.sha3_256()
    hash_obj.update(data)
    return hash_obj.hexdigest()


test_round_bytes = b64decode('AAAAAALl7jA=')
test_proof = b64decode('r5Pk400IrCVoAc1CvY+Nm84JfBiysafYK4EMoI40xQ00Giv+vkFFNWYb8sdVMyXQhpslT6jGDV6Dbs3Ons4+zk6JDwZHMMCPoOoWr37hQQA=')
block_seed_bytes = b64decode(algod_client.block_info(int.from_bytes(test_round_bytes, 'big'))['block']['seed'])

global_states = algod_client.application_info(vrf_app_id)['params']['global-state']
for state in global_states:
    if state['key'] == '':
        public_key = b64decode(state['value']['bytes'])[16:48]
        
        
data = test_round_bytes + block_seed_bytes + test_proof + public_key
output = sha512_256(data)[0:32]
user_data = bytearray([171, 251, 35, 149, 52, 254, 178, 9, 195, 21, 237, 204, 24, 41, 127, 64, 40, 22, 212, 128])
result = list(b64decode(sha3_256(output + test_round_bytes + user_data)))
print(result)```

Which is producing an incorrect result atm
#

I'm using AVM version 10

#

I got the proof by following the submit app calls for the VRF round-- the output was

[215, 78, 28, 123, 93, 223, 111, 199, 30, 219, 150, 188, 231, 94, 118, 121, 174, 184, 243, 173, 28, 221, 231, 218, 109, 182, 219, 227, 102, 156, 113, 231, 189, 235, 167, 154, 217, 199, 124, 105, 189, 252, 107, 158, 253, 239, 182, 220]

When it should have been:

[176, 62, 103, 55, 140, 18, 142, 35, 205, 208, 105, 25, 40, 240, 210, 129, 184, 246, 235, 24, 85, 160, 78, 247, 135, 72, 42, 187, 8, 110, 209, 127]

delicate marsh
#

Well no I believe the output is truncated somewhere

#

Just because the randomness beacon doesn't need that much enthropy

#

Hold on a sec

#

Actually now that I think about it, if you want to verify something off-chain you could always run a read-only app call (which is done by using simulate)

#

That way you don't have to mess about with different PK/proof/output representations and weird C wrappers for Python

#

Also, what is this?

data = test_round_bytes + block_seed_bytes + test_proof + public_key
output = sha512_256(data)[0:32]

fresh dust
#

from the randomness beacon

#

Using the proof, input bytes, and block seed for that VRF round

#

If you're sure the method that is read-only doesn't expire that could work, or I could even replicate the logic if I had a reference

#

Do you remember what the method was called?

#

Also Gm ☕

delicate marsh
#

The submit method on the Randomness Beacon is not read-only. It changes the state of the app by saving the truncated output therefore it can't be read-only

#

All read-only methods are called with simulate but the inverse is not true. You can still call a non read-only method with simulate and fake arguments. You can even the signature step on a simulate app call

#

If you want to verify a VRF proof off-chain, I would just write a smart contract that does what you want and then only call it with a simulate call on a node you trust is running the original AVM

fresh dust
#

Ahh I see

delicate marsh
#

If you want to write the verification algorithm yourself, you should at least do it in golang using gnark because otherwise you are in a world of EC point representation pain

fresh dust
#

Nono lol, but does the vrf_verify opcode only work with simulate then?

fresh dust
#

But didn’t simulate

fresh dust
#

Sorry I sent you a bunch of stuff at the time lol

delicate marsh
#

That's why I asked what did you use to compile it because that error about proto outside of callsub is sus

fresh dust
delicate marsh
#

algokit -v? That's not it. I meant to ask which version of puyapy you are on

fresh dust
#

Hmmm…. I’ll have to check in a moment

#

If I’m remembering correctly it did autocomplete which may indicate a sufficient version for puyapy but I’ll double check shortly

#
AlgoKit: 2.5.2
AlgoKit Python: 3.13.1 (tags/v3.13.1:0671451, Dec  3 2024, 19:06:28) [MSC v.1942 64 bit (AMD64)] (location: C:\Users\atsoc\AppData\Local\Programs\Python\Python313)
OS: Windows-11-10.0.26100-SP0
docker: 27.4.0
docker compose: 2.31.0-desktop.2
git: 2.47.1.windows.2
python: 3.13.1 (location: C:\Users\atsoc\AppData\Local\Programs\Python\Python313\python.EXE)
python3: Command not found!
pipx: 1.7.1
poetry: 2.0.1
node: 22.13.1
npm: 10.9.2
winget: 1.9.25200```
delicate marsh
#

Ok. I doubt that's the issue so in the meantime I'll also point out that the VRF public key is saved in the main slot of the randomness beacon concat how I linked before (I doubt that it's what you did public_key=decode_address('ELJEB3OYX325FATYL765AM5ZSJPSWZX745TYM5KCLTTSHJN2BJSHEMQ2JE')

#

The proof you want to extract it from a real transaction to the randomness beacon

fresh dust
#
for state in global_states:
    if state['key'] == '':
        public_key = b64decode(state['value']['bytes'])[16:48]
        ```

Output:
```Y6G3KWDTQ2A5Y6B6UBOHUFP4BQPQUYST4K536QSATN7NZ4RIHELZUT5PRE```
#

Ahh yes but I didnt use this public key in my method initially

#

algosdk.error.AlgodHTTPError: TransactionPool.Remember: transaction ZYJ3567QZLJOWOK6LNLHUZBQSCOLGRQ7PK62QR26SLGJL5MFYVUA: logic eval error: proto was executed without a callsub. Details: app=733564386, pc=285, opcodes=uncover 3; retsub; label5:; proto 3 2

#
    atc,
    call_abi_method='verify_proof',
    input_bytes=bytearray([216, 189, 26, 61, 137, 191, 5, 153, 133, 167, 223, 254, 84, 254, 108, 37, 145, 245, 191, 151]),
    proof=b'\x00\x00\x00\x00\x02\xe3\xff\xb8\x00\x00\x00\x00\x02\xe3\xf9\xd8\xc7\x8d\xb5Xs\x86\x81\xdcx>\xa0\\z\x15\xfc\x0c\x1f\nbS\xe2\xbb\xbfB@\x9b~\xdc\xf2(9\x17\xf4\xae\x8e\xbb\xbc\xba\xae/\xf9>\x9b_\xcfG\xd1mpl\x02\tp\x18\xae\x1bC/\x86\x1f \xaaa\x18',
    public_key=decode_address('Y6G3KWDTQ2A5Y6B6UBOHUFP4BQPQUYST4K536QSATN7NZ4RIHELZUT5PRE'),
)
#
    def verify_proof(
        self,
        input_bytes: Bytes,
        proof: Bytes,
        public_key: Bytes
    ) -> tuple[Bytes, bool]:
        
    
        ensure_budget(required_budget=6000, fee_source=OpUpFeeSource.GroupCredit)
        random_bytes, correct = op.vrf_verify(op.VrfVerify.VrfAlgorand, input_bytes, proof, public_key)

        return random_bytes, correct```
#

The proof I got from the latest transaction before the VRF round, but I'm not sure if that error comes from an incorrect proof

#
    "285": {
      "subroutine": "vrf.get_onchain_randomness.verify_proof",
      "params": {
        "input_bytes#0": "bytes",
        "proof#0": "bytes",
        "public_key#0": "bytes"
      },
      "block": "verify_proof",
      "stack_in": [],
      "op": "proto 3 2"
    },```
#
    atc,
    call_abi_method='verify_proof',
    input_bytes=bytearray([216, 189, 26, 61, 137, 191, 5, 153, 133, 167, 223, 254, 84, 254, 108, 37, 145, 245, 191, 151]),
    proof=b'\x00\x00\x00\x00\x02\xe3\xff\xb8\x00\x00\x00\x00\x02\xe3\xf9\xd8\xc7\x8d\xb5Xs\x86\x81\xdcx>\xa0\\z\x15\xfc\x0c\x1f\nbS\xe2\xbb\xbfB@\x9b~\xdc\xf2(9\x17\xf4\xae\x8e\xbb\xbc\xba\xae/\xf9>\x9b_\xcfG\xd1mpl\x02\tp\x18\xae\x1bC/\x86\x1f \xaaa\x18',
    public_key=decode_address('Y6G3KWDTQ2A5Y6B6UBOHUFP4BQPQUYST4K536QSATN7NZ4RIHELZUT5PRE'),
)

sim_req = SimulateRequest(txn_groups=atc.build_group())
results = atc.simulate(algod_client, sim_req)
print(results.simulate_response)```

Failure response:

```op\Projects\vrf\3_call_verify.py' 
{'last-round': 48714885, 'txn-groups': [{'app-budget-added': 1400, 'app-budget-consumed': 41, 'failed-at': [0], 'failure-message': 'transaction AQ7J4EOSNQJEV44HP5Z7IJSOXU7DZAXQTDDZZPPDLD45NSCI2OGQ: logic eval error: proto was executed without a callsub. Details: app=733564386, pc=285, opcodes=uncover 3; retsub; label5:; proto 3 2',

Same thing when simulating as well

delicate marsh
#

I believe the VRF public key is 64 bytes long

#

Yeah I just confirmed it because the main slot is 80 bytes long

fresh dust
#

Hmm

for state in global_states:
    if state['key'] == '':
        public_key = b64decode(state['value']['bytes'])
        print(public_key)
        print(len(public_key))```


```b'\x00\x00\x00\x00\x02\xe7U@\x00\x00\x00\x00\x02\xe7O`\xc7\x8d\xb5Xs\x86\x81\xdcx>\xa0\\z\x15\xfc\x0c\x1f\nbS\xe2\xbb\xbfB@\x9b~\xdc\xf2(9\x17\xf4\xae\x8e\xbb\xbc\xba\xae/\xf9>\x9b_\xcfG\xd1mpl\x02\tp\x18\xae\x1bC/\x86\x1f \xaaa\x18'
80```
#

Yeah

#

16:80 then?

delicate marsh
#

Sure

fresh dust
#

Tried with [16:80] and [0:64] for the verify_proof method above, still returning the same error

delicate marsh
#

I think the issue is that you are not increasing the group fees leading to the contract perhaps not making the ensure budget inner calls

#

It's 16:80. Don't need to try 0:64

fresh dust
#

Oh, got it working

delicate marsh
#

Oh cool. What was the issue

fresh dust
#

I removed the vrf_verify op code

#
    def verify_my_proof(
        self,
        input_bytes: Bytes,
        proof: Bytes,
        public_key: Bytes
    ) -> tuple[Bytes, bool]:
        
    
        ensure_budget(required_budget=6000, fee_source=OpUpFeeSource.GroupCredit)
      #  random_bytes, correct = op.vrf_verify(op.VrfVerify.VrfAlgorand, input_bytes, proof, public_key)

        return input_bytes, False```
#

Works now

#

Jk lol

delicate marsh
#

Can you paste the TEAL for both?

fresh dust
#

So I changed the method from 'verify_proof' to 'verify_my_proof'

#
    def verify_my_proof(
        self,
        input_bytes: Bytes,
        proof: Bytes,
        public_key: Bytes
    ) -> tuple[Bytes, bool]:
        
    
        ensure_budget(required_budget=6000, fee_source=OpUpFeeSource.GroupCredit)
        random_bytes, correct = op.vrf_verify(op.VrfVerify.VrfAlgorand, input_bytes, proof, public_key)

        return random_bytes, correct```
delicate marsh
#

the compiled code I mean

#

Not the bytecode, the file .teal

fresh dust
#

Sure

#
    // vrf.py:29
    // @abimethod
    txn OnCompletion
    !
    assert // OnCompletion is not NoOp
    txn ApplicationID
    assert // can only call when not creating
    // vrf.py:6
    // class get_onchain_randomness(ARC4Contract):
    txna ApplicationArgs 1
    extract 2 0
    // vrf.py:29
    // @abimethod
    dup
    len
    itob
    extract 6 2
    swap
    concat
    pushbytes 0x000300
    swap
    concat
    bytec_0 // 0x151f7c75
    swap
    concat
    log
    intc_0 // 1
    return```
delicate marsh
#

Is this the whole file?

fresh dust
#

Calling for VRF requested for 48716202, where the next VRF round is 48716204

https://lora.algokit.io/testnet/transaction/QCNVQL7UNGXAEZN4VAL2CAXRJRSYQSWBGEESDNXM62WTKMVZRVAQ

with input bytes:

[251, 149, 255, 191, 188, 243, 54, 219, 84, 37, 161, 31, 27, 28, 63, 186, 64, 252, 150, 49]

I check for the proof on LORA and find this TX from randomness beacon host at round 48716204:

https://lora.algokit.io/testnet/transaction/4MD3EI4VFXTJUG2CMED7LRL3O4JKOKKKEYLLOQ5SA5MGHNPZUV3Q

Where the proof arg is the 3rd app arg, and the first arg is 'pJ1KeA==' :

and the public key is bytes [16:80] in global state " "

b'\xc7\x8d\xb5Xs\x86\x81\xdcx>\xa0\\z\x15\xfc\x0c\x1f\nbS\xe2\xbb\xbfB@\x9b~\xdc\xf2(9\x17\xf4\xae\x8e\xbb\xbc\xba\xae/\xf9>\x9b_\xcfG\xd1mpl\x02\tp\x18\xae\x1bC/\x86\x1f \xaaa\x18'```

Using this info on my 'verify_my_proof' method it returns the bytes (input bytes) and False. I also tried the nearest next tx in case there was a delay:

```[[251, 149, 255, 191, 188, 243, 54, 219, 84, 37, 161, 31, 27, 28, 63, 186, 64, 252, 150, 49], False]```

```input_bytes = bytes(bytearray([251, 149, 255, 191, 188, 243, 54, 219, 84, 37, 161, 31, 27, 28, 63, 186, 64, 252, 150, 49]))
proof = b64decode('HygleKYlU9g6TWcN9YSPpiLPPvQ0d/jLbQTtEr6U1TYZ9vuwg+MhQIDNu1uVgYhRU5q2tbruxBC8hVKTCSDN+cRJD6lJ+nBX6aEGux19ngw=')
public_key=b'\xc7\x8d\xb5Xs\x86\x81\xdcx>\xa0\\z\x15\xfc\x0c\x1f\nbS\xe2\xbb\xbfB@\x9b~\xdc\xf2(9\x17\xf4\xae\x8e\xbb\xbc\xba\xae/\xf9>\x9b_\xcfG\xd1mpl\x02\tp\x18\xae\x1bC/\x86\x1f \xaaa\x18'


app_client.compose_call(
    atc,
    call_abi_method='verify_my_proof',
    input_bytes=input_bytes,
    proof=proof,
    public_key=public_key,
)

results = atc.execute(algod_client, 2)
abi_results = [results.abi_results[i].return_value for i in range(len(results.abi_results))][0]
tx_ids = results.tx_ids[0]

print(abi_results)
print(tx_ids)```
#

Ignore block seed if you see it, its not actually doing anything

delicate marsh
#

With regards to the proto 3 2 error, we confirmed offline that it's a compilation optimization issue. With optimization disabled that error shouldn't be there

delicate marsh
tepid canyon
#

Thank you @delicate marsh !