#!/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) return Seq([ 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())) != 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)) ]) ), 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)