292 lines
10 KiB
Python
292 lines
10 KiB
Python
#!/usr/bin/python3
|
|
"""
|
|
================================================================================================
|
|
|
|
The VAA Processor Program
|
|
|
|
(c) 2021 Randlabs, Inc.
|
|
|
|
------------------------------------------------------------------------------------------------
|
|
|
|
This program is the core client to signed VAAs from Wormhole, working in tandem with the
|
|
verify-vaa.teal stateless programs.
|
|
|
|
The following application calls are available.
|
|
|
|
setvphash: Set verify program hash.
|
|
|
|
Must be part of group:
|
|
|
|
verify: Verify guardian signature subset i..j, works in tandem with stateless program.
|
|
Arguments: #0 guardian public keys subset i..j (must match stored in global state)
|
|
#1 guardian signatures subset i..j
|
|
TX Note: payload to verify
|
|
Last verification step (the last TX in group) triggers the VAA commiting stage,
|
|
where we decide what to do based on the payload.
|
|
------------------------------------------------------------------------------------------------
|
|
|
|
Global state:
|
|
|
|
"vphash" : Hash of verification program logic
|
|
"gsexp" : Guardian set expiration time
|
|
"gscount" : Guardian set size
|
|
"vssize" : Verification step size.
|
|
key N : address of guardian N
|
|
|
|
------------------------------------------------------------------------------------------------
|
|
Stores in scratch:
|
|
|
|
SLOT 255: number of guardians in set
|
|
================================================================================================
|
|
|
|
"""
|
|
from pyteal.ast import *
|
|
from pyteal.types import *
|
|
from pyteal.compiler import *
|
|
from pyteal.ir import *
|
|
from globals import *
|
|
import sys
|
|
|
|
GUARDIAN_ADDRESS_SIZE = 20
|
|
METHOD = Txn.application_args[0]
|
|
VERIFY_ARG_GUARDIAN_KEY_SUBSET = Txn.application_args[1]
|
|
VERIFY_ARG_GUARDIAN_SET_SIZE = Txn.application_args[2]
|
|
VERIFY_ARG_PAYLOAD = Txn.note()
|
|
SLOTID_TEMP_0 = 251
|
|
SLOTID_VERIFIED_BIT = 254
|
|
STATELESS_LOGIC_HASH = App.globalGet(Bytes("vphash"))
|
|
NUM_GUARDIANS = App.globalGet(Bytes("gscount"))
|
|
SLOT_VERIFIED_BITFIELD = ScratchVar(TealType.uint64, SLOTID_VERIFIED_BIT)
|
|
SLOT_TEMP = ScratchVar(TealType.uint64, SLOTID_TEMP_0)
|
|
|
|
# defined chainId/contracts
|
|
|
|
GOVERNANCE_CHAIN_ID = 1
|
|
GOVERNANCE_EMITTER_ID = '00000000000000000000000000000000000000000000'
|
|
PYTH2WORMHOLE_CHAIN_ID = 1
|
|
PYTH2WORMHOLE_EMITTER_ID = '0x71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b'
|
|
|
|
# VAA fields
|
|
|
|
VAA_RECORD_EMITTER_CHAIN_POS = 4
|
|
VAA_RECORD_EMITTER_CHAIN_LEN = 4
|
|
VAA_RECORD_EMITTER_ADDR_POS = 10
|
|
VAA_RECORD_EMITTER_ADDR_LEN = 32
|
|
|
|
# -------------------------------------------------------------------------------------------------
|
|
|
|
|
|
@Subroutine(TealType.uint64)
|
|
# Arg0: Bootstrap with the initial list of guardians packed.
|
|
# Arg1: Expiration time in second argument.
|
|
# Arg2: Guardian set Index.
|
|
#
|
|
# Guardian public keys are 20-bytes wide, so
|
|
# using arguments a maximum 1000/20 ~ 200 public keys can be specified in this version.
|
|
def bootstrap():
|
|
guardian_count = ScratchVar(TealType.uint64)
|
|
i = SLOT_TEMP
|
|
return Seq([
|
|
Assert(Txn.application_args.length() == Int(3)),
|
|
Assert(Len(Txn.application_args[0]) %
|
|
Int(GUARDIAN_ADDRESS_SIZE) == Int(0)),
|
|
guardian_count.store(
|
|
Len(Txn.application_args[0]) / Int(GUARDIAN_ADDRESS_SIZE)),
|
|
Assert(guardian_count.load() > Int(0)),
|
|
For(i.store(Int(0)), i.load() < guardian_count.load(), i.store(i.load() + Int(1))).Do(
|
|
App.globalPut(Itob(i.load()), Extract(
|
|
Txn.application_args[0], i.load() * Int(GUARDIAN_ADDRESS_SIZE), Int(GUARDIAN_ADDRESS_SIZE)))
|
|
),
|
|
App.globalPut(Bytes("gscount"), guardian_count.load()),
|
|
App.globalPut(Bytes("gsexp"), Btoi(Txn.application_args[1])),
|
|
App.globalPut(Bytes("gsindex"), Btoi(Txn.application_args[2])),
|
|
App.globalPut(Bytes("vssize"), Int(MAX_SIGNATURES_PER_VERIFICATION_STEP)),
|
|
Approve()
|
|
])
|
|
|
|
|
|
@Subroutine(TealType.uint64)
|
|
def is_creator():
|
|
return Txn.sender() == Global.creator_address()
|
|
|
|
|
|
@Subroutine(TealType.uint64)
|
|
def check_guardian_key_subset():
|
|
# Verify that the passed argument for guardian keys [i..j] match the
|
|
# global state for the same keys.
|
|
#
|
|
i = SLOT_TEMP
|
|
sig_count = ScratchVar(TealType.uint64)
|
|
idx_base = ScratchVar(TealType.uint64)
|
|
return Seq([
|
|
idx_base.store(Int(MAX_SIGNATURES_PER_VERIFICATION_STEP) * Txn.group_index()),
|
|
sig_count.store(get_sig_count_in_step(Txn.group_index(), NUM_GUARDIANS)),
|
|
For(i.store(Int(0)),
|
|
i.load() < sig_count.load(),
|
|
i.store(i.load() + Int(1))).Do(
|
|
If(
|
|
App.globalGet(Itob(i.load() + idx_base.load())) != Extract(VERIFY_ARG_GUARDIAN_KEY_SUBSET,
|
|
i.load() * Int(GUARDIAN_ADDRESS_SIZE),
|
|
Int(GUARDIAN_ADDRESS_SIZE))).Then(Return(Int(0))) # get and compare stored global key
|
|
),
|
|
Return(Int(1))
|
|
])
|
|
|
|
|
|
@Subroutine(TealType.uint64)
|
|
def check_guardian_set_size():
|
|
#
|
|
# Verify that the passed argument for guardian set size matches the global state.
|
|
#
|
|
return NUM_GUARDIANS == Btoi(VERIFY_ARG_GUARDIAN_SET_SIZE)
|
|
|
|
|
|
@Subroutine(TealType.uint64)
|
|
def handle_governance():
|
|
return Int(1)
|
|
|
|
|
|
@Subroutine(TealType.uint64)
|
|
def handle_pyth_price_ticker():
|
|
return Int(1)
|
|
|
|
|
|
@Subroutine(TealType.uint64)
|
|
#
|
|
# Unpack the verified VAA payload and process it according to
|
|
# the source based by emitterChainId, emitterAddress.
|
|
#
|
|
def commit_vaa():
|
|
chainId = Btoi(Extract(VERIFY_ARG_PAYLOAD, Int(
|
|
VAA_RECORD_EMITTER_CHAIN_POS), Int(VAA_RECORD_EMITTER_CHAIN_LEN)))
|
|
emitterId = Extract(VERIFY_ARG_PAYLOAD, Int(
|
|
VAA_RECORD_EMITTER_ADDR_POS), Int(VAA_RECORD_EMITTER_ADDR_LEN))
|
|
return Seq([
|
|
If(And(
|
|
chainId == Int(GOVERNANCE_CHAIN_ID),
|
|
emitterId == Bytes(GOVERNANCE_EMITTER_ID))).Then(
|
|
Return(handle_governance()))
|
|
.ElseIf(And(
|
|
chainId == Int(PYTH2WORMHOLE_CHAIN_ID),
|
|
emitterId == Bytes('base16', PYTH2WORMHOLE_EMITTER_ID)
|
|
)).Then(
|
|
Return(handle_pyth_price_ticker())
|
|
).Else(
|
|
Return(Int(0))
|
|
)
|
|
])
|
|
|
|
|
|
@Subroutine(TealType.uint64)
|
|
def check_final_verification_state():
|
|
#
|
|
# Verifies that previous steps had set their verification bits.
|
|
#
|
|
i = SLOT_TEMP
|
|
return Seq([
|
|
For(i.store(Int(1)),
|
|
i.load() < Global.group_size(),
|
|
i.store(i.load() + Int(1))).Do(Seq([
|
|
Assert(Gtxn[i.load()].type_enum() == TxnType.ApplicationCall),
|
|
Assert(Gtxn[i.load()].application_id() == Txn.application_id()),
|
|
Assert(GetBit(ImportScratchValue(i.load() - Int(1), SLOTID_VERIFIED_BIT), i.load() - Int(1)) == Int(1))
|
|
])
|
|
),
|
|
Return(Int(1))
|
|
])
|
|
|
|
|
|
def setvphash():
|
|
#
|
|
# Sets the hash of the verification stateless program.
|
|
#
|
|
|
|
return Seq([
|
|
Assert(is_creator()),
|
|
Assert(Global.group_size() == Int(1)),
|
|
Assert(Txn.application_args.length() == Int(2)),
|
|
Assert(Len(Txn.application_args[1]) == Int(32)),
|
|
App.globalPut(Bytes("vphash"), Txn.application_args[1]),
|
|
Approve()
|
|
])
|
|
|
|
|
|
def verify():
|
|
# * Sender must be stateless logic.
|
|
# * Let N be the number of signatures per verification step, for the TX(i) in group, we verify signatures [j..k] where j = i*N, k = j+(N-1)
|
|
# * Argument 0 must contain guardian public keys for guardians [i..j] (read by stateless logic).
|
|
# Public keys are 32 bytes long so expected argument length is 32 * (j - i + 1)
|
|
# * Argument 1 must contain current guardian set size (read by stateless logic)
|
|
# * Passed guardian public keys [i..j] must match the current global state.
|
|
# * Note must contain VAA message-in-digest (header+payload) (up to 1KB) (read by stateless logic)
|
|
#
|
|
# Last TX in group will trigger VAA handling depending on payload. It is required that
|
|
# all previous transactions are app-calls for this AppId and all bitfields are set.
|
|
|
|
return Seq([
|
|
SLOT_VERIFIED_BITFIELD.store(Int(0)),
|
|
Assert(Global.group_size() == get_group_size(NUM_GUARDIANS)),
|
|
Assert(Txn.application_args.length() == Int(3)),
|
|
Assert(Txn.sender() == STATELESS_LOGIC_HASH),
|
|
Assert(check_guardian_set_size()),
|
|
Assert(check_guardian_key_subset()),
|
|
SLOT_VERIFIED_BITFIELD.store(
|
|
SetBit(SLOT_VERIFIED_BITFIELD.load(), Txn.group_index(), Int(1))),
|
|
If(Txn.group_index() == Global.group_size() -
|
|
Int(1)).Then(
|
|
Return(Seq([
|
|
Assert(check_final_verification_state()),
|
|
commit_vaa()
|
|
]))),
|
|
Approve()])
|
|
|
|
|
|
def vaa_processor_program():
|
|
handle_create = Return(bootstrap())
|
|
handle_update = Return(is_creator())
|
|
handle_delete = Return(is_creator())
|
|
handle_noop = Cond(
|
|
[METHOD == Bytes("setvphash"), setvphash()],
|
|
[METHOD == Bytes("verify"), verify()],
|
|
)
|
|
return Cond(
|
|
[Txn.application_id() == Int(0), handle_create],
|
|
[Txn.on_completion() == OnComplete.UpdateApplication, handle_update],
|
|
[Txn.on_completion() == OnComplete.DeleteApplication, handle_delete],
|
|
[Txn.on_completion() == OnComplete.NoOp, handle_noop]
|
|
)
|
|
|
|
|
|
def clear_state_program():
|
|
return Int(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
approval_outfile = "teal/wormhole/build/vaa-processor-approval.teal"
|
|
clear_state_outfile = "teal/wormhole/build/vaa-processor-clear.teal"
|
|
|
|
if len(sys.argv) >= 2:
|
|
approval_outfile = sys.argv[1]
|
|
|
|
if len(sys.argv) >= 3:
|
|
clear_state_outfile = sys.argv[2]
|
|
|
|
print("VAA Processor Program, (c) 2021-22 Randlabs Inc. ")
|
|
print("Compiling approval program...")
|
|
|
|
with open(approval_outfile, "w") as f:
|
|
compiled = compileTeal(vaa_processor_program(),
|
|
mode=Mode.Application, version=5)
|
|
f.write(compiled)
|
|
|
|
print("Written to " + approval_outfile)
|
|
print("Compiling clear state program...")
|
|
|
|
with open(clear_state_outfile, "w") as f:
|
|
compiled = compileTeal(clear_state_program(),
|
|
mode=Mode.Application, version=5)
|
|
f.write(compiled)
|
|
|
|
print("Written to " + clear_state_outfile)
|