팔렛을 수정하여 오프체인 워커를 포함시키고, 오프체인 워커에서 온체인 상태를 업데이트하기 위해 트랜잭션을 제출하는 방법을 설명합니다.
이 튜토리얼은 팔렛을 수정하여 오프체인 워커를 포함시키고, 오프체인 워커가 온체인 상태를 업데이트하는 트랜잭션을 제출할 수 있도록 팔렛과 런타임을 구성하는 방법을 설명합니다.
오프체인 워커 사용하기
오프체인 워커를 사용하여 장기 실행 계산 또는 오프라인 소스에서 데이터를 가져오는 경우, 해당 작업의 결과를 온체인에 저장하고자 할 것입니다. 그러나 오프체인 스토리지는 온체인 리소스와 별개이며, 오프체인 워커가 처리한 데이터를 직접 온체인 스토리지에 저장할 수는 없습니다. 오프체인 워커에서 처리한 데이터를 온체인 상태의 일부로 저장하려면, 오프체인 워커 스토리지에서 데이터를 온체인 스토리지 시스템으로 전송하는 트랜잭션을 생성해야 합니다.
이 튜토리얼은 오프체인 데이터를 온체인에 저장하기 위해 서명된 또는 서명되지 않은 트랜잭션을 제출할 수 있는 오프체인 워커를 생성하는 방법을 설명합니다. 일반적으로 서명된 트랜잭션은 보안성이 높지만, 호출하는 계정이 트랜잭션 수수료를 처리해야 합니다. 예를 들어:
트랜잭션 호출자를 기록하려고 하지만 호출자가 트랜잭션 수수료 지불에 대한 책임을 지지 않으려면 서명된 페이로드를 포함한 서명되지 않은 트랜잭션을 사용하세요.
서명되지 않은 트랜잭션 사용하기
서명되지 않은 트랜잭션을 서명된 페이로드 없이 제출하는 것도 가능합니다. 이는 트랜잭션 호출자를 기록하지 않으려는 경우에 사용될 수 있습니다. 그러나 서명되지 않은 트랜잭션을 사용하여 체인 상태를 수정하는 것은 상당한 위험이 따릅니다. 서명되지 않은 트랜잭션은 악의적인 사용자가 악용할 수 있는 잠재적인 공격 경로를 제공합니다. 오프체인 워커가 서명되지 않은 트랜잭션을 전송할 수 있도록 허용하는 경우, 트랜잭션이 인가되었는지 확인하는 로직을 포함해야 합니다. 서명되지 않은 트랜잭션이 온체인 상태를 확인하여 검증되는 방법에 대한 예제는 enact_authorized_upgrade 호출의 ValidateUnsigned 구현을 참조하세요. 이 예제에서는 주어진 코드 해시가 이전에 인가되었는지 확인하여 서명되지 않은 트랜잭션을 검증합니다.
또한, 서명된 페이로드를 가진 서명되지 않은 트랜잭션도 악용될 수 있습니다. 오프체인 워커는 트랜잭션의 유효성을 확인하기 위해 엄격한 로직을 구현하지 않는 한 신뢰할 수 있는 소스로 가정할 수 없습니다. 대부분의 경우, 저장소에 쓰기 전에 트랜잭션이 오프체인 워커에 의해 제출되었는지 확인하는 것만으로는 네트워크를 보호하기에 충분하지 않습니다. 오프체인 워커를 보호하기 위해 엄격한 로직을 구현하지 않는 한, 오프체인 워커를 신뢰할 수 있다고 가정해서는 안 됩니다. 대신 오프체인 워커의 프로세스에 대한 액세스와 수행할 수 있는 작업을 제한하는 제한적인 권한을 명시적으로 설정해야 합니다.
서명되지 않은 트랜잭션은 사실상 런타임으로의 개방된 문입니다. 트랜잭션이 실행되는 조건을 신중히 고려한 후에만 사용해야 합니다. 보호장치 없이는 악의적인 사용자가 오프체인 워커로 위장하고 런타임 스토리지에 액세스할 수 있습니다.
#[pallet::hooks]impl<T:Config> Hooks<BlockNumberFor<T>> forPallet<T> {/// 오프체인 워커 진입점.////// `fn offchain_worker`를 구현함으로써 새로운 오프체인 워커를 선언합니다./// 이 함수는 노드가 완전히 동기화되고 새로운 최상의 블록이 성공적으로 가져올 때 호출됩니다./// 모든 블록에서 오프체인 워커가 실행되는 것은 보장되지 않으며, 일부 블록이 건너뛰어지거나 워커가 두 번 실행되는(재조직) 경우가 있을 수 있으므로 이를 처리할 수 있는 코드여야 합니다.fnoffchain_worker(block_number: T::BlockNumber) { log::info!("팔렛-ocw에서 안녕하세요.");// 오프체인 워커에 의해 호출되는 코드의 진입점 }// ...}
app_crypto 매크로는 KEY_TYPE로 식별되는 sr25519 서명을 가진 계정을 선언합니다. 이 예시에서 KEY_TYPE은 demo입니다. 이 매크로는 새로운 계정을 생성하지 않습니다. 이 매크로는 팔렛에서 사용할 수 있는 crypto 계정이 있는 것을 선언하는 것입니다.
오프체인 워커가 사용할 서명된 트랜잭션을 온체인 스토리지에 전송하기 위해 사용할 계정을 초기화합니다.
use codec::Encode;use sp_runtime::{generic::Era, SaturatedConversion};// ...impl<LocalCall> frame_system::offchain::CreateSignedTransaction<LocalCall> forRuntimewhereRuntimeCall:From<LocalCall>,{fncreate_transaction<C: frame_system::offchain::AppCrypto<Self::Public, Self::Signature>>( call:RuntimeCall, public: <SignatureasVerify>::Signer, account:AccountId, nonce:Nonce, ) ->Option<(RuntimeCall, <UncheckedExtrinsicas traits::Extrinsic>::SignaturePayload)> {let tip =0;// take the biggest period possible.let period =BlockHashCount::get().checked_next_power_of_two().map(|c| c /2).unwrap_or(2) asu64;let current_block =System::block_number().saturated_into::<u64>()// The `System::block_number` is initialized with `n+1`,// so the actual block number is `n`..saturating_sub(1);let era =Era::mortal(period, current_block);let extra = ( frame_system::CheckNonZeroSender::<Runtime>::new(), frame_system::CheckSpecVersion::<Runtime>::new(), frame_system::CheckTxVersion::<Runtime>::new(), frame_system::CheckGenesis::<Runtime>::new(), frame_system::CheckEra::<Runtime>::from(era), frame_system::CheckNonce::<Runtime>::from(nonce), frame_system::CheckWeight::<Runtime>::new(), pallet_transaction_payment::ChargeTransactionPayment::<Runtime>::from(tip), );let raw_payload =SignedPayload::new(call, extra).map_err(|e| { log::warn!("서명된 페이로드를 생성할 수 없습니다: {:?}", e); }).ok()?;let signature = raw_payload.using_encoded(|payload| C::sign(payload, public))?;let address = account;let (call, extra, _) = raw_payload.deconstruct();Some((call, (sp_runtime::MultiAddress::Id(address), signature, extra))) }}
이 코드는 길지만, 기본적으로 다음 주요 단계를 보여줍니다:
extra의 SignedExtra 유형을 생성하고 준비하고, 다양한 체커를 설정합니다.
전달된 call과 extra를 기반으로 원시(raw) 페이로드를 생성합니다.
계정 공개 키로 원시(raw) 페이로드에 서명합니다.
모든 데이터를 번들로 묶어 호출, 호출자, 서명 및 서명 확장 데이터를 포함하는 튜플을 반환합니다.
이제 팔렛의 오프체인 워커가 서명된 트랜잭션을 제출할 수 있도록 준비되었습니다. 팔렛을 준비하는 데는 다음 단계가 필요합니다:
--dev 명령줄 옵션을 사용하여 개발 모드에서 노드를 실행하는 경우, 개발 계정을 위한 계정 키를 수동으로 생성하고 삽입합니다. 이를 위해 node/src/service.rs 파일을 수정합니다.
pubfnnew_partial(config:&Configuration) ->Result <SomeStruct, SomeError> {//...if config.offchain_worker.enabled {// 오프체인 워커가 트랜잭션을 서명하는 데 사용할 수 있도록 편의를 위해 트랜잭션 서명에 사용할 시드를 초기화합니다.// 학습자가 트랜잭션을 제출할 수 있도록 노드를 실행하기만 하면 트랜잭션을 볼 수 있습니다.// 일반적으로 이러한 키는 RPC 호출을 통해 `author_insertKey`에 삽입되어야 합니다. sp_keystore::SyncCryptoStore::sr25519_generate_new(&*keystore, node_template_runtime::pallet_your_ocw_pallet::KEY_TYPE,Some("//Alice"), ).expect("Creating key with account Alice should succeed."); }
}
이 예시에서는 `Alice` 계정의 키를 `KEY_TYPE`에 정의된 팔렛의 키스토어에 추가합니다.
작동하는 예제는 [service.rs](https://github.com/jimmychu0807/substrate-offchain-worker-demo/blob/v2.0.0/node/src/service.rs#L87-L105) 파일을 참조하세요.
- 다른 계정을 사용하는 경우, `subkey`와 같은 도구를 사용하여 오프체인 워커가 사용할 계정을 생성한 다음, 해당 키를 노드 키스토어에 추가할 수 있습니다.
- 체인 스펙 파일의 구성을 수정합니다.
- `author_insertKey` RPC 메서드를 사용하여 매개변수를 전달합니다.
예를 들어, [Polkadot/Substrate Portal](https://polkadot.js.org/apps/#/rpc), Polkadot-JS API 또는 `curl` 명령을 사용하여 `author_insertKey` 메서드를 선택하고, 키 유형, 비밀 문구 및 공개 키 매개변수를 지정할 수 있습니다.
![`author_insertKey` 메서드를 사용하여 계정 삽입](/media/images/docs/author_insertKey.png)
이 예제에서 `demo` 키 유형은 팔렛에서 선언한 `KEY_TYPE`과 일치합니다.
이제 팔렛의 오프체인 워커가 서명된 트랜잭션을 온체인에 제출할 수 있습니다.
## 서명되지 않은 트랜잭션
Substrate에서는 기본적으로 모든 서명되지 않은 트랜잭션을 거부합니다.
Substrate에서 특정 서명되지 않은 트랜잭션을 허용하려면 팔렛에 `ValidateUnsigned` 트레이트를 구현해야 합니다.
서명되지 않은 트랜잭션을 제출하려면 `ValidateUnsigned` 트레이트를 구현해야 하지만, 이 체크만으로 **오프체인 워커만** 트랜잭션을 제출할 수 있다는 것을 보장할 수는 없습니다.
이러한 트랜잭션은 항상 악용 가능한 잠재적인 공격 경로를 제공하며, 오프체인 워커가 신뢰할 수 있는 소스로 가정할 수 없습니다.
서명되지 않은 트랜잭션이 오프체인 워커에 의해 제출된 것인지 확인하기 위해 항상 추가적인 보호장치 또는 검증 로직을 구현해야 합니다.
오프체인 워커가 서명되지 않은 트랜잭션을 전송할 수 있도록 허용하려면 다음 단계를 수행하세요.
### 팔렛 구성
오프체인 워커가 서명되지 않은 트랜잭션을 전송할 수 있도록 하려면:
1. 팔렛의 `src/lib.rs` 파일을 텍스트 편집기로 엽니다.
2. [`validate_unsigned`](https://paritytech.github.io/substrate/master/frame_support/attr.pallet.html#validate-unsigned-palletvalidate_unsigned-optional) 매크로를 추가합니다.
예시:
```rust
#[pallet::validate_unsigned]
impl<T: Config> ValidateUnsigned for Pallet<T> {
type Call = Call<T>;
/// 이 모듈에 대한 서명되지 않은 호출을 검증합니다.
///
/// 기본적으로 서명되지 않은 트랜잭션은 허용되지 않지만, 여기에서 유효성 검사기를 구현함으로써 특정 호출(오프체인 워커가 생성한 호출)을 화이트리스트에 추가하고 유효한 것으로 표시할 수 있습니다.
fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity {
//...
}
}
다음과 같이 트레이트를 구현합니다.
fnvalidate_unsigned(source:TransactionSource, call:&Self::Call) ->TransactionValidity {let valid_tx =|provide|ValidTransaction::with_tag_prefix("my-pallet").priority(UNSIGNED_TXS_PRIORITY) // 이 줄 이전에 `UNSIGNED_TXS_PRIORITY`를 정의하세요.and_provides([&provide]).longevity(3).propagate(true).build();// ...}
호출되는 익스트린식을 확인하여 호출이 허용되는지 확인하고, 호출이 허용되는 경우 ValidTransaction을 반환하거나 호출이 허용되지 않는 경우 TransactionValidityError를 반환합니다.
offchain_worker 함수에서 서명자를 호출한 다음 트랜잭션을 전송하는 함수를 호출합니다.
#[pallet::hooks]impl<T:Config> Hooks<BlockNumberFor<T>> forPallet<T> {/// 오프체인 워커 진입점.fnoffchain_worker(block_number: T::BlockNumber) {let value:u64=10;// 페이로드에 서명하기 위해 서명자를 검색합니다.let signer =Signer::<T, T::AuthorityId>::any_account();// `send_unsigned_transaction`의 반환 타입은 `Option<(Account<T>, Result<(), ()>)>`입니다.// 반환된 결과는 다음과 같습니다:// - `None`: 트랜잭션을 전송할 수 있는 계정이 없음// - `Some((account, Ok(())))`: 트랜잭션이 성공적으로 전송됨// - `Some((account, Err(())))`: 트랜잭션 전송 중 오류 발생ifletSome((_, res)) = signer.send_unsigned_transaction(// 이 줄은 페이로드를 준비하고 반환합니다.|acct|Payload { number, public: acct.public.clone() },|payload, signature| RuntimeCall::some_extrinsics { payload, signature }, ) {match res {Ok(()) => log::info!("서명된 페이로드를 포함한 서명되지 않은 트랜잭션 성공적으로 전송."),Err(()) => log::error!("서명된 페이로드를 포함한 서명되지 않은 트랜잭션 전송 실패."), }; } else {// `None`인 경우: 트랜잭션을 전송할 수 있는 계정이 없음 log::error!("로컬 계정을 찾을 수 없음"); } }}
이 코드는 signer를 검색한 다음 send_unsigned_transaction()을 두 개의 함수 클로저와 함께 호출합니다. 첫 번째 함수 클로저는 사용할 페이로드를 반환하고, 두 번째 함수 클로저는 페이로드와 서명이 포함된 온체인 호출을 반환합니다. 이 호출은 Option<(Account<T>, Result<(), ()>)> 결과 타입을 반환하여 다음 결과를 처리할 수 있습니다:
이 예제는 SignedPayload을 사용하여 페이로드의 공개 키가 제공된 시그니처와 동일한지 확인합니다. 그러나 이 예제의 코드는 제공된 signature가 payload 내에 포함된 public 키에 대해 유효한지만 확인합니다. 이 검사는 사인한 페이로드를 사용하여 상태를 수정하는 권한이 있는지 여부를 확인하지 않습니다. 이 간단한 검사는 권한이 없는 사용자가 사인된 페이로드를 사용하여 상태를 수정하는 것을 방지하지 않습니다.