본문으로 건너뛰기
전체 글

Solana 프로그램 배포는 왜 여러 거래로 나뉠까

· 약 9분
Bankware Global Engineering

배포할 프로그램 파일은 하나다. 그런데 배포를 시작하자 거래가 수십 번, 수백 번 발생한다. 마지막 거래까지 보내기 전에 연결이 끊기면, 이미 올라간 부분은 어떻게 될까? 파일을 다시 보내면 되는지, 남은 조각만 보내면 되는지부터 판단해야 한다.

스마트 컨트랙트 배포 계획에서는 앞에서 만든 계약의 주소를 다음 계약의 입력으로 연결했다. Solana에서는 프로그램 하나를 올리는 작업 자체가 여러 거래로 나뉠 수 있다. 프로그램 파일을 체인에 기록하는 단계와, 그 파일을 실행할 프로그램으로 배치하는 단계가 분리되기 때문이다.

BXB의 Solana 배포 코드를 따라 이 과정을 살펴보자. 설명 범위는 업그레이드 가능한 프로그램을 관리하는 loader-v3 경로다. 아래 숫자는 실제 배포 측정값이 아니라, 분할 전송과 결과 확인을 설명하기 위한 예제다.

1. 파일 하나를 올리기 위해 거래 102개를 준비한다​

정상적으로 빌드된 90,000바이트의 프로그램 파일이 있다고 하자. Solana에서 실행할 코드가 담긴 .so 파일이며, 이번에 처음 배포할 프로그램의 주소를 P라고 부르겠다. 배포 지갑 D에는 수수료와 계정 생성에 필요한 SOL이 충분히 있고, 배포에 필요한 권한도 있다고 가정한다. 프로그램에 고객 자산을 넣거나 업무 데이터를 초기화하는 작업은 이 예제에 포함하지 않는다.

이 파일 전체를 거래 하나에 담을 수는 없다. 거래에는 파일 내용 외에도 서명, 사용할 계정의 주소, 명령 정보와 최근 블록을 식별하는 blockhash가 들어간다. Solana 거래 문서는 기존 거래 형식의 크기 한도를 1,232바이트로 설명한다. 여기서는 그 한도에 맞춰 구성된 BXB 배포 경로를 다룬다. 새 거래 형식의 크기 한도와 이 구현의 전송 단위는 구별해야 한다.

확인한 BXB 설정의 기본값은 파일 내용 900바이트당 Write 거래 하나다. 900바이트는 Solana 프로그램 파일의 규격이 아니라, 거래의 다른 필드를 함께 담기 위해 선택한 전송 단위다. 따라서 이 예제에서는 90,000바이트를 100조각으로 나눈다.

정상적으로 한 번씩만 제출되는 최초 배포라면 거래 구성은 다음과 같다.

단계하는 일거래 수
준비임시 buffer 생성과 초기화1
기록900바이트씩 100조각 쓰기100
배포Program 생성과 최종 배포1

합계는 102개다. 재전송이나 실패 후 정리 거래가 생기면 더 늘어난다. 이는 배포 시간이나 비용의 측정값도, 모든 프로그램의 고정 거래 수도 아니다. 파일 크기와 전송 단위가 바뀌면 Write 거래 수도 달라진다.

2. 먼저 코드를 담아 둘 buffer가 필요하다​

여기서 buffer는 서버 메모리가 아니라 Solana 체인 위의 임시 계정이다. 파일의 조각을 이 계정에 기록하고, 모두 모인 뒤 최종 배포에 사용한다.

loader-v3의 계정 세 가지를 구분하면 이 과정이 보인다.

계정역할
Buffer배포할 프로그램 바이트를 임시로 모은다.
Program프로그램 주소 P에 해당하며 ProgramData를 가리킨다.
ProgramData배포된 코드와 업그레이드 권한 정보를 보관한다.

이 구조와 배포·업그레이드 명령은 Solana 프로그램 배포 문서에 설명돼 있다. 이 글의 P는 호출 대상인 Program의 주소다. 임시 buffer의 주소와는 다르다. 일반 업무 데이터를 담는 계정 역시 이 세 역할과 구분된다.

BXB는 배포를 시작할 때 새 buffer 계정을 위한 키 쌍을 만들고, 계정 생성과 InitializeBuffer를 같은 거래에 넣는다. 생성과 권한 설정을 하나의 거래로 묶어, 생성만 된 계정을 다음 거래에서 따로 초기화하는 중간 단계를 없앤다. 이때 buffer에 쓸 수 있는 권한은 배포 지갑 D로 지정한다.

계정을 만드는 데 참여한 buffer의 키와, 이후 내용을 수정하는 권한자의 키는 역할이 다르다. 계정 생성 거래에는 D와 buffer의 서명이 필요하지만, Write 거래에는 buffer 권한자로 지정된 D가 서명한다. buffer의 주소를 안다는 것만으로 그 안의 코드를 바꿀 수 있는 것은 아니다.

3. 조각의 도착 순서보다 기록할 위치가 중요하다​

Write 명령은 기록할 위치와 바이트를 지정하는 형태다. 위치를 나타내는 offset은 프로그램 파일의 시작을 0으로 세었을 때의 바이트 위치다.

Write(offset=0, bytes=파일의 0~899)
Write(offset=900, bytes=파일의 900~1799)
...
Write(offset=89100, bytes=파일의 89100~89999)

각 조각이 자신의 자리를 알고 있으므로 서로 다른 범위를 쓰는 거래가 제출 순서대로 실행돼야만 파일이 완성되는 것은 아니다. 다만 여러 거래가 같은 buffer를 수정하므로, 동시에 제출한다고 해서 체인에서 모두 병렬 실행된다는 뜻은 아니다.

BXB의 기본 설정은 Write 거래를 4개씩 묶어 제출하고 다음 묶음을 보내기 전에 간격을 두는 방식이다. 묶음마다 새로운 blockhash를 받아 거래를 구성한다. 여기서 묶음은 전송을 조절하는 단위다. 거래 4개가 하나의 원자적 거래로 합쳐지는 것은 아니다. 그중 일부만 기록될 수도 있다.

buffer의 실제 계정 데이터 앞에는 상태와 권한을 담는 37바이트의 메타데이터가 있다. Write의 offset=0은 그 메타데이터 다음에 놓이는 프로그램 바이트의 시작을 뜻한다. BXB가 계정 내용을 읽어 원본 파일과 비교할 때도 이 앞부분을 제외한다. 파일 내 위치와 계정 데이터 내 위치를 섞으면 같은 조각을 다른 위치와 비교하게 된다.

전송 묶음의 크기와 간격은 RPC, 즉 체인 노드에 요청하는 통로의 부하를 조절한다. 그러나 전송 속도를 조절하는 것만으로 모든 조각이 기록됐다고 판단할 수는 없다. 다음 단계에는 실제로 무엇이 남았는지 읽는 작업이 필요하다.

4. 응답 개수가 아니라 실제 바이트를 확인한다​

100조각을 보냈는데 buffer를 읽어 보니, 파일의 36,000~36,899 위치에 해당하는 900바이트가 원본과 다르다고 하자. 일부 전송 응답을 받지 못했거나 거래가 아직 반영되지 않았을 수 있다. 이 예제에서 알고 있는 사실은 그 범위의 내용이 아직 원본과 일치하지 않는다는 것이다.

BXB는 한 기록 회차에서 대상 조각들을 묶음 단위로 제출한 뒤 buffer를 조회하고, 메타데이터를 제외한 내용을 원본 파일과 대조한다. 일정 시간 안에 전체 내용이 일치하면 기록 단계를 마친다. 계속 다른 부분이 있으면 다른 조각만 추려 다음 기록 회차에서 다시 보낸다. 예제에서는 offset=36000에 같은 900바이트를 쓰는 거래가 다음 대상이 된다.

이 방식에서는 거래의 응답을 잃었어도 해당 바이트가 이미 기록돼 있으면 다시 쓸 필요가 없다. 반대로 제출 응답을 받았더라도 buffer의 내용이 다르면 기록 완료로 넘어가지 않는다. 중요한 것은 “전송 함수가 몇 번 성공했는가”보다 “원하는 파일이 그곳에 있는가”다.

같은 위치에 같은 바이트를 다시 쓰는 것은 파일 내용을 누적하지 않는다. 따라서 이 재시도는 토큰을 추가로 보내는 송금 재시도와 성격이 다르다. 다만 재전송된 거래에도 수수료가 들 수 있다. 거래 식별자와 업무 결과를 구분하는 이유는 송금 요청의 타임아웃과 재시도에서도 다뤘다.

위 순환은 무한히 계속되지 않는다. 확인한 기본 설정은 최대 3회의 기록 회차를 허용한다. 그 안에 전체 내용이 맞지 않으면 배포를 실패로 끝내고 정리를 시도한다. 또한 이 동작은 진행 중인 배포 호출 안에서의 재시도다. 서버가 종료된 뒤 저장된 진행 지점부터 이어서 배포하는 기능과는 다르다. 현재 경로는 새로운 배포 호출마다 새 buffer를 만든다.

5. 파일 기록이 끝나야 배포나 업그레이드로 넘어간다​

buffer에 90,000바이트가 모두 모여도 아직 P를 호출할 수 있는 프로그램이 만들어진 것은 아니다. 임시로 모은 코드를 최종 프로그램으로 배치하는 거래가 남아 있다.

처음 배포할 때 BXB는 Program 계정 생성과 DeployWithMaxDataLen을 한 거래로 구성한다. 이 거래에는 지갑 D와 Program 계정을 만들 키의 서명이 들어간다. loader는 buffer의 코드를 검사하고 ProgramData에 배치하며, P가 그 ProgramData를 가리키게 한다. 정상 결과에서는 임시 파일 조각 대신 P를 통해 실행할 프로그램을 얻게 된다.

확인한 BXB 구현은 최초 배포 때 프로그램 파일 크기의 두 배를 코드 공간으로 요청한다. 이 예제라면 180,000바이트의 코드 공간이며, 계정 메타데이터는 별도다. 나중에 코드가 커질 여유를 미리 두는 선택이다. 그만큼 계정 크기에 따른 SOL도 필요하며, 이 여유가 어떤 크기의 업그레이드든 허용한다는 뜻은 아니다.

이미 존재하는 P를 갱신할 때는 새 buffer에 새 파일을 모은 뒤 Upgrade를 보낸다. P라는 호출 주소를 유지하면서 ProgramData의 코드를 바꾸는 흐름이다. BXB의 이 경로에서는 지갑 D가 비용 지불자이자 buffer 권한자이며 업그레이드 서명자다. 기존 ProgramData의 업그레이드 권한도 D와 맞아야 한다. 비용을 낼 수 있는 지갑이라는 이유만으로 다른 프로그램을 바꿀 수는 없다.

최초 배포와 업그레이드는 결과를 확인하는 질문도 다르다. BXB의 최초 배포 경로는 예상한 P의 계정이 생겼고 loader-v3 소유인지 조회한다. 업그레이드 경로는 P가 원래 존재하므로 이번 Upgrade 거래의 서명 식별자로 상태를 조회하고, 오류 없이 confirmed 또는 finalized에 도달했는지 확인한다. 계정이 있다는 관찰만으로 새 코드가 적용됐다고 판단하지 않는 것이다.

이 확인은 배포 단계의 결과를 판단하는 절차다. 공개 소스에서 재현한 빌드와 배포 바이트가 같은지 검증하거나, 배포된 프로그램의 업무 기능을 호출해 시험하는 작업까지 대신하지는 않는다.

6. 실패하면 남은 buffer와 비용을 함께 살펴야 한다​

이번에는 전체 기록을 끝내지 못해 최종 배포 거래를 보내기 전에 작업이 실패했다고 하자. P를 처음 만드는 예제이므로 아직 실행할 프로그램은 없다. 하지만 앞에서 만들어 둔 buffer와 일부 바이트는 체인에 남아 있을 수 있다.

buffer 생성 시 넣은 SOL은 파일 전송 수수료와 구분해야 한다. 계정을 유지하는 데 필요한 최소 잔액을 buffer에 넣었고, 그 계정을 정상적으로 닫으면 남은 잔액을 지정한 수취인에게 돌려줄 수 있다. 이미 처리된 거래의 수수료까지 되돌리는 것은 아니다.

BXB는 배포 중 예외가 나면 D의 권한으로 buffer를 닫는 거래를 자동으로 보내려고 한다. 그러나 배포가 실패할 정도로 RPC 상태가 좋지 않으면 정리 요청도 실패할 수 있다. 이 자동 정리 경로는 제출을 시도하는 수준이므로, 호출이 끝났다는 사실을 SOL 회수 완료로 읽으면 안 된다.

남은 buffer를 별도로 조회하고 닫는 경로도 있다. 여기서는 loader-v3의 Buffer 계정인지와 현재 권한자를 확인하고, 지정한 지갑이 그 권한자와 일치할 때 닫기 거래를 보낸다. 그 뒤 buffer 계정이 사라졌는지 조회한다. BXB의 이 경로에서 회수 대상은 해당 권한자 지갑이다. 계정 주소, 네트워크, 권한자를 함께 알아야 정리 대상을 잘못 고르지 않는다.

실패한 배포에서 회수할 수 있는 것은 남은 임시 계정의 잔액이다. 계속 유지할 Program과 ProgramData에 필요한 잔액, 이미 처리된 거래의 수수료와 섞어서 “배포비 전액 환불”이라고 설명할 수는 없다.

최종 배포 거래를 보낸 뒤 응답을 잃은 경우라면 상황이 또 다르다. 그때는 먼저 P와 해당 거래의 결과를 확인해야 한다. 클라이언트의 실패 응답이 체인에 프로그램이 없다는 증거는 아니며, buffer 정리도 성공한 배포를 이전 상태로 되돌리는 기능은 아니다.

7. 배포 완료는 파일 전송 완료보다 넓은 상태다​

처음의 정상 예제로 돌아가 보자. 90,000바이트를 100개의 Write 거래로 기록하고, buffer의 바이트를 확인한 뒤 최종 배포를 마쳤다. 결과는 주소 P와 그 주소에서 실행할 코드다. 고객 자산의 이동이나 업무 데이터 생성은 아직 요청하지 않았으므로 이 배포 결과에 포함되지 않는다.

운영 기록에서도 이 단계를 구분할 필요가 있다. BXB는 buffer 준비, 최초 배포 또는 업그레이드의 주요 단계에 요청·응답과 실패 기록을 연결한다. 이 기록이 모든 Write 거래의 영속 진행표를 뜻하는 것은 아니다. “어느 단계에서 멈췄는가”와 “어떤 바이트까지 기록됐는가”는 서로 다른 질문이다.

프로그램 파일 하나를 배포한다는 요청 안에는 전송 단위, 기록 위치, 서명 권한, 최종 결과 확인과 임시 계정의 정리가 들어 있다. 분할한 파일을 다시 모으는 데서 끝내지 않고, 실행할 코드가 준비됐는지와 실패 후 무엇이 남았는지까지 확인하는 것이 이 배포 흐름의 역할이다.